1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
|
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Environment (:mod:`sigima.env`)
--------------------------------
This module defines the execution environment for `sigima` library, which is used
by Sigima test suite. It provides a way to manage execution settings, such as
unattended mode and verbosity level, and allows for parsing command line arguments
to set these settings.
.. note::
This module is intended for use in testing and development environments, and
should not be used in production code. It is designed to facilitate testing
and debugging by providing a controlled environment for running tests and
executing code.
"""
from __future__ import annotations
import argparse
import enum
import os
import pprint
import sys
from contextlib import contextmanager
from typing import Any, Generator
from guidata.env import ExecEnv as GuiDataExecEnv
# We could import DEBUG from sigima.config, but is it really worth it?
DEBUG = os.environ.get("DEBUG", "").lower() in ("1", "true")
class VerbosityLevels(enum.Enum):
"""Print verbosity levels (for testing purpose)"""
QUIET = "quiet"
NORMAL = "normal"
DEBUG = "debug"
class SigimaExecEnv:
"""Object representing `sigima` test environment"""
UNATTENDED_ARG = "unattended"
VERBOSE_ARG = "verbose"
SCREENSHOT_ARG = "screenshot"
SCREENSHOT_PATH_ARG = "screenshot_path"
UNATTENDED_ENV = GuiDataExecEnv.UNATTENDED_ENV
VERBOSE_ENV = GuiDataExecEnv.VERBOSE_ENV
SCREENSHOT_ENV = GuiDataExecEnv.SCREENSHOT_ENV
SCREENSHOT_PATH_ENV = GuiDataExecEnv.SCREENSHOT_PATH_ENV
def __init__(self):
# Check if "pytest" is in the command line arguments:
if "pytest" not in sys.argv[0]:
# Do not parse command line arguments when running tests with pytest
# (otherwise, pytest arguments are parsed as DataLab arguments)
self.parse_args()
if self.unattended: # Do not run this code in production
# Check that calling `to_dict` do not raise any exception
self.to_dict()
def iterate_over_attrs_envvars(self) -> Generator[tuple[str, str], None, None]:
"""Iterate over Sigima environment variables
Yields:
A tuple (attribute name, environment variable name)
"""
for name in dir(self):
if name.endswith("_ENV"):
envvar: str = getattr(self, name)
attrname = "_".join(name.split("_")[:-1]).lower()
yield attrname, envvar
def to_dict(self):
"""Return a dictionary representation of the object"""
# The list of properties match the list of environment variable attribute names,
# modulo the "_ENV" suffix:
props = [attrname for attrname, _envvar in self.iterate_over_attrs_envvars()]
# Check that all properties are defined in the class and that they are
# really properties:
for prop in props:
assert hasattr(self, prop), (
f"Property {prop} is not defined in class {self.__class__.__name__}"
)
assert isinstance(getattr(self.__class__, prop), property), (
f"Attribute {prop} is not a property in class {self.__class__.__name__}"
)
# Return a dictionary with the properties as keys and their values as values:
return {p: getattr(self, p) for p in props}
def __str__(self):
"""Return a string representation of the object"""
return pprint.pformat(self.to_dict())
@staticmethod
def __get_mode(env):
"""Get mode value"""
env_val = os.environ.get(env)
if env_val is None:
return False
return env_val.lower() in ("1", "true", "yes", "on", "enable", "enabled")
@staticmethod
def __set_mode(env, value):
"""Set mode value"""
if env in os.environ:
os.environ.pop(env)
if value:
os.environ[env] = "1"
@property
def unattended(self):
"""Get unattended value"""
return self.__get_mode(self.UNATTENDED_ENV)
@unattended.setter
def unattended(self, value):
"""Set unattended value"""
self.__set_mode(self.UNATTENDED_ENV, value)
@property
def screenshot(self):
"""Get screenshot value"""
return self.__get_mode(self.SCREENSHOT_ENV)
@screenshot.setter
def screenshot(self, value):
"""Set screenshot value"""
self.__set_mode(self.SCREENSHOT_ENV, value)
@property
def screenshot_path(self):
"""Get screenshot path"""
return os.environ.get(self.SCREENSHOT_PATH_ENV, "")
@screenshot_path.setter
def screenshot_path(self, value):
"""Set screenshot path"""
if value:
os.environ[self.SCREENSHOT_PATH_ENV] = str(value)
elif self.SCREENSHOT_PATH_ENV in os.environ:
os.environ.pop(self.SCREENSHOT_PATH_ENV)
@property
def verbose(self):
"""Get verbosity level"""
env_val = os.environ.get(self.VERBOSE_ENV)
if env_val in (None, ""):
return VerbosityLevels.NORMAL.value
return env_val.lower()
@verbose.setter
def verbose(self, value):
"""Set verbosity level"""
os.environ[self.VERBOSE_ENV] = value
def parse_args(self):
"""Parse command line arguments"""
parser = argparse.ArgumentParser(description="Run `sigima` tests")
parser.add_argument(
"--" + self.UNATTENDED_ARG,
action="store_true",
help="non-interactive mode",
default=None,
)
parser.add_argument(
"--" + self.SCREENSHOT_ARG,
action="store_true",
help="automatic screenshots",
default=None,
)
parser.add_argument(
"--" + self.SCREENSHOT_PATH_ARG,
type=str,
help="path to save screenshots",
default=None,
)
parser.add_argument(
"--" + self.VERBOSE_ARG,
choices=[lvl.value for lvl in VerbosityLevels],
required=False,
default=None,
help="verbosity level: for debugging/testing purpose",
)
args, _unknown = parser.parse_known_args()
self.set_env_from_args(args)
def set_env_from_args(self, args):
"""Set appropriate environment variables"""
for argname in (
self.UNATTENDED_ARG,
self.SCREENSHOT_ARG,
self.SCREENSHOT_PATH_ARG,
self.VERBOSE_ARG,
):
argvalue = getattr(args, argname)
if argvalue is not None:
setattr(self, argname, argvalue)
def log(self, source: Any, *objects: Any) -> None:
"""Log text on screen
Args:
source: object from which the log is issued
*objects: objects to log
"""
if DEBUG or self.verbose == VerbosityLevels.DEBUG.value:
print(str(source) + ":", *objects)
# TODO: [P4] Eventually, log in a file (optionally)
def print(self, *objects, sep=" ", end="\n", file=sys.stdout, flush=False):
"""Print in file, depending on verbosity level"""
if self.verbose != VerbosityLevels.QUIET.value or DEBUG:
print(*objects, sep=sep, end=end, file=file, flush=flush)
def pprint(
self,
obj,
stream=None,
indent=1,
width=80,
depth=None,
compact=False,
sort_dicts=True,
):
"""Pretty-print in stream, depending on verbosity level"""
if self.verbose != VerbosityLevels.QUIET.value or DEBUG:
pprint.pprint(
obj,
stream=stream,
indent=indent,
width=width,
depth=depth,
compact=compact,
sort_dicts=sort_dicts,
)
@contextmanager
def context(
self,
unattended=None,
screenshot=None,
verbose=None,
) -> Generator[None, None, None]:
"""Return a context manager that sets some execenv properties at enter,
and restores them at exit. This is useful to run some code in a
controlled environment, for example to accept dialogs in unattended
mode, and restore the previous value at exit.
Args:
unattended: whether to run in unattended mode
screenshot: whether to take screenshots
verbose: verbosity level
.. note::
If a passed value is None, the corresponding property is not changed.
"""
old_values = self.to_dict()
new_values = {
"unattended": unattended,
"screenshot": screenshot,
"verbose": verbose,
}
for key, value in new_values.items():
if value is not None:
setattr(self, key, value)
try:
yield
finally:
for key, value in old_values.items():
setattr(self, key, value)
execenv = SigimaExecEnv()
|