sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
sigima/tests/env.py ADDED
@@ -0,0 +1,280 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Environment (:mod:`sigima.env`)
5
+ --------------------------------
6
+
7
+ This module defines the execution environment for `sigima` library, which is used
8
+ by Sigima test suite. It provides a way to manage execution settings, such as
9
+ unattended mode and verbosity level, and allows for parsing command line arguments
10
+ to set these settings.
11
+
12
+ .. note::
13
+
14
+ This module is intended for use in testing and development environments, and
15
+ should not be used in production code. It is designed to facilitate testing
16
+ and debugging by providing a controlled environment for running tests and
17
+ executing code.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import enum
24
+ import os
25
+ import pprint
26
+ import sys
27
+ from contextlib import contextmanager
28
+ from typing import Any, Generator
29
+
30
+ from guidata.env import ExecEnv as GuiDataExecEnv
31
+
32
+ # We could import DEBUG from sigima.config, but is it really worth it?
33
+ DEBUG = os.environ.get("DEBUG", "").lower() in ("1", "true")
34
+
35
+
36
+ class VerbosityLevels(enum.Enum):
37
+ """Print verbosity levels (for testing purpose)"""
38
+
39
+ QUIET = "quiet"
40
+ NORMAL = "normal"
41
+ DEBUG = "debug"
42
+
43
+
44
+ class SigimaExecEnv:
45
+ """Object representing `sigima` test environment"""
46
+
47
+ UNATTENDED_ARG = "unattended"
48
+ VERBOSE_ARG = "verbose"
49
+ SCREENSHOT_ARG = "screenshot"
50
+ SCREENSHOT_PATH_ARG = "screenshot_path"
51
+ UNATTENDED_ENV = GuiDataExecEnv.UNATTENDED_ENV
52
+ VERBOSE_ENV = GuiDataExecEnv.VERBOSE_ENV
53
+ SCREENSHOT_ENV = GuiDataExecEnv.SCREENSHOT_ENV
54
+ SCREENSHOT_PATH_ENV = GuiDataExecEnv.SCREENSHOT_PATH_ENV
55
+
56
+ def __init__(self):
57
+ # Check if "pytest" is in the command line arguments:
58
+ if "pytest" not in sys.argv[0]:
59
+ # Do not parse command line arguments when running tests with pytest
60
+ # (otherwise, pytest arguments are parsed as DataLab arguments)
61
+ self.parse_args()
62
+ if self.unattended: # Do not run this code in production
63
+ # Check that calling `to_dict` do not raise any exception
64
+ self.to_dict()
65
+
66
+ def iterate_over_attrs_envvars(self) -> Generator[tuple[str, str], None, None]:
67
+ """Iterate over Sigima environment variables
68
+
69
+ Yields:
70
+ A tuple (attribute name, environment variable name)
71
+ """
72
+ for name in dir(self):
73
+ if name.endswith("_ENV"):
74
+ envvar: str = getattr(self, name)
75
+ attrname = "_".join(name.split("_")[:-1]).lower()
76
+ yield attrname, envvar
77
+
78
+ def to_dict(self):
79
+ """Return a dictionary representation of the object"""
80
+ # The list of properties match the list of environment variable attribute names,
81
+ # modulo the "_ENV" suffix:
82
+ props = [attrname for attrname, _envvar in self.iterate_over_attrs_envvars()]
83
+
84
+ # Check that all properties are defined in the class and that they are
85
+ # really properties:
86
+ for prop in props:
87
+ assert hasattr(self, prop), (
88
+ f"Property {prop} is not defined in class {self.__class__.__name__}"
89
+ )
90
+ assert isinstance(getattr(self.__class__, prop), property), (
91
+ f"Attribute {prop} is not a property in class {self.__class__.__name__}"
92
+ )
93
+
94
+ # Return a dictionary with the properties as keys and their values as values:
95
+ return {p: getattr(self, p) for p in props}
96
+
97
+ def __str__(self):
98
+ """Return a string representation of the object"""
99
+ return pprint.pformat(self.to_dict())
100
+
101
+ @staticmethod
102
+ def __get_mode(env):
103
+ """Get mode value"""
104
+ env_val = os.environ.get(env)
105
+ if env_val is None:
106
+ return False
107
+ return env_val.lower() in ("1", "true", "yes", "on", "enable", "enabled")
108
+
109
+ @staticmethod
110
+ def __set_mode(env, value):
111
+ """Set mode value"""
112
+ if env in os.environ:
113
+ os.environ.pop(env)
114
+ if value:
115
+ os.environ[env] = "1"
116
+
117
+ @property
118
+ def unattended(self):
119
+ """Get unattended value"""
120
+ return self.__get_mode(self.UNATTENDED_ENV)
121
+
122
+ @unattended.setter
123
+ def unattended(self, value):
124
+ """Set unattended value"""
125
+ self.__set_mode(self.UNATTENDED_ENV, value)
126
+
127
+ @property
128
+ def screenshot(self):
129
+ """Get screenshot value"""
130
+ return self.__get_mode(self.SCREENSHOT_ENV)
131
+
132
+ @screenshot.setter
133
+ def screenshot(self, value):
134
+ """Set screenshot value"""
135
+ self.__set_mode(self.SCREENSHOT_ENV, value)
136
+
137
+ @property
138
+ def screenshot_path(self):
139
+ """Get screenshot path"""
140
+ return os.environ.get(self.SCREENSHOT_PATH_ENV, "")
141
+
142
+ @screenshot_path.setter
143
+ def screenshot_path(self, value):
144
+ """Set screenshot path"""
145
+ if value:
146
+ os.environ[self.SCREENSHOT_PATH_ENV] = str(value)
147
+ elif self.SCREENSHOT_PATH_ENV in os.environ:
148
+ os.environ.pop(self.SCREENSHOT_PATH_ENV)
149
+
150
+ @property
151
+ def verbose(self):
152
+ """Get verbosity level"""
153
+ env_val = os.environ.get(self.VERBOSE_ENV)
154
+ if env_val in (None, ""):
155
+ return VerbosityLevels.NORMAL.value
156
+ return env_val.lower()
157
+
158
+ @verbose.setter
159
+ def verbose(self, value):
160
+ """Set verbosity level"""
161
+ os.environ[self.VERBOSE_ENV] = value
162
+
163
+ def parse_args(self):
164
+ """Parse command line arguments"""
165
+ parser = argparse.ArgumentParser(description="Run `sigima` tests")
166
+ parser.add_argument(
167
+ "--" + self.UNATTENDED_ARG,
168
+ action="store_true",
169
+ help="non-interactive mode",
170
+ default=None,
171
+ )
172
+ parser.add_argument(
173
+ "--" + self.SCREENSHOT_ARG,
174
+ action="store_true",
175
+ help="automatic screenshots",
176
+ default=None,
177
+ )
178
+ parser.add_argument(
179
+ "--" + self.SCREENSHOT_PATH_ARG,
180
+ type=str,
181
+ help="path to save screenshots",
182
+ default=None,
183
+ )
184
+ parser.add_argument(
185
+ "--" + self.VERBOSE_ARG,
186
+ choices=[lvl.value for lvl in VerbosityLevels],
187
+ required=False,
188
+ default=None,
189
+ help="verbosity level: for debugging/testing purpose",
190
+ )
191
+ args, _unknown = parser.parse_known_args()
192
+ self.set_env_from_args(args)
193
+
194
+ def set_env_from_args(self, args):
195
+ """Set appropriate environment variables"""
196
+ for argname in (
197
+ self.UNATTENDED_ARG,
198
+ self.SCREENSHOT_ARG,
199
+ self.SCREENSHOT_PATH_ARG,
200
+ self.VERBOSE_ARG,
201
+ ):
202
+ argvalue = getattr(args, argname)
203
+ if argvalue is not None:
204
+ setattr(self, argname, argvalue)
205
+
206
+ def log(self, source: Any, *objects: Any) -> None:
207
+ """Log text on screen
208
+
209
+ Args:
210
+ source: object from which the log is issued
211
+ *objects: objects to log
212
+ """
213
+ if DEBUG or self.verbose == VerbosityLevels.DEBUG.value:
214
+ print(str(source) + ":", *objects)
215
+ # TODO: [P4] Eventually, log in a file (optionally)
216
+
217
+ def print(self, *objects, sep=" ", end="\n", file=sys.stdout, flush=False):
218
+ """Print in file, depending on verbosity level"""
219
+ if self.verbose != VerbosityLevels.QUIET.value or DEBUG:
220
+ print(*objects, sep=sep, end=end, file=file, flush=flush)
221
+
222
+ def pprint(
223
+ self,
224
+ obj,
225
+ stream=None,
226
+ indent=1,
227
+ width=80,
228
+ depth=None,
229
+ compact=False,
230
+ sort_dicts=True,
231
+ ):
232
+ """Pretty-print in stream, depending on verbosity level"""
233
+ if self.verbose != VerbosityLevels.QUIET.value or DEBUG:
234
+ pprint.pprint(
235
+ obj,
236
+ stream=stream,
237
+ indent=indent,
238
+ width=width,
239
+ depth=depth,
240
+ compact=compact,
241
+ sort_dicts=sort_dicts,
242
+ )
243
+
244
+ @contextmanager
245
+ def context(
246
+ self,
247
+ unattended=None,
248
+ screenshot=None,
249
+ verbose=None,
250
+ ) -> Generator[None, None, None]:
251
+ """Return a context manager that sets some execenv properties at enter,
252
+ and restores them at exit. This is useful to run some code in a
253
+ controlled environment, for example to accept dialogs in unattended
254
+ mode, and restore the previous value at exit.
255
+
256
+ Args:
257
+ unattended: whether to run in unattended mode
258
+ screenshot: whether to take screenshots
259
+ verbose: verbosity level
260
+
261
+ .. note::
262
+ If a passed value is None, the corresponding property is not changed.
263
+ """
264
+ old_values = self.to_dict()
265
+ new_values = {
266
+ "unattended": unattended,
267
+ "screenshot": screenshot,
268
+ "verbose": verbose,
269
+ }
270
+ for key, value in new_values.items():
271
+ if value is not None:
272
+ setattr(self, key, value)
273
+ try:
274
+ yield
275
+ finally:
276
+ for key, value in old_values.items():
277
+ setattr(self, key, value)
278
+
279
+
280
+ execenv = SigimaExecEnv()
@@ -0,0 +1,163 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Utilities to manage GUI activation for tests executed with pytest
5
+ or as standalone scripts.
6
+
7
+ ?? This module must not import any Qt-related module at the top level,
8
+ as Qt is an optional dependency of Sigima.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import types
15
+ from contextlib import contextmanager
16
+ from typing import TYPE_CHECKING, Generator, Optional
17
+
18
+ from sigima.tests import SIGIMA_TESTS_GUI_ENV
19
+
20
+ if TYPE_CHECKING:
21
+ # ?? Type-only: no runtime Qt import
22
+ from qtpy.QtWidgets import QApplication
23
+
24
+
25
+ # Single source of truth (module-global); None means "not forced".
26
+ _FORCED_GUI: bool | None = None
27
+
28
+
29
+ def enable_gui(on: bool | None = True) -> None:
30
+ """Force GUI mode on/off (or reset to auto when None)."""
31
+ global _FORCED_GUI # pylint: disable=global-statement
32
+ _FORCED_GUI = on
33
+
34
+
35
+ def is_gui_enabled() -> bool:
36
+ """Return True if GUI mode is enabled."""
37
+ # 1) explicit override
38
+ if _FORCED_GUI is not None:
39
+ return _FORCED_GUI
40
+ # 2) pytest --gui, exposed by conftest via env var (see below)
41
+ if os.environ.get(SIGIMA_TESTS_GUI_ENV, "") in ("1", "true", "True"):
42
+ return True
43
+ return False
44
+
45
+
46
+ class DummyRequest:
47
+ """
48
+ Dummy request object to simulate pytest --gui when running a test manually.
49
+
50
+ Example usage:
51
+ test_x(request=DummyRequest(gui=True))
52
+ """
53
+
54
+ def __init__(self, gui: bool = True):
55
+ self.config = types.SimpleNamespace()
56
+ self.config.getoption = lambda name: gui if name == "--gui" else None
57
+
58
+
59
+ @contextmanager
60
+ def lazy_qt_app_context(
61
+ *, exec_loop: bool = False, force: bool | None = None
62
+ ) -> Generator[Optional[QApplication], None, None]:
63
+ """Provide a Qt app context lazily; no-op if GUI is disabled.
64
+
65
+ Args:
66
+ exec_loop: Run the Qt event loop (e.g. when showing a non-blocking widget).
67
+ force: None ? auto (use is_gui_enabled());
68
+ True ? force GUI ON (always create Qt app);
69
+ False ? force GUI OFF (no-op).
70
+
71
+ Yields:
72
+ The QApplication instance if enabled, else None.
73
+
74
+ .. note::
75
+
76
+ This context manager is useful for tests that require a Qt application context,
77
+ but should be used with caution to avoid unnecessary Qt imports. For tests
78
+ that are exclusively GUI-based, option `force=True` can be used to ensure
79
+ the Qt application context is always created. For tests that must be executable
80
+ without a GUI, option `force` may be skipped so that operations inside the
81
+ context are only performed if the GUI is enabled.
82
+ """
83
+ enabled = is_gui_enabled() if force is None else force
84
+ if not enabled:
85
+ # No Qt import, block executes as a no-op context
86
+ yield None
87
+ return
88
+
89
+ # Lazy import: only when enabled
90
+ # pylint: disable=import-outside-toplevel
91
+ from guidata.qthelpers import qt_app_context
92
+
93
+ with qt_app_context(exec_loop=exec_loop) as qt_app:
94
+ yield qt_app
95
+
96
+
97
+ def _vistools_call_if_gui(func_name: str, *args, **kwargs) -> bool:
98
+ """Call sigima.tests.vistools.<func_name>(...) only if GUI is enabled.
99
+
100
+ Returns:
101
+ True if the call executed (GUI enabled or forced), else False.
102
+ """
103
+ with lazy_qt_app_context() as app:
104
+ if app is None:
105
+ return False
106
+ from sigima.tests import vistools # pylint: disable=import-outside-toplevel
107
+
108
+ getattr(vistools, func_name)(*args, **kwargs)
109
+ return True
110
+
111
+
112
+ def view_curves_if_gui(*args, **kwargs) -> None:
113
+ """Create a curve dialog and plot curves if GUI mode enabled.
114
+
115
+ Args:
116
+ data_or_objs: Single `SignalObj` or `np.ndarray`, or a list/tuple of these,
117
+ or a list/tuple of (xdata, ydata) pairs
118
+ name: Name of the dialog, or None to use a default name
119
+ title: Title of the dialog, or None to use a default title
120
+ xlabel: Label for the x-axis, or None for no label
121
+ ylabel: Label for the y-axis, or None for no label
122
+ """
123
+ return _vistools_call_if_gui("view_curves", *args, **kwargs)
124
+
125
+
126
+ def view_images_if_gui(*args, **kwargs) -> None:
127
+ """Show sequence of images if GUI mode enabled.
128
+
129
+ Args:
130
+ data_or_objs: Single `ImageObj` or `np.ndarray`, or a list/tuple of these
131
+ name: Name of the dialog, or None to use a default name
132
+ title: Title of the dialog, or None to use a default title
133
+ xlabel: Label for the x-axis, or None for no label
134
+ ylabel: Label for the y-axis, or None for no label
135
+ """
136
+ return _vistools_call_if_gui("view_images", *args, **kwargs)
137
+
138
+
139
+ def view_curves_and_images_if_gui(*args, **kwargs) -> None:
140
+ """View signals, then images in two successive dialogs if GUI mode enabled.
141
+
142
+ Args:
143
+ data_or_objs: List of `SignalObj`, `ImageObj`, `np.ndarray` or a mix of these
144
+ name: Name of the dialog, or None to use a default name
145
+ title: Title of the dialog, or None to use a default title
146
+ xlabel: Label for the x-axis, or None for no label
147
+ ylabel: Label for the y-axis, or None for no label
148
+ """
149
+ return _vistools_call_if_gui("view_curves_and_images", *args, **kwargs)
150
+
151
+
152
+ def view_images_side_by_side_if_gui(*args, **kwargs) -> None:
153
+ """Show sequence of images side-by-side if GUI mode enabled.
154
+
155
+ Args:
156
+ images: List of `ImageItem`, `np.ndarray`, or `ImageObj` objects to display
157
+ titles: List of titles for each image
158
+ share_axes: Whether to share axes across plots, default is True
159
+ rows: Fixed number of rows in the grid, or None to compute automatically
160
+ maximized: Whether to show the dialog maximized, default is False
161
+ title: Title of the dialog, or None for a default title
162
+ """
163
+ return _vistools_call_if_gui("view_images_side_by_side", *args, **kwargs)