experimaestro 1.8.0rc6__py3-none-any.whl → 1.8.0rc7__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.
Potentially problematic release.
This version of experimaestro might be problematic. Click here for more details.
- experimaestro/core/identifier.py +296 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +157 -549
- experimaestro/core/objects/config_utils.py +58 -0
- experimaestro/core/objects/config_walk.py +150 -0
- experimaestro/core/objects.pyi +6 -38
- experimaestro/core/types.py +28 -4
- experimaestro/notifications.py +1 -0
- experimaestro/scheduler/base.py +1 -0
- experimaestro/tests/core/__init__.py +0 -0
- experimaestro/tests/core/test_generics.py +206 -0
- experimaestro/tests/restart.py +3 -1
- experimaestro/tests/test_instance.py +3 -3
- experimaestro/tests/test_objects.py +20 -4
- experimaestro/tests/test_serializers.py +3 -3
- experimaestro/tests/test_types.py +2 -2
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.0rc7.dist-info}/METADATA +5 -5
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.0rc7.dist-info}/RECORD +21 -15
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.0rc7.dist-info}/LICENSE +0 -0
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.0rc7.dist-info}/WHEEL +0 -0
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.0rc7.dist-info}/entry_points.txt +0 -0
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
"""Configuration and tasks"""
|
|
2
2
|
|
|
3
|
-
from functools import cached_property
|
|
4
3
|
import json
|
|
5
4
|
|
|
6
5
|
from attr import define
|
|
6
|
+
import fasteners
|
|
7
7
|
|
|
8
8
|
from experimaestro import taskglobals
|
|
9
9
|
|
|
10
|
-
try:
|
|
11
|
-
from types import NoneType
|
|
12
|
-
except Exception:
|
|
13
|
-
# compatibility: python-3.8
|
|
14
|
-
NoneType = type(None)
|
|
15
10
|
from termcolor import cprint
|
|
16
|
-
import os
|
|
17
11
|
from pathlib import Path
|
|
18
12
|
import hashlib
|
|
19
13
|
import logging
|
|
20
|
-
import struct
|
|
21
14
|
import io
|
|
22
|
-
import fasteners
|
|
23
15
|
from enum import Enum
|
|
24
16
|
import inspect
|
|
25
17
|
import importlib
|
|
@@ -42,306 +34,33 @@ from typing import (
|
|
|
42
34
|
import sys
|
|
43
35
|
import experimaestro
|
|
44
36
|
from experimaestro.utils import logger
|
|
45
|
-
from
|
|
46
|
-
from
|
|
47
|
-
from .context import SerializationContext, SerializedPath, SerializedPathLoader
|
|
37
|
+
from experimaestro.core.types import DeprecatedAttribute, ObjectType, TypeVarType
|
|
38
|
+
from ..context import SerializationContext, SerializedPath, SerializedPathLoader
|
|
48
39
|
|
|
49
40
|
if TYPE_CHECKING:
|
|
50
|
-
from
|
|
41
|
+
from ..callbacks import TaskEventListener
|
|
42
|
+
from ..identifier import Identifier
|
|
51
43
|
from experimaestro.scheduler.base import Job
|
|
52
44
|
from experimaestro.scheduler.workspace import RunMode
|
|
53
45
|
from experimaestro.launchers import Launcher
|
|
54
46
|
from experimaestro.scheduler import Workspace
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def all(self):
|
|
66
|
-
"""Returns the overall identifier"""
|
|
67
|
-
return self.main
|
|
68
|
-
|
|
69
|
-
def __hash__(self) -> int:
|
|
70
|
-
return hash(self.main)
|
|
71
|
-
|
|
72
|
-
def state_dict(self):
|
|
73
|
-
return self.main.hex()
|
|
74
|
-
|
|
75
|
-
def __eq__(self, other: "Identifier"):
|
|
76
|
-
return self.main == other.main
|
|
77
|
-
|
|
78
|
-
@staticmethod
|
|
79
|
-
def from_state_dict(data: Union[Dict[str, str], str]):
|
|
80
|
-
if isinstance(data, str):
|
|
81
|
-
return Identifier(bytes.fromhex(data))
|
|
82
|
-
|
|
83
|
-
return Identifier(bytes.fromhex(data["main"]))
|
|
84
|
-
|
|
85
|
-
def __repr__(self):
|
|
86
|
-
return self.main.hex()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def is_ignored(value):
|
|
90
|
-
"""Returns True if the value should be ignored by itself"""
|
|
91
|
-
return value is not None and isinstance(value, Config) and (value.__xpm__.meta)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def remove_meta(value):
|
|
95
|
-
"""Cleanup a dict/list by removing ignored values"""
|
|
96
|
-
if isinstance(value, list):
|
|
97
|
-
return [el for el in value if not is_ignored(el)]
|
|
98
|
-
if isinstance(value, dict):
|
|
99
|
-
return {key: value for key, value in value.items() if not is_ignored(value)}
|
|
100
|
-
return value
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class ConfigPath:
|
|
104
|
-
"""Used to keep track of cycles when computing a hash"""
|
|
105
|
-
|
|
106
|
-
def __init__(self):
|
|
107
|
-
self.loops: List[bool] = []
|
|
108
|
-
"""Indicates whether a loop was detected up to this node"""
|
|
109
|
-
|
|
110
|
-
self.config2index = {}
|
|
111
|
-
"""Associate an index in the list with a configuration"""
|
|
112
|
-
|
|
113
|
-
def detect_loop(self, config) -> Optional[int]:
|
|
114
|
-
"""If there is a loop, return the relative index and update the path"""
|
|
115
|
-
index = self.config2index.get(id(config), None)
|
|
116
|
-
if index is not None:
|
|
117
|
-
for i in range(index, self.depth):
|
|
118
|
-
self.loops[i] = True
|
|
119
|
-
return self.depth - index
|
|
120
|
-
|
|
121
|
-
def has_loop(self):
|
|
122
|
-
return self.loops[-1]
|
|
123
|
-
|
|
124
|
-
@property
|
|
125
|
-
def depth(self):
|
|
126
|
-
return len(self.loops)
|
|
127
|
-
|
|
128
|
-
@contextmanager
|
|
129
|
-
def push(self, config):
|
|
130
|
-
config_id = id(config)
|
|
131
|
-
assert config_id not in self.config2index
|
|
132
|
-
|
|
133
|
-
self.config2index[config_id] = self.depth
|
|
134
|
-
self.loops.append(False)
|
|
135
|
-
|
|
136
|
-
try:
|
|
137
|
-
yield
|
|
138
|
-
finally:
|
|
139
|
-
self.loops.pop()
|
|
140
|
-
del self.config2index[config_id]
|
|
48
|
+
from .config_walk import ConfigWalk, ConfigWalkContext
|
|
49
|
+
from .config_utils import (
|
|
50
|
+
getqualattr,
|
|
51
|
+
add_to_path,
|
|
52
|
+
SealedError,
|
|
53
|
+
TaggedValue,
|
|
54
|
+
ObjectStore,
|
|
55
|
+
classproperty,
|
|
56
|
+
)
|
|
141
57
|
|
|
58
|
+
T = TypeVar("T", bound="Config")
|
|
142
59
|
|
|
143
|
-
hash_logger = logging.getLogger("xpm.hash")
|
|
144
60
|
|
|
145
61
|
DependentMarker = Callable[["Config"], None]
|
|
146
62
|
|
|
147
63
|
|
|
148
|
-
class HashComputer:
|
|
149
|
-
"""This class is in charge of computing a config/task identifier"""
|
|
150
|
-
|
|
151
|
-
OBJECT_ID = b"\x00"
|
|
152
|
-
INT_ID = b"\x01"
|
|
153
|
-
FLOAT_ID = b"\x02"
|
|
154
|
-
STR_ID = b"\x03"
|
|
155
|
-
PATH_ID = b"\x04"
|
|
156
|
-
NAME_ID = b"\x05"
|
|
157
|
-
NONE_ID = b"\x06"
|
|
158
|
-
LIST_ID = b"\x07"
|
|
159
|
-
TASK_ID = b"\x08"
|
|
160
|
-
DICT_ID = b"\x09"
|
|
161
|
-
ENUM_ID = b"\x0a"
|
|
162
|
-
CYCLE_REFERENCE = b"\x0b"
|
|
163
|
-
INIT_TASKS = b"\x0c"
|
|
164
|
-
|
|
165
|
-
def __init__(self, config: "Config", config_path: ConfigPath, *, version=None):
|
|
166
|
-
# Hasher for parameters
|
|
167
|
-
self._hasher = hashlib.sha256()
|
|
168
|
-
self.config = config
|
|
169
|
-
self.config_path = config_path
|
|
170
|
-
self.version = version or int(os.environ.get("XPM_HASH_COMPUTER", 2))
|
|
171
|
-
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
172
|
-
hash_logger.debug(
|
|
173
|
-
"starting hash (%s): %s", hash(str(self.config)), self.config
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def identifier(self) -> Identifier:
|
|
177
|
-
main = self._hasher.digest()
|
|
178
|
-
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
179
|
-
hash_logger.debug("hash (%s): %s", hash(str(self.config)), str(main))
|
|
180
|
-
return Identifier(main)
|
|
181
|
-
|
|
182
|
-
def _hashupdate(self, bytes: bytes):
|
|
183
|
-
"""Update the hash computers with some bytes"""
|
|
184
|
-
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
185
|
-
hash_logger.debug(
|
|
186
|
-
"updating hash (%s): %s", hash(str(self.config)), str(bytes)
|
|
187
|
-
)
|
|
188
|
-
self._hasher.update(bytes)
|
|
189
|
-
|
|
190
|
-
def update(self, value, *, myself=False): # noqa: C901
|
|
191
|
-
"""Update the hash
|
|
192
|
-
|
|
193
|
-
:param value: The value to add to the hash
|
|
194
|
-
:param myself: True if the value is the configuration for which we wish
|
|
195
|
-
to compute the identifier, defaults to False
|
|
196
|
-
:raises NotImplementedError: If the value cannot be processed
|
|
197
|
-
"""
|
|
198
|
-
if value is None:
|
|
199
|
-
self._hashupdate(HashComputer.NONE_ID)
|
|
200
|
-
elif isinstance(value, float):
|
|
201
|
-
self._hashupdate(HashComputer.FLOAT_ID)
|
|
202
|
-
self._hashupdate(struct.pack("!d", value))
|
|
203
|
-
elif isinstance(value, int):
|
|
204
|
-
self._hashupdate(HashComputer.INT_ID)
|
|
205
|
-
self._hashupdate(struct.pack("!q", value))
|
|
206
|
-
elif isinstance(value, str):
|
|
207
|
-
self._hashupdate(HashComputer.STR_ID)
|
|
208
|
-
self._hashupdate(value.encode("utf-8"))
|
|
209
|
-
elif isinstance(value, list):
|
|
210
|
-
values = [el for el in value if not is_ignored(el)]
|
|
211
|
-
self._hashupdate(HashComputer.LIST_ID)
|
|
212
|
-
self._hashupdate(struct.pack("!d", len(values)))
|
|
213
|
-
for x in values:
|
|
214
|
-
self.update(x)
|
|
215
|
-
elif isinstance(value, Enum):
|
|
216
|
-
self._hashupdate(HashComputer.ENUM_ID)
|
|
217
|
-
k = value.__class__
|
|
218
|
-
self._hashupdate(
|
|
219
|
-
f"{k.__module__}.{k.__qualname__ }:{value.name}".encode("utf-8"),
|
|
220
|
-
)
|
|
221
|
-
elif isinstance(value, dict):
|
|
222
|
-
self._hashupdate(HashComputer.DICT_ID)
|
|
223
|
-
items = [
|
|
224
|
-
(key, value) for key, value in value.items() if not is_ignored(value)
|
|
225
|
-
]
|
|
226
|
-
items.sort(key=lambda x: x[0])
|
|
227
|
-
for key, value in items:
|
|
228
|
-
self.update(key)
|
|
229
|
-
self.update(value)
|
|
230
|
-
|
|
231
|
-
# Handles configurations
|
|
232
|
-
elif isinstance(value, Config):
|
|
233
|
-
# Encodes the identifier
|
|
234
|
-
self._hashupdate(HashComputer.OBJECT_ID)
|
|
235
|
-
|
|
236
|
-
# If we encode another config, then
|
|
237
|
-
if not myself:
|
|
238
|
-
if loop_ix := self.config_path.detect_loop(value):
|
|
239
|
-
# Loop detected: use cycle reference
|
|
240
|
-
self._hashupdate(HashComputer.CYCLE_REFERENCE)
|
|
241
|
-
self._hashupdate(struct.pack("!q", loop_ix))
|
|
242
|
-
|
|
243
|
-
else:
|
|
244
|
-
# Just use the object identifier
|
|
245
|
-
value_id = HashComputer.compute(
|
|
246
|
-
value, version=self.version, config_path=self.config_path
|
|
247
|
-
)
|
|
248
|
-
self._hashupdate(value_id.all)
|
|
249
|
-
|
|
250
|
-
# And that's it!
|
|
251
|
-
return
|
|
252
|
-
|
|
253
|
-
# Process tasks
|
|
254
|
-
if value.__xpm__.task is not None and (value.__xpm__.task is not value):
|
|
255
|
-
hash_logger.debug("Computing hash for task %s", value.__xpm__.task)
|
|
256
|
-
self._hashupdate(HashComputer.TASK_ID)
|
|
257
|
-
self.update(value.__xpm__.task)
|
|
258
|
-
|
|
259
|
-
xpmtype = value.__xpmtype__
|
|
260
|
-
self._hashupdate(xpmtype.identifier.name.encode("utf-8"))
|
|
261
|
-
|
|
262
|
-
# Process arguments (sort by name to ensure uniqueness)
|
|
263
|
-
arguments = sorted(xpmtype.arguments.values(), key=lambda a: a.name)
|
|
264
|
-
for argument in arguments:
|
|
265
|
-
# Ignored argument
|
|
266
|
-
if argument.ignored:
|
|
267
|
-
argvalue = value.__xpm__.values.get(argument.name, None)
|
|
268
|
-
|
|
269
|
-
# ... unless meta is set to false
|
|
270
|
-
if (
|
|
271
|
-
argvalue is None
|
|
272
|
-
or not isinstance(argvalue, Config)
|
|
273
|
-
or (argvalue.__xpm__.meta is not False)
|
|
274
|
-
):
|
|
275
|
-
continue
|
|
276
|
-
|
|
277
|
-
if argument.generator:
|
|
278
|
-
continue
|
|
279
|
-
|
|
280
|
-
# Argument value
|
|
281
|
-
# Skip if the argument is not a constant, and
|
|
282
|
-
# - optional argument: both value and default are None
|
|
283
|
-
# - the argument value is equal to the default value
|
|
284
|
-
argvalue = getattr(value, argument.name, None)
|
|
285
|
-
if not argument.constant and (
|
|
286
|
-
(
|
|
287
|
-
not argument.required
|
|
288
|
-
and argument.default is None
|
|
289
|
-
and argvalue is None
|
|
290
|
-
)
|
|
291
|
-
or (
|
|
292
|
-
argument.default is not None
|
|
293
|
-
and argument.default == remove_meta(argvalue)
|
|
294
|
-
)
|
|
295
|
-
):
|
|
296
|
-
# No update if same value (and not constant)
|
|
297
|
-
continue
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
argvalue is not None
|
|
301
|
-
and isinstance(argvalue, Config)
|
|
302
|
-
and argvalue.__xpm__.meta
|
|
303
|
-
):
|
|
304
|
-
continue
|
|
305
|
-
|
|
306
|
-
# Hash name
|
|
307
|
-
self.update(argument.name)
|
|
308
|
-
|
|
309
|
-
# Hash value
|
|
310
|
-
self._hashupdate(HashComputer.NAME_ID)
|
|
311
|
-
self.update(argvalue)
|
|
312
|
-
|
|
313
|
-
else:
|
|
314
|
-
raise NotImplementedError("Cannot compute hash of type %s" % type(value))
|
|
315
|
-
|
|
316
|
-
@staticmethod
|
|
317
|
-
def compute(
|
|
318
|
-
config: "Config", config_path: ConfigPath = None, version=None
|
|
319
|
-
) -> Identifier:
|
|
320
|
-
"""Compute the identifier for a configuration
|
|
321
|
-
|
|
322
|
-
:param config: the configuration for which we compute the identifier
|
|
323
|
-
:param config_path: used to track down cycles between configurations
|
|
324
|
-
:param version: version for the hash computation (None for the last one)
|
|
325
|
-
"""
|
|
326
|
-
|
|
327
|
-
# Try to use the cached value first
|
|
328
|
-
# (if there are no loops)
|
|
329
|
-
if config.__xpm__._sealed:
|
|
330
|
-
identifier = config.__xpm__._raw_identifier
|
|
331
|
-
if identifier is not None and not identifier.has_loops:
|
|
332
|
-
return identifier
|
|
333
|
-
|
|
334
|
-
config_path = config_path or ConfigPath()
|
|
335
|
-
|
|
336
|
-
with config_path.push(config):
|
|
337
|
-
self = HashComputer(config, config_path, version=version)
|
|
338
|
-
self.update(config, myself=True)
|
|
339
|
-
identifier = self.identifier()
|
|
340
|
-
identifier.has_loop = config_path.has_loop()
|
|
341
|
-
|
|
342
|
-
return identifier
|
|
343
|
-
|
|
344
|
-
|
|
345
64
|
def updatedependencies(
|
|
346
65
|
dependencies, value: "Config", path: List[str], taskids: Set[int]
|
|
347
66
|
):
|
|
@@ -370,199 +89,9 @@ def updatedependencies(
|
|
|
370
89
|
raise NotImplementedError("update dependencies for type %s" % type(value))
|
|
371
90
|
|
|
372
91
|
|
|
373
|
-
class SealedError(Exception):
|
|
374
|
-
"""Exception when trying to modify a sealed configuration"""
|
|
375
|
-
|
|
376
|
-
pass
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
class TaggedValue:
|
|
380
|
-
def __init__(self, value):
|
|
381
|
-
self.value = value
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
@contextmanager
|
|
385
|
-
def add_to_path(p):
|
|
386
|
-
"""Temporarily add a path to sys.path"""
|
|
387
|
-
import sys
|
|
388
|
-
|
|
389
|
-
old_path = sys.path
|
|
390
|
-
sys.path = sys.path[:]
|
|
391
|
-
sys.path.insert(0, p)
|
|
392
|
-
try:
|
|
393
|
-
yield
|
|
394
|
-
finally:
|
|
395
|
-
sys.path = old_path
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
class ConfigWalkContext:
|
|
399
|
-
"""Context when generating values in configurations"""
|
|
400
|
-
|
|
401
|
-
@property
|
|
402
|
-
def path(self):
|
|
403
|
-
"""Returns the path of the job directory"""
|
|
404
|
-
raise NotImplementedError()
|
|
405
|
-
|
|
406
|
-
def __init__(self):
|
|
407
|
-
self._configpath = None
|
|
408
|
-
|
|
409
|
-
@property
|
|
410
|
-
def task(self):
|
|
411
|
-
return None
|
|
412
|
-
|
|
413
|
-
def currentpath(self) -> Path:
|
|
414
|
-
"""Returns the configuration folder"""
|
|
415
|
-
if self._configpath:
|
|
416
|
-
return self.path / self._configpath
|
|
417
|
-
return self.path
|
|
418
|
-
|
|
419
|
-
@contextmanager
|
|
420
|
-
def push(self, key: str):
|
|
421
|
-
"""Push a new key to contextualize paths"""
|
|
422
|
-
p = self._configpath
|
|
423
|
-
try:
|
|
424
|
-
self._configpath = (Path("out") if p is None else p) / key
|
|
425
|
-
yield key
|
|
426
|
-
finally:
|
|
427
|
-
self._configpath = p
|
|
428
|
-
|
|
429
|
-
|
|
430
92
|
NOT_SET = object()
|
|
431
93
|
|
|
432
94
|
|
|
433
|
-
class ConfigWalk:
|
|
434
|
-
"""Allows to perform an operation on all nested configurations"""
|
|
435
|
-
|
|
436
|
-
def __init__(self, context: ConfigWalkContext = None, recurse_task=False):
|
|
437
|
-
"""
|
|
438
|
-
|
|
439
|
-
:param recurse_task: Recurse into linked tasks
|
|
440
|
-
:param context: The context, by default only tracks the position in the
|
|
441
|
-
config tree
|
|
442
|
-
"""
|
|
443
|
-
self.recurse_task = recurse_task
|
|
444
|
-
self.context = ConfigWalkContext() if context is None else context
|
|
445
|
-
|
|
446
|
-
# Stores already visited nodes
|
|
447
|
-
self.visited = {}
|
|
448
|
-
|
|
449
|
-
def preprocess(self, config: "Config") -> Tuple[bool, Any]:
|
|
450
|
-
"""Returns a tuple boolean/value
|
|
451
|
-
|
|
452
|
-
The boolean value is used to stop the processing if False.
|
|
453
|
-
The value is returned
|
|
454
|
-
"""
|
|
455
|
-
return True, None
|
|
456
|
-
|
|
457
|
-
def postprocess(self, stub, config: "Config", values: Dict[str, Any]):
|
|
458
|
-
return stub
|
|
459
|
-
|
|
460
|
-
def list(self, i: int):
|
|
461
|
-
return self.context.push(str(i))
|
|
462
|
-
|
|
463
|
-
def map(self, k: str):
|
|
464
|
-
return self.context.push(k)
|
|
465
|
-
|
|
466
|
-
def stub(self, config: "Config"):
|
|
467
|
-
return config
|
|
468
|
-
|
|
469
|
-
def __call__(self, x):
|
|
470
|
-
if isinstance(x, Config):
|
|
471
|
-
info = x.__xpm__ # type: ConfigInformation
|
|
472
|
-
|
|
473
|
-
# Avoid loops
|
|
474
|
-
xid = id(x)
|
|
475
|
-
if xid in self.visited:
|
|
476
|
-
return self.visited[xid]
|
|
477
|
-
|
|
478
|
-
# Get a stub
|
|
479
|
-
stub = self.stub(x)
|
|
480
|
-
self.visited[xid] = stub
|
|
481
|
-
|
|
482
|
-
# Pre-process
|
|
483
|
-
flag, value = self.preprocess(x)
|
|
484
|
-
|
|
485
|
-
if not flag:
|
|
486
|
-
# Stop processing and returns value
|
|
487
|
-
return value
|
|
488
|
-
|
|
489
|
-
# Process all the arguments
|
|
490
|
-
result = {}
|
|
491
|
-
for arg, v in info.xpmvalues():
|
|
492
|
-
if v is not None:
|
|
493
|
-
with self.map(arg.name):
|
|
494
|
-
result[arg.name] = self(v)
|
|
495
|
-
else:
|
|
496
|
-
result[arg.name] = None
|
|
497
|
-
|
|
498
|
-
# Deals with pre-tasks
|
|
499
|
-
if info.pre_tasks:
|
|
500
|
-
with self.map("__pre_tasks__"):
|
|
501
|
-
self(info.pre_tasks)
|
|
502
|
-
|
|
503
|
-
if info.init_tasks:
|
|
504
|
-
with self.map("__init_tasks__"):
|
|
505
|
-
self(info.init_tasks)
|
|
506
|
-
|
|
507
|
-
# Process task if different
|
|
508
|
-
if (
|
|
509
|
-
x.__xpm__.task is not None
|
|
510
|
-
and self.recurse_task
|
|
511
|
-
and x.__xpm__.task is not x
|
|
512
|
-
):
|
|
513
|
-
self(x.__xpm__.task)
|
|
514
|
-
|
|
515
|
-
processed = self.postprocess(stub, x, result)
|
|
516
|
-
self.visited[xid] = processed
|
|
517
|
-
return processed
|
|
518
|
-
|
|
519
|
-
if isinstance(x, list):
|
|
520
|
-
result = []
|
|
521
|
-
for i, sv in enumerate(x):
|
|
522
|
-
with self.list(i):
|
|
523
|
-
result.append(self(sv))
|
|
524
|
-
return result
|
|
525
|
-
|
|
526
|
-
if isinstance(x, dict):
|
|
527
|
-
result = {}
|
|
528
|
-
for key, value in x.items():
|
|
529
|
-
assert isinstance(key, (str, float, int))
|
|
530
|
-
with self.map(key):
|
|
531
|
-
result[key] = self(value)
|
|
532
|
-
return result
|
|
533
|
-
|
|
534
|
-
if isinstance(x, (float, int, str, Path, Enum)):
|
|
535
|
-
return x
|
|
536
|
-
|
|
537
|
-
raise NotImplementedError(f"Cannot handle a value of type {type(x)}")
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def getqualattr(module, qualname):
|
|
541
|
-
"""Get a qualified attributed value"""
|
|
542
|
-
cls = module
|
|
543
|
-
for part in qualname.split("."):
|
|
544
|
-
cls = getattr(cls, part)
|
|
545
|
-
return cls
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
class ObjectStore:
|
|
549
|
-
def __init__(self):
|
|
550
|
-
self.store: Dict[int, Any] = {}
|
|
551
|
-
self.constructed: Set[int] = set()
|
|
552
|
-
|
|
553
|
-
def set_constructed(self, identifier: int):
|
|
554
|
-
self.constructed.add(identifier)
|
|
555
|
-
|
|
556
|
-
def is_constructed(self, identifier: int):
|
|
557
|
-
return identifier in self.constructed
|
|
558
|
-
|
|
559
|
-
def retrieve(self, identifier: int):
|
|
560
|
-
return self.store.get(identifier, None)
|
|
561
|
-
|
|
562
|
-
def add_stub(self, identifier: int, stub: Any):
|
|
563
|
-
self.store[identifier] = stub
|
|
564
|
-
|
|
565
|
-
|
|
566
95
|
@define()
|
|
567
96
|
class WatchedOutput:
|
|
568
97
|
#: The enclosing job
|
|
@@ -590,7 +119,7 @@ class ConfigInformation:
|
|
|
590
119
|
# Set to true when loading from JSON
|
|
591
120
|
LOADING: ClassVar[bool] = False
|
|
592
121
|
|
|
593
|
-
def __init__(self, pyobject: "
|
|
122
|
+
def __init__(self, pyobject: "ConfigMixin"):
|
|
594
123
|
# The underlying pyobject and XPM type
|
|
595
124
|
self.pyobject = pyobject
|
|
596
125
|
self.xpmtype = pyobject.__xpmtype__ # type: ObjectType
|
|
@@ -616,6 +145,10 @@ class ConfigInformation:
|
|
|
616
145
|
# Explicitely added dependencies
|
|
617
146
|
self.dependencies = []
|
|
618
147
|
|
|
148
|
+
# Concrete type variables resolutions
|
|
149
|
+
# This is used to check typevars coherence
|
|
150
|
+
self.concrete_typevars: Dict[TypeVar, type] = {}
|
|
151
|
+
|
|
619
152
|
# Lightweight tasks
|
|
620
153
|
self.pre_tasks: List["LightweightTask"] = []
|
|
621
154
|
|
|
@@ -655,6 +188,8 @@ class ConfigInformation:
|
|
|
655
188
|
return object.__getattribute__(self.pyobject, name)
|
|
656
189
|
|
|
657
190
|
def set(self, k, v, bypass=False):
|
|
191
|
+
from experimaestro.generators import Generator
|
|
192
|
+
|
|
658
193
|
# Not an argument, bypass
|
|
659
194
|
if k not in self.xpmtype.arguments:
|
|
660
195
|
setattr(self.pyobject, k, v)
|
|
@@ -666,10 +201,19 @@ class ConfigInformation:
|
|
|
666
201
|
try:
|
|
667
202
|
argument = self.xpmtype.arguments.get(k, None)
|
|
668
203
|
if argument:
|
|
669
|
-
if not bypass and (
|
|
204
|
+
if not bypass and (
|
|
205
|
+
(isinstance(argument.generator, Generator)) or argument.constant
|
|
206
|
+
):
|
|
670
207
|
raise AttributeError("Property %s is read-only" % (k))
|
|
671
208
|
if v is not None:
|
|
672
209
|
self.values[k] = argument.validate(v)
|
|
210
|
+
# Check for type variables
|
|
211
|
+
if type(argument.type) is TypeVarType:
|
|
212
|
+
self.check_typevar(argument.type.typevar, type(v))
|
|
213
|
+
if isinstance(v, Config):
|
|
214
|
+
# If the value is a Config, fuse type variables
|
|
215
|
+
v.__xpm__.fuse_concrete_typevars(self.concrete_typevars)
|
|
216
|
+
self.fuse_concrete_typevars(v.__xpm__.concrete_typevars)
|
|
673
217
|
elif argument.required:
|
|
674
218
|
raise AttributeError("Cannot set required attribute to None")
|
|
675
219
|
else:
|
|
@@ -682,6 +226,43 @@ class ConfigInformation:
|
|
|
682
226
|
logger.error("Error while setting value %s in %s", k, self.xpmtype)
|
|
683
227
|
raise
|
|
684
228
|
|
|
229
|
+
def fuse_concrete_typevars(self, typevars: Dict[TypeVar, type]):
|
|
230
|
+
"""Fuses concrete type variables with the current ones"""
|
|
231
|
+
for typevar, v in typevars.items():
|
|
232
|
+
self.check_typevar(typevar, v)
|
|
233
|
+
|
|
234
|
+
def check_typevar(self, typevar: TypeVar, v: type):
|
|
235
|
+
"""Check if a type variable is coherent with the current typevars bindings,
|
|
236
|
+
updates the bindings if necessary"""
|
|
237
|
+
if typevar not in self.concrete_typevars:
|
|
238
|
+
self.concrete_typevars[typevar] = v
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
concrete_typevar = self.concrete_typevars[typevar]
|
|
242
|
+
bound = typevar.__bound__
|
|
243
|
+
# Check that v is a subclass of the typevar OR that typevar is a subclass of v
|
|
244
|
+
# Then set the concrete type variable to the most generic type
|
|
245
|
+
|
|
246
|
+
# First, limiting to the specified bound
|
|
247
|
+
if bound is not None:
|
|
248
|
+
if not issubclass(v, bound):
|
|
249
|
+
raise TypeError(
|
|
250
|
+
f"Type variable {typevar} is bound to {bound}, but tried to set it to {v}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if issubclass(v, concrete_typevar):
|
|
254
|
+
# v is a subclass of the typevar, keep the typevar
|
|
255
|
+
return
|
|
256
|
+
if issubclass(concrete_typevar, v):
|
|
257
|
+
# typevar is a subclass of v, keep v
|
|
258
|
+
self.concrete_typevars[typevar] = v
|
|
259
|
+
return
|
|
260
|
+
raise TypeError(
|
|
261
|
+
f"Type variable {typevar} is already set to {self.concrete_typevars[typevar]}, "
|
|
262
|
+
f"but tried to set it to {v}"
|
|
263
|
+
f" (current typevars bindings: {self.concrete_typevars})"
|
|
264
|
+
)
|
|
265
|
+
|
|
685
266
|
def addtag(self, name, value):
|
|
686
267
|
self._tags[name] = value
|
|
687
268
|
|
|
@@ -752,18 +333,29 @@ class ConfigInformation:
|
|
|
752
333
|
|
|
753
334
|
def postprocess(self, stub, config: Config, values):
|
|
754
335
|
# Generate values
|
|
336
|
+
from experimaestro.generators import Generator
|
|
337
|
+
|
|
755
338
|
for k, argument in config.__xpmtype__.arguments.items():
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
339
|
+
try:
|
|
340
|
+
if argument.generator:
|
|
341
|
+
if not isinstance(argument.generator, Generator):
|
|
342
|
+
value = argument.generator()
|
|
343
|
+
else:
|
|
344
|
+
sig = inspect.signature(argument.generator)
|
|
345
|
+
if len(sig.parameters) == 0:
|
|
346
|
+
value = argument.generator()
|
|
347
|
+
elif len(sig.parameters) == 2:
|
|
348
|
+
value = argument.generator(self.context, config)
|
|
349
|
+
else:
|
|
350
|
+
assert (
|
|
351
|
+
False
|
|
352
|
+
), "generator has either two parameters (context and config), or none"
|
|
353
|
+
config.__xpm__.set(k, value, bypass=True)
|
|
354
|
+
except Exception:
|
|
355
|
+
logger.error(
|
|
356
|
+
"While setting %s of %s", argument.name, config.__xpmtype__
|
|
357
|
+
)
|
|
358
|
+
raise
|
|
767
359
|
|
|
768
360
|
config.__xpm__._sealed = True
|
|
769
361
|
|
|
@@ -805,6 +397,7 @@ class ConfigInformation:
|
|
|
805
397
|
|
|
806
398
|
def identifiers(self, only_raw: bool):
|
|
807
399
|
"""Computes the unique identifier"""
|
|
400
|
+
from ..identifier import IdentifierComputer, Identifier
|
|
808
401
|
|
|
809
402
|
raw_identifier = self._raw_identifier
|
|
810
403
|
full_identifier = self._full_identifier
|
|
@@ -812,7 +405,7 @@ class ConfigInformation:
|
|
|
812
405
|
# Computes raw identifier if needed
|
|
813
406
|
if raw_identifier is None or not self._sealed:
|
|
814
407
|
# Get the main identifier
|
|
815
|
-
raw_identifier =
|
|
408
|
+
raw_identifier = IdentifierComputer.compute(self.pyobject)
|
|
816
409
|
if self._sealed:
|
|
817
410
|
self._raw_identifier = raw_identifier
|
|
818
411
|
|
|
@@ -833,7 +426,7 @@ class ConfigInformation:
|
|
|
833
426
|
|
|
834
427
|
# Adds init tasks
|
|
835
428
|
if self.init_tasks:
|
|
836
|
-
hasher.update(
|
|
429
|
+
hasher.update(IdentifierComputer.INIT_TASKS)
|
|
837
430
|
for init_task in self.init_tasks:
|
|
838
431
|
hasher.update(init_task.__xpm__.raw_identifier.all)
|
|
839
432
|
|
|
@@ -847,13 +440,13 @@ class ConfigInformation:
|
|
|
847
440
|
return raw_identifier, full_identifier
|
|
848
441
|
|
|
849
442
|
@property
|
|
850
|
-
def raw_identifier(self) -> Identifier:
|
|
443
|
+
def raw_identifier(self) -> "Identifier":
|
|
851
444
|
"""Computes the unique identifier (without task modifiers)"""
|
|
852
445
|
raw_identifier, _ = self.identifiers(True)
|
|
853
446
|
return raw_identifier
|
|
854
447
|
|
|
855
448
|
@property
|
|
856
|
-
def full_identifier(self) -> Identifier:
|
|
449
|
+
def full_identifier(self) -> "Identifier":
|
|
857
450
|
"""Computes the unique identifier (with task modifiers)"""
|
|
858
451
|
_, full_identifier = self.identifiers(False)
|
|
859
452
|
return full_identifier
|
|
@@ -954,7 +547,7 @@ class ConfigInformation:
|
|
|
954
547
|
|
|
955
548
|
:param callback: _description_
|
|
956
549
|
"""
|
|
957
|
-
from
|
|
550
|
+
from ..callbacks import TaskEventListener
|
|
958
551
|
|
|
959
552
|
TaskEventListener.on_completed(self, callback)
|
|
960
553
|
|
|
@@ -968,7 +561,7 @@ class ConfigInformation:
|
|
|
968
561
|
):
|
|
969
562
|
from experimaestro.scheduler import experiment, JobContext
|
|
970
563
|
from experimaestro.scheduler.workspace import RunMode
|
|
971
|
-
from
|
|
564
|
+
from ..callbacks import TaskEventListener
|
|
972
565
|
|
|
973
566
|
# --- Prepare the object
|
|
974
567
|
|
|
@@ -1355,7 +948,7 @@ class ConfigInformation:
|
|
|
1355
948
|
as_instance=True,
|
|
1356
949
|
save_directory: Optional[Path] = None,
|
|
1357
950
|
discard_id: bool = False,
|
|
1358
|
-
) -> "
|
|
951
|
+
) -> "ConfigMixin":
|
|
1359
952
|
...
|
|
1360
953
|
|
|
1361
954
|
@overload
|
|
@@ -1390,6 +983,7 @@ class ConfigInformation:
|
|
|
1390
983
|
o = None
|
|
1391
984
|
objects = {}
|
|
1392
985
|
import experimaestro.taskglobals as taskglobals
|
|
986
|
+
from ..identifier import Identifier
|
|
1393
987
|
|
|
1394
988
|
# Loop over all the definitions and create objects
|
|
1395
989
|
for definition in definitions:
|
|
@@ -1656,28 +1250,7 @@ def clone(v):
|
|
|
1656
1250
|
raise NotImplementedError("Clone not implemented for type %s" % type(v))
|
|
1657
1251
|
|
|
1658
1252
|
|
|
1659
|
-
|
|
1660
|
-
def __call__(config, *args, **kwargs):
|
|
1661
|
-
import experimaestro.taskglobals as taskglobals
|
|
1662
|
-
|
|
1663
|
-
# Get path and create directory if needed
|
|
1664
|
-
hexid = config.__xpmidentifier__ # type: Identifier
|
|
1665
|
-
typename = config.__xpmtypename__ # type: str
|
|
1666
|
-
dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
|
|
1667
|
-
|
|
1668
|
-
if not dir.exists():
|
|
1669
|
-
dir.mkdir(parents=True, exist_ok=True)
|
|
1670
|
-
|
|
1671
|
-
path = dir / name
|
|
1672
|
-
ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
|
|
1673
|
-
with ipc_lock:
|
|
1674
|
-
r = fn(config, path, *args, **kwargs)
|
|
1675
|
-
return r
|
|
1676
|
-
|
|
1677
|
-
return __call__
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
class TypeConfig:
|
|
1253
|
+
class ConfigMixin:
|
|
1681
1254
|
"""Class for configuration objects"""
|
|
1682
1255
|
|
|
1683
1256
|
__xpmtype__: ObjectType
|
|
@@ -1828,7 +1401,7 @@ class TypeConfig:
|
|
|
1828
1401
|
|
|
1829
1402
|
def add_pretasks_from(self, *configs: "Config"):
|
|
1830
1403
|
assert all(
|
|
1831
|
-
[isinstance(config,
|
|
1404
|
+
[isinstance(config, ConfigMixin) for config in configs]
|
|
1832
1405
|
), "One of the parameters is not a configuration object"
|
|
1833
1406
|
for config in configs:
|
|
1834
1407
|
self.add_pretasks(*config.__xpm__.pre_tasks)
|
|
@@ -1851,11 +1424,6 @@ class TypeConfig:
|
|
|
1851
1424
|
self.__xpm__.add_dependencies(*other.__xpm__.dependencies)
|
|
1852
1425
|
|
|
1853
1426
|
|
|
1854
|
-
class classproperty(property):
|
|
1855
|
-
def __get__(self, owner_self, owner_cls):
|
|
1856
|
-
return self.fget(owner_cls)
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
1427
|
class Config:
|
|
1860
1428
|
"""Base type for all objects in python interface"""
|
|
1861
1429
|
|
|
@@ -1873,24 +1441,20 @@ class Config:
|
|
|
1873
1441
|
|
|
1874
1442
|
@classproperty
|
|
1875
1443
|
def XPMConfig(cls):
|
|
1876
|
-
if issubclass(cls,
|
|
1444
|
+
if issubclass(cls, ConfigMixin):
|
|
1877
1445
|
return cls
|
|
1878
1446
|
return cls.__getxpmtype__().configtype
|
|
1879
1447
|
|
|
1880
|
-
@classproperty
|
|
1881
|
-
def C(cls):
|
|
1882
|
-
return cls.XPMConfig
|
|
1883
|
-
|
|
1884
1448
|
@classproperty
|
|
1885
1449
|
def XPMValue(cls):
|
|
1886
1450
|
"""Returns the value object for this configuration"""
|
|
1887
|
-
if issubclass(cls,
|
|
1451
|
+
if issubclass(cls, ConfigMixin):
|
|
1888
1452
|
return cls.__xpmtype__.objecttype
|
|
1889
1453
|
|
|
1890
1454
|
if value_cls := cls.__dict__.get("__XPMValue__", None):
|
|
1891
1455
|
pass
|
|
1892
1456
|
else:
|
|
1893
|
-
from
|
|
1457
|
+
from ..types import XPMValue
|
|
1894
1458
|
|
|
1895
1459
|
__objectbases__ = tuple(
|
|
1896
1460
|
s.XPMValue
|
|
@@ -1907,9 +1471,20 @@ class Config:
|
|
|
1907
1471
|
|
|
1908
1472
|
return value_cls
|
|
1909
1473
|
|
|
1474
|
+
@classproperty
|
|
1475
|
+
def C(cls):
|
|
1476
|
+
"""Alias for XPMConfig"""
|
|
1477
|
+
return cls.XPMConfig
|
|
1478
|
+
|
|
1479
|
+
@classproperty
|
|
1480
|
+
def V(cls):
|
|
1481
|
+
"""Alias for XPMValue"""
|
|
1482
|
+
return cls.XPMValue
|
|
1483
|
+
|
|
1910
1484
|
@classmethod
|
|
1911
1485
|
def __getxpmtype__(cls) -> "ObjectType":
|
|
1912
|
-
"""Get (and create if necessary) the Object type
|
|
1486
|
+
"""Get (and create if necessary) the Object type associated
|
|
1487
|
+
with thie Config object"""
|
|
1913
1488
|
xpmtype = cls.__dict__.get("__xpmtype__", None)
|
|
1914
1489
|
if xpmtype is None:
|
|
1915
1490
|
from experimaestro.core.types import ObjectType
|
|
@@ -1923,9 +1498,12 @@ class Config:
|
|
|
1923
1498
|
return xpmtype
|
|
1924
1499
|
|
|
1925
1500
|
def __new__(cls: Type[T], *args, **kwargs) -> T:
|
|
1926
|
-
"""Returns an instance of a
|
|
1927
|
-
or C if possible)
|
|
1501
|
+
"""Returns an instance of a ConfigMixin (for compatibility, use XPMConfig
|
|
1502
|
+
or C if possible)
|
|
1928
1503
|
|
|
1504
|
+
:deprecated: Use Config.C or Config.XPMConfig to construct a new
|
|
1505
|
+
configuration, and Config.V (or Config.XPMValue) for a new value
|
|
1506
|
+
"""
|
|
1929
1507
|
# If this is an XPMValue, just return a new instance
|
|
1930
1508
|
from experimaestro.core.types import XPMValue
|
|
1931
1509
|
|
|
@@ -1934,15 +1512,24 @@ class Config:
|
|
|
1934
1512
|
|
|
1935
1513
|
# If this is the XPMConfig, just return a new instance
|
|
1936
1514
|
# __init__ will be called
|
|
1937
|
-
if issubclass(cls,
|
|
1515
|
+
if issubclass(cls, ConfigMixin):
|
|
1938
1516
|
return object.__new__(cls)
|
|
1939
1517
|
|
|
1518
|
+
# Log a deprecation warning for this way of creating a configuration
|
|
1519
|
+
caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
1520
|
+
logger.warning(
|
|
1521
|
+
"Creating a configuration using Config.__new__ is deprecated, and will be removed in a future version. "
|
|
1522
|
+
"Use Config.C or Config.XPMConfig to create a new configuration. "
|
|
1523
|
+
"Issue created at %s:%s",
|
|
1524
|
+
str(Path(caller.filename).absolute()),
|
|
1525
|
+
caller.lineno,
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1940
1528
|
# otherwise, we use the configuration type
|
|
1941
|
-
o:
|
|
1529
|
+
o: ConfigMixin = object.__new__(cls.__getxpmtype__().configtype)
|
|
1942
1530
|
try:
|
|
1943
1531
|
o.__init__(*args, **kwargs)
|
|
1944
1532
|
except Exception:
|
|
1945
|
-
caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
1946
1533
|
logger.error(
|
|
1947
1534
|
"Init error in %s:%s"
|
|
1948
1535
|
% (str(Path(caller.filename).absolute()), caller.lineno)
|
|
@@ -1963,8 +1550,8 @@ class Config:
|
|
|
1963
1550
|
"""Returns a JSON version of the object (if possible)"""
|
|
1964
1551
|
return self.__xpm__.__json__()
|
|
1965
1552
|
|
|
1966
|
-
def __identifier__(self) -> Identifier:
|
|
1967
|
-
return self.__xpm__.
|
|
1553
|
+
def __identifier__(self) -> "Identifier":
|
|
1554
|
+
return self.__xpm__.full_identifier
|
|
1968
1555
|
|
|
1969
1556
|
def add_pretasks(self, *tasks: "LightweightTask"):
|
|
1970
1557
|
"""Add pre-tasks"""
|
|
@@ -2069,3 +1656,24 @@ def setmeta(config: Config, flag: bool):
|
|
|
2069
1656
|
"""Flags the configuration as a meta-parameter"""
|
|
2070
1657
|
config.__xpm__.set_meta(flag)
|
|
2071
1658
|
return config
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def cache(fn, name: str):
|
|
1662
|
+
def __call__(config, *args, **kwargs):
|
|
1663
|
+
import experimaestro.taskglobals as taskglobals
|
|
1664
|
+
|
|
1665
|
+
# Get path and create directory if needed
|
|
1666
|
+
hexid = config.__xpmidentifier__ # type: Identifier
|
|
1667
|
+
typename = config.__xpmtypename__ # type: str
|
|
1668
|
+
dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
|
|
1669
|
+
|
|
1670
|
+
if not dir.exists():
|
|
1671
|
+
dir.mkdir(parents=True, exist_ok=True)
|
|
1672
|
+
|
|
1673
|
+
path = dir / name
|
|
1674
|
+
ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
|
|
1675
|
+
with ipc_lock:
|
|
1676
|
+
r = fn(config, path, *args, **kwargs)
|
|
1677
|
+
return r
|
|
1678
|
+
|
|
1679
|
+
return __call__
|