pymodaq_utils 0.0.2__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.
- pymodaq_utils/__init__.py +35 -0
- pymodaq_utils/abstract.py +48 -0
- pymodaq_utils/array_manipulation.py +386 -0
- pymodaq_utils/config.py +379 -0
- pymodaq_utils/enums.py +72 -0
- pymodaq_utils/factory.py +82 -0
- pymodaq_utils/icon.ico +0 -0
- pymodaq_utils/logger.py +78 -0
- pymodaq_utils/math_utils.py +584 -0
- pymodaq_utils/resources/__init__.py +0 -0
- pymodaq_utils/resources/config_template.toml +96 -0
- pymodaq_utils/resources/hatch_build_plugins.py +41 -0
- pymodaq_utils/splash.png +0 -0
- pymodaq_utils/units.py +216 -0
- pymodaq_utils/utils.py +649 -0
- pymodaq_utils/warnings.py +10 -0
- pymodaq_utils-0.0.2.dist-info/METADATA +152 -0
- pymodaq_utils-0.0.2.dist-info/RECORD +20 -0
- pymodaq_utils-0.0.2.dist-info/WHEEL +4 -0
- pymodaq_utils-0.0.2.dist-info/licenses/LICENSE +21 -0
pymodaq_utils/config.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from abc import abstractproperty
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
|
|
4
|
+
from os import environ
|
|
5
|
+
import sys
|
|
6
|
+
import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Union, Dict, TypeVar, Any, List, TYPE_CHECKING
|
|
9
|
+
from typing import Iterable as IterableType
|
|
10
|
+
|
|
11
|
+
import toml
|
|
12
|
+
import logging
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pyqtgraph import Parameter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
USER = environ['USERNAME'] if sys.platform == 'win32' else environ['USER']
|
|
19
|
+
except:
|
|
20
|
+
USER = 'unknown_user'
|
|
21
|
+
|
|
22
|
+
CONFIG_BASE_PATH = Path(environ['PROGRAMDATA']) if sys.platform == 'win32' else \
|
|
23
|
+
Path('Library/Application Support') if sys.platform == 'darwin' else Path('/etc')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
KeyType = TypeVar('KeyType')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def deep_update(mapping: Dict[KeyType, Any], *updating_mappings: Dict[KeyType, Any]) -> Dict[KeyType, Any]:
|
|
30
|
+
""" Make sure a dictionary is updated using another dict in any nested level
|
|
31
|
+
Taken from Pydantic v1
|
|
32
|
+
"""
|
|
33
|
+
updated_mapping = mapping.copy()
|
|
34
|
+
for updating_mapping in updating_mappings:
|
|
35
|
+
for k, v in updating_mapping.items():
|
|
36
|
+
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
|
|
37
|
+
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
|
38
|
+
else:
|
|
39
|
+
updated_mapping[k] = v
|
|
40
|
+
return updated_mapping
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def replace_file_extension(filename: str, ext: str):
|
|
44
|
+
"""Replace the extension of a file by the specified one, without the dot"""
|
|
45
|
+
file_name = Path(filename).stem # remove eventual extensions
|
|
46
|
+
if ext[0] == '.':
|
|
47
|
+
ext = ext[1:]
|
|
48
|
+
file_name += '.' + ext
|
|
49
|
+
return file_name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def getitem_recursive(dic, *args, ndepth=0, create_if_missing=False):
|
|
53
|
+
"""Will scan recursively a dictionary in order to get the item defined by the iterable args
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
dic: dict
|
|
58
|
+
the dictionary to scan
|
|
59
|
+
args: an iterable of str
|
|
60
|
+
keys of the dict
|
|
61
|
+
ndepth: int
|
|
62
|
+
by default (0) get the last element defined by args. 1 would mean it get the parent dict, 2 the parent of the
|
|
63
|
+
parent...
|
|
64
|
+
create_if_missing: bool
|
|
65
|
+
if the entry is not present, create it assigning the 'none' default value (as a lower case string)
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
object or dict
|
|
69
|
+
"""
|
|
70
|
+
args = list(args)
|
|
71
|
+
while len(args) > ndepth:
|
|
72
|
+
try:
|
|
73
|
+
arg = args.pop(0)
|
|
74
|
+
dic = dic[arg]
|
|
75
|
+
except KeyError as e:
|
|
76
|
+
if create_if_missing:
|
|
77
|
+
if len(args) > 0:
|
|
78
|
+
dic[arg] = {}
|
|
79
|
+
dic = dic[arg]
|
|
80
|
+
else:
|
|
81
|
+
dic[arg] = 'none'
|
|
82
|
+
dic = 'none'
|
|
83
|
+
else:
|
|
84
|
+
raise e
|
|
85
|
+
return dic
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def recursive_iterable_flattening(aniterable: IterableType):
|
|
89
|
+
flatten_iter = []
|
|
90
|
+
for elt in aniterable:
|
|
91
|
+
if not isinstance(elt, str) and isinstance(elt, Iterable):
|
|
92
|
+
flatten_iter.extend(recursive_iterable_flattening(elt))
|
|
93
|
+
else:
|
|
94
|
+
flatten_iter.append(elt)
|
|
95
|
+
return flatten_iter
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_set_path(a_base_path: Path, dir_name: str) -> Path:
|
|
99
|
+
path_to_get = a_base_path.joinpath(dir_name)
|
|
100
|
+
if not path_to_get.is_dir():
|
|
101
|
+
try:
|
|
102
|
+
path_to_get.mkdir()
|
|
103
|
+
except PermissionError as e:
|
|
104
|
+
logging.warning(f"Cannot create local config folder at this location: {path_to_get}"
|
|
105
|
+
f", try using admin rights. "
|
|
106
|
+
f"Changing the not permitted path to a user "
|
|
107
|
+
f"one: {Path.home().joinpath(dir_name)}.")
|
|
108
|
+
path_to_get = Path.home().joinpath(dir_name)
|
|
109
|
+
if not path_to_get.is_dir():
|
|
110
|
+
path_to_get.mkdir()
|
|
111
|
+
return path_to_get
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_set_local_dir(user=False) -> Path:
|
|
115
|
+
"""Defines, creates and returns a local folder where configuration files will be saved
|
|
116
|
+
|
|
117
|
+
Depending on the os the configurations files will be stored in CONFIG_BASE_PATH, then
|
|
118
|
+
each user will have another one created that could override the default and system-wide base folder
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
user: bool
|
|
123
|
+
if False get the system-wide folder, otherwise the user folder
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
Path: the local path
|
|
128
|
+
"""
|
|
129
|
+
if user:
|
|
130
|
+
local_path = get_set_path(Path.home(), '.pymodaq')
|
|
131
|
+
else:
|
|
132
|
+
local_path = get_set_path(CONFIG_BASE_PATH, '.pymodaq')
|
|
133
|
+
return local_path
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_config_file(config_file_name: str, user=False):
|
|
137
|
+
return get_set_local_dir(user).joinpath(replace_file_extension(config_file_name, 'toml'))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_set_config_dir(config_name='config', user=False):
|
|
141
|
+
"""Creates a folder in the local config directory to store specific configuration files
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
config_name: (str) name of the configuration folder
|
|
146
|
+
user: bool
|
|
147
|
+
if False get the system-wide folder, otherwise the user folder
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
Path
|
|
152
|
+
|
|
153
|
+
See Also
|
|
154
|
+
--------
|
|
155
|
+
get_set_local_dir
|
|
156
|
+
"""
|
|
157
|
+
return get_set_path(get_set_local_dir(user=user), config_name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_set_log_path():
|
|
161
|
+
""" creates and return the config folder path for log files
|
|
162
|
+
"""
|
|
163
|
+
return get_set_config_dir('log')
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def create_toml_from_dict(mydict: dict, dest_path: Path):
|
|
167
|
+
"""Create a Toml file at a given path from a dictionnary"""
|
|
168
|
+
dest_path.write_text(toml.dumps(mydict))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_config(config_base: dict, config_local: dict):
|
|
172
|
+
"""Compare two configuration dictionaries. Adding missing keys
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
config_base: dict
|
|
177
|
+
The base dictionaries with possible new keys
|
|
178
|
+
config_local: dict
|
|
179
|
+
a dict from a local config file potentially missing keys
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
bool: True if keys where missing else False
|
|
184
|
+
"""
|
|
185
|
+
status = False
|
|
186
|
+
for key in config_base:
|
|
187
|
+
if key in config_local:
|
|
188
|
+
if isinstance(config_base[key], dict):
|
|
189
|
+
status = status or check_config(config_base[key], config_local[key])
|
|
190
|
+
else:
|
|
191
|
+
config_local[key] = config_base[key]
|
|
192
|
+
status = True
|
|
193
|
+
return status
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def copy_template_config(config_file_name: str = 'config', source_path: Union[Path, str] = None,
|
|
197
|
+
dest_path: Union[Path, str] = None):
|
|
198
|
+
"""Get a toml file path and copy it
|
|
199
|
+
|
|
200
|
+
the destination is made of a given folder path (or the system-wide local path by default) and the config_file_name
|
|
201
|
+
appended by the suffix '.toml'
|
|
202
|
+
|
|
203
|
+
The source file (or pymodaq config template path by default) is read and dumped in this destination file
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
config_file_name: str
|
|
208
|
+
the name of the destination config file
|
|
209
|
+
source_path: Path or str
|
|
210
|
+
the path of the toml source to be copied
|
|
211
|
+
dest_path: Path or str
|
|
212
|
+
the destination path of the copied config
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
Path: the path of the copied file
|
|
217
|
+
"""
|
|
218
|
+
if dest_path is None:
|
|
219
|
+
dest_path = get_set_local_dir()
|
|
220
|
+
|
|
221
|
+
file_name = Path(config_file_name).stem # remove eventual extensions
|
|
222
|
+
file_name += '.toml'
|
|
223
|
+
dest_path_with_filename = dest_path.joinpath(file_name)
|
|
224
|
+
|
|
225
|
+
if source_path is None:
|
|
226
|
+
config_template_dict = {}
|
|
227
|
+
else:
|
|
228
|
+
config_template_dict = toml.load(Path(source_path))
|
|
229
|
+
|
|
230
|
+
create_toml_from_dict(config_template_dict, dest_path_with_filename)
|
|
231
|
+
return dest_path_with_filename
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def load_system_config_and_update_from_user(config_file_name: str):
|
|
235
|
+
"""load from a system-wide config file, update it from the user config file
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
config_file_name: str
|
|
240
|
+
The config file to be loaded
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
dict: contains the toml system-wide file update with the user file
|
|
244
|
+
"""
|
|
245
|
+
config_dict = dict([])
|
|
246
|
+
toml_base_path = get_config_file(config_file_name, user=False)
|
|
247
|
+
if toml_base_path.is_file():
|
|
248
|
+
config_dict = toml.load(toml_base_path)
|
|
249
|
+
toml_user_path = get_config_file(config_file_name, user=True)
|
|
250
|
+
if toml_user_path.is_file():
|
|
251
|
+
config_dict = deep_update(config_dict, toml.load(toml_user_path))
|
|
252
|
+
return config_dict
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ConfigError(Exception):
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class BaseConfig:
|
|
260
|
+
"""Base class to manage configuration files
|
|
261
|
+
|
|
262
|
+
Should be subclassed with proper class attributes for each configuration file you need with pymodaq
|
|
263
|
+
|
|
264
|
+
Attributes
|
|
265
|
+
----------
|
|
266
|
+
config_name: str
|
|
267
|
+
The name with which the configuration will be saved
|
|
268
|
+
config_template_path: Path
|
|
269
|
+
The Path of the template from which the config is constructed
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
config_template_path: Path = abstractproperty()
|
|
273
|
+
config_name: str = abstractproperty()
|
|
274
|
+
|
|
275
|
+
def __init__(self):
|
|
276
|
+
self._config = self.load_config(self.config_name, self.config_template_path)
|
|
277
|
+
|
|
278
|
+
def __repr__(self):
|
|
279
|
+
return f'{self.config_name} configuration file'
|
|
280
|
+
|
|
281
|
+
def __call__(self, *args):
|
|
282
|
+
try:
|
|
283
|
+
ret = getitem_recursive(self._config, *args)
|
|
284
|
+
except KeyError as e:
|
|
285
|
+
raise ConfigError(f'the path {args} does not exist in your configuration toml'
|
|
286
|
+
f' file, check your config folder')
|
|
287
|
+
return ret
|
|
288
|
+
|
|
289
|
+
def to_dict(self):
|
|
290
|
+
return self._config
|
|
291
|
+
|
|
292
|
+
def __getitem__(self, item):
|
|
293
|
+
"""for backcompatibility when it was a dictionnary"""
|
|
294
|
+
if isinstance(item, tuple):
|
|
295
|
+
return getitem_recursive(self._config, *item)
|
|
296
|
+
else:
|
|
297
|
+
return self._config[item]
|
|
298
|
+
|
|
299
|
+
# def __setitem__(self, key, value):
|
|
300
|
+
# if isinstance(key, tuple):
|
|
301
|
+
# dic = getitem_recursive(self._config, *key, ndepth=1, create_if_missing=False)
|
|
302
|
+
# dic[key[-1]] = value
|
|
303
|
+
# else:
|
|
304
|
+
# self._config[key] = value
|
|
305
|
+
|
|
306
|
+
def __setitem__(self, key, value):
|
|
307
|
+
if isinstance(key, tuple):
|
|
308
|
+
dic = getitem_recursive(self._config, *key, ndepth=1, create_if_missing=True)
|
|
309
|
+
if value is None: # means the setting is a group
|
|
310
|
+
value = {}
|
|
311
|
+
dic[key[-1]] = value
|
|
312
|
+
else:
|
|
313
|
+
self._config[key] = value
|
|
314
|
+
|
|
315
|
+
def load_config(self, config_file_name, template_path: Path):
|
|
316
|
+
"""Load a configuration file from both system-wide and user file
|
|
317
|
+
|
|
318
|
+
check also if missing entries in the configuration file compared to the template"""
|
|
319
|
+
toml_base_path = get_config_file(config_file_name, user=False)
|
|
320
|
+
toml_user_path = get_config_file(config_file_name, user=True)
|
|
321
|
+
if toml_base_path.is_file():
|
|
322
|
+
config = toml.load(toml_base_path)
|
|
323
|
+
if template_path is not None:
|
|
324
|
+
config_template = toml.load(template_path)
|
|
325
|
+
else:
|
|
326
|
+
config_template = {}
|
|
327
|
+
if check_config(config_template, config): # check if all fields from template are there
|
|
328
|
+
# (could have been modified by some commits)
|
|
329
|
+
create_toml_from_dict(config, toml_base_path)
|
|
330
|
+
|
|
331
|
+
else:
|
|
332
|
+
copy_template_config(config_file_name, template_path, toml_base_path.parent)
|
|
333
|
+
|
|
334
|
+
if not toml_user_path.is_file():
|
|
335
|
+
# create the author from environment variable
|
|
336
|
+
config_dict = self.dict_to_add_to_user()
|
|
337
|
+
if config_dict is not None:
|
|
338
|
+
create_toml_from_dict(config_dict, toml_user_path)
|
|
339
|
+
|
|
340
|
+
config_dict = load_system_config_and_update_from_user(config_file_name)
|
|
341
|
+
return config_dict
|
|
342
|
+
|
|
343
|
+
def dict_to_add_to_user(self):
|
|
344
|
+
"""To subclass"""
|
|
345
|
+
return dict([])
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def config_path(self):
|
|
349
|
+
"""Get the user config path"""
|
|
350
|
+
return get_config_file(self.config_name, user=True)
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def system_config_path(self):
|
|
354
|
+
"""Get the system_wide config path"""
|
|
355
|
+
return get_config_file(self.config_name, user=False)
|
|
356
|
+
|
|
357
|
+
def save(self):
|
|
358
|
+
"""Save the current Config object into the user toml file"""
|
|
359
|
+
self.config_path.write_text(toml.dumps(self.to_dict()))
|
|
360
|
+
|
|
361
|
+
def get_children(self, *path: IterableType[str]):
|
|
362
|
+
""" Get the list of config entries at a given path within the configulation toml file
|
|
363
|
+
|
|
364
|
+
new in 4.3.0
|
|
365
|
+
"""
|
|
366
|
+
return list(getitem_recursive(self._config, *path).keys())
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class Config(BaseConfig):
|
|
370
|
+
"""Main class to deal with configuration values for PyMoDAQ"""
|
|
371
|
+
config_template_path = Path(__file__).parent.joinpath('resources/config_template.toml')
|
|
372
|
+
config_name = 'config_pymodaq'
|
|
373
|
+
|
|
374
|
+
def dict_to_add_to_user(self):
|
|
375
|
+
"""To subclass"""
|
|
376
|
+
return dict(user=dict(name=USER))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
|
pymodaq_utils/enums.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseEnum(Enum):
|
|
6
|
+
"""Enum to be used within pymodaq with some utility methods"""
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def names(cls) -> List[str]:
|
|
10
|
+
"""Returns all the names of the enum"""
|
|
11
|
+
return list(cls.__members__.keys())
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def values(cls) -> List[str]:
|
|
15
|
+
"""Returns all the names of the enum"""
|
|
16
|
+
return [cls[name].value for name in cls.names()]
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def to_dict(cls):
|
|
20
|
+
""" Returns the enum in form of a dict with names os keys
|
|
21
|
+
|
|
22
|
+
New in 4.0.2
|
|
23
|
+
"""
|
|
24
|
+
return {name: cls[name].value for name in cls.names()}
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def to_dict_value(cls):
|
|
28
|
+
""" Returns the enum in form of a dict with values os keys
|
|
29
|
+
|
|
30
|
+
New in 4.0.2
|
|
31
|
+
"""
|
|
32
|
+
return {cls[name].value: name for name in cls.names()}
|
|
33
|
+
|
|
34
|
+
def __eq__(self, other: Union[str, Enum]):
|
|
35
|
+
"""testing for equality using the enum name"""
|
|
36
|
+
if isinstance(other, str):
|
|
37
|
+
if other == self.name:
|
|
38
|
+
return True
|
|
39
|
+
return super().__eq__(other)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def enum_checker(enum: BaseEnum, item: Union[BaseEnum, str]):
|
|
43
|
+
"""Check if the item parameter is a valid enum or at least one valid string name of the enum
|
|
44
|
+
|
|
45
|
+
If a string, transforms it to a valid enum (case not important)
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
enum: BaseEnum class or one of its derivated class
|
|
50
|
+
|
|
51
|
+
item: str or BaseEnum instance
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
BaseEnum class or one of its derivated class
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if not isinstance(item, enum):
|
|
59
|
+
if not isinstance(item, str):
|
|
60
|
+
raise ValueError(f'{item} is an invalid {enum}. Should be a {enum} enum or '
|
|
61
|
+
f'a string in {enum.names()}')
|
|
62
|
+
for ind, name in enumerate(enum.names()):
|
|
63
|
+
if item.lower() == name.lower():
|
|
64
|
+
item = enum[name]
|
|
65
|
+
break
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f'{item} is an invalid {enum}. Should be a {enum} enum or '
|
|
68
|
+
f'a string in {enum.names()}')
|
|
69
|
+
return item
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
pymodaq_utils/factory.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created the 04/11/2022
|
|
4
|
+
|
|
5
|
+
@author: Sebastien Weber
|
|
6
|
+
"""
|
|
7
|
+
from typing import Callable, Union
|
|
8
|
+
from abc import ABCMeta, abstractmethod
|
|
9
|
+
|
|
10
|
+
from pymodaq_utils.logger import set_logger, get_module_name
|
|
11
|
+
|
|
12
|
+
logger = set_logger(get_module_name(__file__))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BuilderBase(ABCMeta):
|
|
16
|
+
"""Abstract class defining an object/service builder with a callable interface accepting some arguments
|
|
17
|
+
|
|
18
|
+
See https://realpython.com/factory-method-python/ for some details
|
|
19
|
+
|
|
20
|
+
See Also
|
|
21
|
+
--------
|
|
22
|
+
pymodaq.post_treatment.process_1d_to_scalar
|
|
23
|
+
"""
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def __call__(self, *args, **kwargs):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ObjectFactory(metaclass=ABCMeta):
|
|
30
|
+
"""Generic ObjectFactory with a decorator register to add object builders to the factory with a
|
|
31
|
+
unique key identifier
|
|
32
|
+
|
|
33
|
+
See https://realpython.com/factory-method-python/ for some details
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
@ObjectFactory.register('custom')
|
|
38
|
+
def my_custom_builder():
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
See Also
|
|
42
|
+
--------
|
|
43
|
+
pymodaq.post_treatment.process_1d_to_scalar.Data1DProcessorFactory
|
|
44
|
+
"""
|
|
45
|
+
_builders = {}
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def register(cls, key: str) -> Callable:
|
|
49
|
+
def inner_wrapper(wrapped_class: Union[BuilderBase, Callable]) -> Callable:
|
|
50
|
+
if cls.__name__ not in cls._builders:
|
|
51
|
+
cls._builders[cls.__name__] = {}
|
|
52
|
+
if key not in cls._builders[cls.__name__]:
|
|
53
|
+
cls._builders[cls.__name__][key] = wrapped_class
|
|
54
|
+
else:
|
|
55
|
+
logger.warning(f'The {cls.__name__}/{key} builder is already registered. Replacing it')
|
|
56
|
+
return wrapped_class
|
|
57
|
+
return inner_wrapper
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def builders(self):
|
|
61
|
+
return self._builders
|
|
62
|
+
|
|
63
|
+
def keys_function(self, do_sort=True):
|
|
64
|
+
if do_sort:
|
|
65
|
+
return self.keys
|
|
66
|
+
else:
|
|
67
|
+
return list(self.builders[self.__class__.__name__].keys())
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def keys(self):
|
|
71
|
+
return sorted(list(self.builders[self.__class__.__name__].keys()))
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def create(cls, key, **kwargs):
|
|
75
|
+
builder = cls._builders[cls.__name__].get(key)
|
|
76
|
+
if not builder:
|
|
77
|
+
raise ValueError(key)
|
|
78
|
+
return builder(**kwargs)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_class(cls, key):
|
|
82
|
+
return cls._builders[cls.__name__].get(key)
|
pymodaq_utils/icon.ico
ADDED
|
Binary file
|
pymodaq_utils/logger.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created the 27/10/2022
|
|
4
|
+
|
|
5
|
+
@author: Sebastien Weber
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import warnings
|
|
9
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from pymodaq_utils.config import get_set_config_dir, Config
|
|
13
|
+
|
|
14
|
+
config = Config()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_logger(logger_name, add_handler=False, base_logger=False, add_to_console=False, log_level=None,
|
|
18
|
+
logger_base_name='pymodaq') -> logging.Logger:
|
|
19
|
+
"""defines a logger of a given name and eventually add an handler to it
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
logger_name: (str) the name of the logger (usually it is the module name as returned by get_module_name
|
|
24
|
+
add_handler (bool) if True adds a TimedRotatingFileHandler to the logger instance (should be True if logger set from
|
|
25
|
+
main app
|
|
26
|
+
base_logger: (bool) specify if this is the parent logger (usually where one defines the handler)
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
logger: (logging.Logger) logger instance
|
|
31
|
+
See Also
|
|
32
|
+
--------
|
|
33
|
+
get_module_name, logging.handlers.TimedRotatingFileHandler
|
|
34
|
+
"""
|
|
35
|
+
if not base_logger:
|
|
36
|
+
logger_name = f'{logger_base_name}.{logger_name}'
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(logger_name)
|
|
39
|
+
log_path = get_set_config_dir('log', user=True)
|
|
40
|
+
if log_level is None:
|
|
41
|
+
log_level = config('general', 'debug_level')
|
|
42
|
+
logger.setLevel(log_level)
|
|
43
|
+
if add_handler:
|
|
44
|
+
log_file_path = log_path.joinpath(f'{logger_base_name}.log')
|
|
45
|
+
if not log_file_path.is_file():
|
|
46
|
+
log_file_path.touch(mode=0o777)
|
|
47
|
+
handler = TimedRotatingFileHandler(log_file_path, when='midnight')
|
|
48
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
49
|
+
handler.setFormatter(formatter)
|
|
50
|
+
logger.addHandler(handler)
|
|
51
|
+
|
|
52
|
+
logging.captureWarnings(True)
|
|
53
|
+
# only catch DeprecationWarning in DEBUG level
|
|
54
|
+
if log_level == 'DEBUG':
|
|
55
|
+
warnings.filterwarnings('default', category=DeprecationWarning)
|
|
56
|
+
else:
|
|
57
|
+
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
58
|
+
warnings_logger = logging.getLogger("py.warnings")
|
|
59
|
+
warnings_logger.addHandler(handler)
|
|
60
|
+
|
|
61
|
+
if add_to_console:
|
|
62
|
+
console_handler = logging.StreamHandler()
|
|
63
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
64
|
+
console_handler.setFormatter(formatter)
|
|
65
|
+
logger.addHandler(console_handler)
|
|
66
|
+
return logger
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_base_logger(logger: logging.Logger) -> logging.Logger:
|
|
70
|
+
while logger.name != 'pymodaq':
|
|
71
|
+
logger = logger.parent
|
|
72
|
+
return logger
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_module_name(module__file__path):
|
|
76
|
+
"""from the full path of a module extract its name"""
|
|
77
|
+
path = Path(module__file__path)
|
|
78
|
+
return path.stem
|