experimaestro 1.6.1__py3-none-any.whl → 1.15.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.
- experimaestro/__init__.py +14 -3
- experimaestro/annotations.py +13 -3
- experimaestro/cli/filter.py +19 -5
- experimaestro/cli/jobs.py +12 -5
- experimaestro/commandline.py +3 -7
- experimaestro/connectors/__init__.py +27 -12
- experimaestro/connectors/local.py +19 -10
- experimaestro/connectors/ssh.py +1 -1
- experimaestro/core/arguments.py +35 -3
- experimaestro/core/callbacks.py +52 -0
- experimaestro/core/context.py +8 -9
- experimaestro/core/identifier.py +301 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +364 -716
- experimaestro/core/objects/config_utils.py +58 -0
- experimaestro/core/objects/config_walk.py +151 -0
- experimaestro/core/objects.pyi +15 -45
- experimaestro/core/serialization.py +63 -9
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/types.py +61 -6
- experimaestro/experiments/cli.py +79 -29
- experimaestro/experiments/configuration.py +3 -0
- experimaestro/generators.py +6 -1
- experimaestro/ipc.py +4 -1
- experimaestro/launcherfinder/parser.py +8 -3
- experimaestro/launcherfinder/registry.py +29 -10
- experimaestro/launcherfinder/specs.py +49 -10
- experimaestro/launchers/slurm/base.py +51 -13
- experimaestro/mkdocs/__init__.py +1 -1
- experimaestro/notifications.py +2 -1
- experimaestro/run.py +3 -1
- experimaestro/scheduler/base.py +114 -6
- experimaestro/scheduler/dynamic_outputs.py +184 -0
- experimaestro/scheduler/state.py +75 -0
- experimaestro/scheduler/workspace.py +2 -1
- experimaestro/scriptbuilder.py +13 -2
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
- experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
- experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
- experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
- experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
- experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
- experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
- experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
- experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
- experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
- experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
- experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
- experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro/server/data/favicon.ico +0 -0
- experimaestro/server/data/index.css +22963 -0
- experimaestro/server/data/index.css.map +1 -0
- experimaestro/server/data/index.html +27 -0
- experimaestro/server/data/index.js +101770 -0
- experimaestro/server/data/index.js.map +1 -0
- experimaestro/server/data/login.html +22 -0
- experimaestro/server/data/manifest.json +15 -0
- experimaestro/settings.py +2 -2
- experimaestro/sphinx/__init__.py +7 -17
- experimaestro/taskglobals.py +7 -2
- experimaestro/tests/core/__init__.py +0 -0
- experimaestro/tests/core/test_generics.py +206 -0
- experimaestro/tests/definitions_types.py +5 -3
- experimaestro/tests/launchers/bin/sbatch +34 -7
- experimaestro/tests/launchers/bin/srun +5 -0
- experimaestro/tests/launchers/common.py +16 -4
- experimaestro/tests/restart.py +9 -4
- experimaestro/tests/tasks/all.py +23 -10
- experimaestro/tests/tasks/foreign.py +2 -4
- experimaestro/tests/test_dependencies.py +0 -6
- experimaestro/tests/test_experiment.py +73 -0
- experimaestro/tests/test_findlauncher.py +11 -4
- experimaestro/tests/test_forward.py +5 -5
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +114 -99
- experimaestro/tests/test_instance.py +6 -21
- experimaestro/tests/test_objects.py +20 -4
- experimaestro/tests/test_param.py +60 -22
- experimaestro/tests/test_serializers.py +24 -64
- experimaestro/tests/test_tags.py +5 -11
- experimaestro/tests/test_tasks.py +10 -23
- experimaestro/tests/test_tokens.py +3 -2
- experimaestro/tests/test_types.py +20 -17
- experimaestro/tests/test_validation.py +48 -91
- experimaestro/tokens.py +16 -5
- experimaestro/typingutils.py +8 -8
- experimaestro/utils/asyncio.py +6 -2
- experimaestro/utils/multiprocessing.py +44 -0
- experimaestro/utils/resources.py +7 -3
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/METADATA +27 -34
- experimaestro-1.15.2.dist-info/RECORD +159 -0
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/WHEEL +1 -1
- experimaestro-1.6.1.dist-info/RECORD +0 -122
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/entry_points.txt +0 -0
- {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
"""Configuration and tasks"""
|
|
2
2
|
|
|
3
|
-
from functools import cached_property
|
|
4
3
|
import json
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
from attr import define
|
|
6
|
+
import fasteners
|
|
7
|
+
|
|
8
|
+
from experimaestro import taskglobals
|
|
9
|
+
|
|
11
10
|
from termcolor import cprint
|
|
12
|
-
import os
|
|
13
11
|
from pathlib import Path
|
|
14
|
-
import hashlib
|
|
15
12
|
import logging
|
|
16
|
-
import struct
|
|
17
13
|
import io
|
|
18
|
-
import fasteners
|
|
19
14
|
from enum import Enum
|
|
20
15
|
import inspect
|
|
21
16
|
import importlib
|
|
22
17
|
from typing import (
|
|
23
18
|
Any,
|
|
19
|
+
Callable,
|
|
24
20
|
ClassVar,
|
|
25
21
|
Dict,
|
|
26
|
-
Iterator,
|
|
27
22
|
List,
|
|
28
23
|
Optional,
|
|
29
24
|
Set,
|
|
@@ -37,301 +32,30 @@ from typing import (
|
|
|
37
32
|
import sys
|
|
38
33
|
import experimaestro
|
|
39
34
|
from experimaestro.utils import logger
|
|
40
|
-
from
|
|
41
|
-
from
|
|
42
|
-
from .context import SerializationContext, SerializedPath, SerializedPathLoader
|
|
35
|
+
from experimaestro.core.types import DeprecatedAttribute, ObjectType, TypeVarType
|
|
36
|
+
from ..context import SerializationContext, SerializedPath, SerializedPathLoader
|
|
43
37
|
|
|
44
38
|
if TYPE_CHECKING:
|
|
39
|
+
from ..callbacks import TaskEventListener
|
|
40
|
+
from ..identifier import Identifier
|
|
45
41
|
from experimaestro.scheduler.base import Job
|
|
46
42
|
from experimaestro.scheduler.workspace import RunMode
|
|
47
43
|
from experimaestro.launchers import Launcher
|
|
48
44
|
from experimaestro.scheduler import Workspace
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@cached_property
|
|
59
|
-
def all(self):
|
|
60
|
-
"""Returns the overall identifier"""
|
|
61
|
-
return self.main
|
|
62
|
-
|
|
63
|
-
def __hash__(self) -> int:
|
|
64
|
-
return hash(self.main)
|
|
65
|
-
|
|
66
|
-
def state_dict(self):
|
|
67
|
-
return self.main.hex()
|
|
68
|
-
|
|
69
|
-
def __eq__(self, other: "Identifier"):
|
|
70
|
-
return self.main == other.main
|
|
71
|
-
|
|
72
|
-
@staticmethod
|
|
73
|
-
def from_state_dict(data: Union[Dict[str, str], str]):
|
|
74
|
-
if isinstance(data, str):
|
|
75
|
-
return Identifier(bytes.fromhex(data))
|
|
76
|
-
|
|
77
|
-
return Identifier(bytes.fromhex(data["main"]))
|
|
78
|
-
|
|
79
|
-
def __repr__(self):
|
|
80
|
-
return self.main.hex()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def is_ignored(value):
|
|
84
|
-
"""Returns True if the value should be ignored by itself"""
|
|
85
|
-
return value is not None and isinstance(value, Config) and (value.__xpm__.meta)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def remove_meta(value):
|
|
89
|
-
"""Cleanup a dict/list by removing ignored values"""
|
|
90
|
-
if isinstance(value, list):
|
|
91
|
-
return [el for el in value if not is_ignored(el)]
|
|
92
|
-
if isinstance(value, dict):
|
|
93
|
-
return {key: value for key, value in value.items() if not is_ignored(value)}
|
|
94
|
-
return value
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class ConfigPath:
|
|
98
|
-
"""Used to keep track of cycles when computing a hash"""
|
|
99
|
-
|
|
100
|
-
def __init__(self):
|
|
101
|
-
self.loops: List[bool] = []
|
|
102
|
-
"""Indicates whether a loop was detected up to this node"""
|
|
103
|
-
|
|
104
|
-
self.config2index = {}
|
|
105
|
-
"""Associate an index in the list with a configuration"""
|
|
106
|
-
|
|
107
|
-
def detect_loop(self, config) -> Optional[int]:
|
|
108
|
-
"""If there is a loop, return the relative index and update the path"""
|
|
109
|
-
index = self.config2index.get(id(config), None)
|
|
110
|
-
if index is not None:
|
|
111
|
-
for i in range(index, self.depth):
|
|
112
|
-
self.loops[i] = True
|
|
113
|
-
return self.depth - index
|
|
114
|
-
|
|
115
|
-
def has_loop(self):
|
|
116
|
-
return self.loops[-1]
|
|
117
|
-
|
|
118
|
-
@property
|
|
119
|
-
def depth(self):
|
|
120
|
-
return len(self.loops)
|
|
121
|
-
|
|
122
|
-
@contextmanager
|
|
123
|
-
def push(self, config):
|
|
124
|
-
config_id = id(config)
|
|
125
|
-
assert config_id not in self.config2index
|
|
126
|
-
|
|
127
|
-
self.config2index[config_id] = self.depth
|
|
128
|
-
self.loops.append(False)
|
|
129
|
-
|
|
130
|
-
try:
|
|
131
|
-
yield
|
|
132
|
-
finally:
|
|
133
|
-
self.loops.pop()
|
|
134
|
-
del self.config2index[config_id]
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
hash_logger = logging.getLogger("xpm.hash")
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class HashComputer:
|
|
141
|
-
"""This class is in charge of computing a config/task identifier"""
|
|
142
|
-
|
|
143
|
-
OBJECT_ID = b"\x00"
|
|
144
|
-
INT_ID = b"\x01"
|
|
145
|
-
FLOAT_ID = b"\x02"
|
|
146
|
-
STR_ID = b"\x03"
|
|
147
|
-
PATH_ID = b"\x04"
|
|
148
|
-
NAME_ID = b"\x05"
|
|
149
|
-
NONE_ID = b"\x06"
|
|
150
|
-
LIST_ID = b"\x07"
|
|
151
|
-
TASK_ID = b"\x08"
|
|
152
|
-
DICT_ID = b"\x09"
|
|
153
|
-
ENUM_ID = b"\x0a"
|
|
154
|
-
CYCLE_REFERENCE = b"\x0b"
|
|
155
|
-
INIT_TASKS = b"\x0c"
|
|
156
|
-
|
|
157
|
-
def __init__(self, config: "Config", config_path: ConfigPath, *, version=None):
|
|
158
|
-
# Hasher for parameters
|
|
159
|
-
self._hasher = hashlib.sha256()
|
|
160
|
-
self.config = config
|
|
161
|
-
self.config_path = config_path
|
|
162
|
-
self.version = version or int(os.environ.get("XPM_HASH_COMPUTER", 2))
|
|
163
|
-
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
164
|
-
hash_logger.debug(
|
|
165
|
-
"starting hash (%s): %s", hash(str(self.config)), self.config
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
def identifier(self) -> Identifier:
|
|
169
|
-
main = self._hasher.digest()
|
|
170
|
-
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
171
|
-
hash_logger.debug("hash (%s): %s", hash(str(self.config)), str(main))
|
|
172
|
-
return Identifier(main)
|
|
173
|
-
|
|
174
|
-
def _hashupdate(self, bytes: bytes):
|
|
175
|
-
"""Update the hash computers with some bytes"""
|
|
176
|
-
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
177
|
-
hash_logger.debug(
|
|
178
|
-
"updating hash (%s): %s", hash(str(self.config)), str(bytes)
|
|
179
|
-
)
|
|
180
|
-
self._hasher.update(bytes)
|
|
181
|
-
|
|
182
|
-
def update(self, value, *, myself=False): # noqa: C901
|
|
183
|
-
"""Update the hash
|
|
184
|
-
|
|
185
|
-
:param value: The value to add to the hash
|
|
186
|
-
:param myself: True if the value is the configuration for which we wish
|
|
187
|
-
to compute the identifier, defaults to False
|
|
188
|
-
:raises NotImplementedError: If the value cannot be processed
|
|
189
|
-
"""
|
|
190
|
-
if value is None:
|
|
191
|
-
self._hashupdate(HashComputer.NONE_ID)
|
|
192
|
-
elif isinstance(value, float):
|
|
193
|
-
self._hashupdate(HashComputer.FLOAT_ID)
|
|
194
|
-
self._hashupdate(struct.pack("!d", value))
|
|
195
|
-
elif isinstance(value, int):
|
|
196
|
-
self._hashupdate(HashComputer.INT_ID)
|
|
197
|
-
self._hashupdate(struct.pack("!q", value))
|
|
198
|
-
elif isinstance(value, str):
|
|
199
|
-
self._hashupdate(HashComputer.STR_ID)
|
|
200
|
-
self._hashupdate(value.encode("utf-8"))
|
|
201
|
-
elif isinstance(value, list):
|
|
202
|
-
values = [el for el in value if not is_ignored(el)]
|
|
203
|
-
self._hashupdate(HashComputer.LIST_ID)
|
|
204
|
-
self._hashupdate(struct.pack("!d", len(values)))
|
|
205
|
-
for x in values:
|
|
206
|
-
self.update(x)
|
|
207
|
-
elif isinstance(value, Enum):
|
|
208
|
-
self._hashupdate(HashComputer.ENUM_ID)
|
|
209
|
-
k = value.__class__
|
|
210
|
-
self._hashupdate(
|
|
211
|
-
f"{k.__module__}.{k.__qualname__ }:{value.name}".encode("utf-8"),
|
|
212
|
-
)
|
|
213
|
-
elif isinstance(value, dict):
|
|
214
|
-
self._hashupdate(HashComputer.DICT_ID)
|
|
215
|
-
items = [
|
|
216
|
-
(key, value) for key, value in value.items() if not is_ignored(value)
|
|
217
|
-
]
|
|
218
|
-
items.sort(key=lambda x: x[0])
|
|
219
|
-
for key, value in items:
|
|
220
|
-
self.update(key)
|
|
221
|
-
self.update(value)
|
|
222
|
-
|
|
223
|
-
# Handles configurations
|
|
224
|
-
elif isinstance(value, Config):
|
|
225
|
-
# Encodes the identifier
|
|
226
|
-
self._hashupdate(HashComputer.OBJECT_ID)
|
|
227
|
-
|
|
228
|
-
# If we encode another config, then
|
|
229
|
-
if not myself:
|
|
230
|
-
if loop_ix := self.config_path.detect_loop(value):
|
|
231
|
-
# Loop detected: use cycle reference
|
|
232
|
-
self._hashupdate(HashComputer.CYCLE_REFERENCE)
|
|
233
|
-
self._hashupdate(struct.pack("!q", loop_ix))
|
|
234
|
-
|
|
235
|
-
else:
|
|
236
|
-
# Just use the object identifier
|
|
237
|
-
value_id = HashComputer.compute(
|
|
238
|
-
value, version=self.version, config_path=self.config_path
|
|
239
|
-
)
|
|
240
|
-
self._hashupdate(value_id.all)
|
|
241
|
-
|
|
242
|
-
# And that's it!
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
# Process tasks
|
|
246
|
-
if value.__xpm__.task is not None and (value.__xpm__.task is not value):
|
|
247
|
-
hash_logger.debug("Computing hash for task %s", value.__xpm__.task)
|
|
248
|
-
self._hashupdate(HashComputer.TASK_ID)
|
|
249
|
-
self.update(value.__xpm__.task)
|
|
250
|
-
|
|
251
|
-
xpmtype = value.__xpmtype__
|
|
252
|
-
self._hashupdate(xpmtype.identifier.name.encode("utf-8"))
|
|
253
|
-
|
|
254
|
-
# Process arguments (sort by name to ensure uniqueness)
|
|
255
|
-
arguments = sorted(xpmtype.arguments.values(), key=lambda a: a.name)
|
|
256
|
-
for argument in arguments:
|
|
257
|
-
# Ignored argument
|
|
258
|
-
if argument.ignored:
|
|
259
|
-
argvalue = value.__xpm__.values.get(argument.name, None)
|
|
260
|
-
|
|
261
|
-
# ... unless meta is set to false
|
|
262
|
-
if (
|
|
263
|
-
argvalue is None
|
|
264
|
-
or not isinstance(argvalue, Config)
|
|
265
|
-
or (argvalue.__xpm__.meta is not False)
|
|
266
|
-
):
|
|
267
|
-
continue
|
|
268
|
-
|
|
269
|
-
if argument.generator:
|
|
270
|
-
continue
|
|
271
|
-
|
|
272
|
-
# Argument value
|
|
273
|
-
# Skip if the argument is not a constant, and
|
|
274
|
-
# - optional argument: both value and default are None
|
|
275
|
-
# - the argument value is equal to the default value
|
|
276
|
-
argvalue = getattr(value, argument.name, None)
|
|
277
|
-
if not argument.constant and (
|
|
278
|
-
(
|
|
279
|
-
not argument.required
|
|
280
|
-
and argument.default is None
|
|
281
|
-
and argvalue is None
|
|
282
|
-
)
|
|
283
|
-
or (
|
|
284
|
-
argument.default is not None
|
|
285
|
-
and argument.default == remove_meta(argvalue)
|
|
286
|
-
)
|
|
287
|
-
):
|
|
288
|
-
# No update if same value (and not constant)
|
|
289
|
-
continue
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
argvalue is not None
|
|
293
|
-
and isinstance(argvalue, Config)
|
|
294
|
-
and argvalue.__xpm__.meta
|
|
295
|
-
):
|
|
296
|
-
continue
|
|
297
|
-
|
|
298
|
-
# Hash name
|
|
299
|
-
self.update(argument.name)
|
|
300
|
-
|
|
301
|
-
# Hash value
|
|
302
|
-
self._hashupdate(HashComputer.NAME_ID)
|
|
303
|
-
self.update(argvalue)
|
|
304
|
-
|
|
305
|
-
else:
|
|
306
|
-
raise NotImplementedError("Cannot compute hash of type %s" % type(value))
|
|
307
|
-
|
|
308
|
-
@staticmethod
|
|
309
|
-
def compute(
|
|
310
|
-
config: "Config", config_path: ConfigPath = None, version=None
|
|
311
|
-
) -> Identifier:
|
|
312
|
-
"""Compute the identifier for a configuration
|
|
313
|
-
|
|
314
|
-
:param config: the configuration for which we compute the identifier
|
|
315
|
-
:param config_path: used to track down cycles between configurations
|
|
316
|
-
:param version: version for the hash computation (None for the last one)
|
|
317
|
-
"""
|
|
318
|
-
|
|
319
|
-
# Try to use the cached value first
|
|
320
|
-
# (if there are no loops)
|
|
321
|
-
if config.__xpm__._sealed:
|
|
322
|
-
identifier = config.__xpm__._raw_identifier
|
|
323
|
-
if identifier is not None and not identifier.has_loops:
|
|
324
|
-
return identifier
|
|
46
|
+
from .config_walk import ConfigWalk, ConfigWalkContext
|
|
47
|
+
from .config_utils import (
|
|
48
|
+
getqualattr,
|
|
49
|
+
add_to_path,
|
|
50
|
+
TaggedValue,
|
|
51
|
+
ObjectStore,
|
|
52
|
+
classproperty,
|
|
53
|
+
)
|
|
325
54
|
|
|
326
|
-
|
|
55
|
+
T = TypeVar("T", bound="Config")
|
|
327
56
|
|
|
328
|
-
with config_path.push(config):
|
|
329
|
-
self = HashComputer(config, config_path, version=version)
|
|
330
|
-
self.update(config, myself=True)
|
|
331
|
-
identifier = self.identifier()
|
|
332
|
-
identifier.has_loop = config_path.has_loop()
|
|
333
57
|
|
|
334
|
-
|
|
58
|
+
DependentMarker = Callable[["Config"], None]
|
|
335
59
|
|
|
336
60
|
|
|
337
61
|
def updatedependencies(
|
|
@@ -362,197 +86,25 @@ def updatedependencies(
|
|
|
362
86
|
raise NotImplementedError("update dependencies for type %s" % type(value))
|
|
363
87
|
|
|
364
88
|
|
|
365
|
-
class SealedError(Exception):
|
|
366
|
-
"""Exception when trying to modify a sealed configuration"""
|
|
367
|
-
|
|
368
|
-
pass
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
class TaggedValue:
|
|
372
|
-
def __init__(self, value):
|
|
373
|
-
self.value = value
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
@contextmanager
|
|
377
|
-
def add_to_path(p):
|
|
378
|
-
"""Temporarily add a path to sys.path"""
|
|
379
|
-
import sys
|
|
380
|
-
|
|
381
|
-
old_path = sys.path
|
|
382
|
-
sys.path = sys.path[:]
|
|
383
|
-
sys.path.insert(0, p)
|
|
384
|
-
try:
|
|
385
|
-
yield
|
|
386
|
-
finally:
|
|
387
|
-
sys.path = old_path
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
class ConfigWalkContext:
|
|
391
|
-
"""Context when generating values in configurations"""
|
|
392
|
-
|
|
393
|
-
@property
|
|
394
|
-
def path(self):
|
|
395
|
-
"""Returns the path of the job directory"""
|
|
396
|
-
raise NotImplementedError()
|
|
397
|
-
|
|
398
|
-
def __init__(self):
|
|
399
|
-
self._configpath = None
|
|
400
|
-
|
|
401
|
-
@property
|
|
402
|
-
def task(self):
|
|
403
|
-
return None
|
|
404
|
-
|
|
405
|
-
def currentpath(self) -> Path:
|
|
406
|
-
"""Returns the configuration folder"""
|
|
407
|
-
if self._configpath:
|
|
408
|
-
return self.path / self._configpath
|
|
409
|
-
return self.path
|
|
410
|
-
|
|
411
|
-
@contextmanager
|
|
412
|
-
def push(self, key: str):
|
|
413
|
-
"""Push a new key to contextualize paths"""
|
|
414
|
-
p = self._configpath
|
|
415
|
-
try:
|
|
416
|
-
self._configpath = (Path("out") if p is None else p) / key
|
|
417
|
-
yield key
|
|
418
|
-
finally:
|
|
419
|
-
self._configpath = p
|
|
420
|
-
|
|
421
|
-
|
|
422
89
|
NOT_SET = object()
|
|
423
90
|
|
|
424
91
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"""
|
|
430
|
-
|
|
431
|
-
:param recurse_task: Recurse into linked tasks
|
|
432
|
-
:param context: The context, by default only tracks the position in the
|
|
433
|
-
config tree
|
|
434
|
-
"""
|
|
435
|
-
self.recurse_task = recurse_task
|
|
436
|
-
self.context = ConfigWalkContext() if context is None else context
|
|
437
|
-
|
|
438
|
-
# Stores already visited nodes
|
|
439
|
-
self.visited = {}
|
|
440
|
-
|
|
441
|
-
def preprocess(self, config: "Config") -> Tuple[bool, Any]:
|
|
442
|
-
"""Returns a tuple boolean/value
|
|
443
|
-
|
|
444
|
-
The boolean value is used to stop the processing if False.
|
|
445
|
-
The value is returned
|
|
446
|
-
"""
|
|
447
|
-
return True, None
|
|
448
|
-
|
|
449
|
-
def postprocess(self, stub, config: "Config", values: Dict[str, Any]):
|
|
450
|
-
return stub
|
|
451
|
-
|
|
452
|
-
def list(self, i: int):
|
|
453
|
-
return self.context.push(str(i))
|
|
454
|
-
|
|
455
|
-
def map(self, k: str):
|
|
456
|
-
return self.context.push(k)
|
|
457
|
-
|
|
458
|
-
def stub(self, config: "Config"):
|
|
459
|
-
return config
|
|
460
|
-
|
|
461
|
-
def __call__(self, x):
|
|
462
|
-
if isinstance(x, Config):
|
|
463
|
-
info = x.__xpm__ # type: ConfigInformation
|
|
464
|
-
|
|
465
|
-
# Avoid loops
|
|
466
|
-
xid = id(x)
|
|
467
|
-
if xid in self.visited:
|
|
468
|
-
return self.visited[xid]
|
|
469
|
-
|
|
470
|
-
# Get a stub
|
|
471
|
-
stub = self.stub(x)
|
|
472
|
-
self.visited[xid] = stub
|
|
473
|
-
|
|
474
|
-
# Pre-process
|
|
475
|
-
flag, value = self.preprocess(x)
|
|
476
|
-
|
|
477
|
-
if not flag:
|
|
478
|
-
# Stop processing and returns value
|
|
479
|
-
return value
|
|
480
|
-
|
|
481
|
-
# Process all the arguments
|
|
482
|
-
result = {}
|
|
483
|
-
for arg, v in info.xpmvalues():
|
|
484
|
-
if v is not None:
|
|
485
|
-
with self.map(arg.name):
|
|
486
|
-
result[arg.name] = self(v)
|
|
487
|
-
else:
|
|
488
|
-
result[arg.name] = None
|
|
489
|
-
|
|
490
|
-
# Deals with pre-tasks
|
|
491
|
-
if info.pre_tasks:
|
|
492
|
-
with self.map("__pre_tasks__"):
|
|
493
|
-
self(info.pre_tasks)
|
|
92
|
+
@define()
|
|
93
|
+
class WatchedOutput:
|
|
94
|
+
#: The enclosing job
|
|
95
|
+
job: "Job"
|
|
494
96
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
self(info.init_tasks)
|
|
97
|
+
#: The configuration containing the watched output
|
|
98
|
+
config: "ConfigInformation"
|
|
498
99
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
x.__xpm__.task is not None
|
|
502
|
-
and self.recurse_task
|
|
503
|
-
and x.__xpm__.task is not x
|
|
504
|
-
):
|
|
505
|
-
self(x.__xpm__.task)
|
|
100
|
+
#: The watched output (name)
|
|
101
|
+
method_name: str
|
|
506
102
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return processed
|
|
103
|
+
#: The watched output method (called with the JSON event)
|
|
104
|
+
method: Callable
|
|
510
105
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
for i, sv in enumerate(x):
|
|
514
|
-
with self.list(i):
|
|
515
|
-
result.append(self(sv))
|
|
516
|
-
return result
|
|
517
|
-
|
|
518
|
-
if isinstance(x, dict):
|
|
519
|
-
result = {}
|
|
520
|
-
for key, value in x.items():
|
|
521
|
-
assert isinstance(key, (str, float, int))
|
|
522
|
-
with self.map(key):
|
|
523
|
-
result[key] = self(value)
|
|
524
|
-
return result
|
|
525
|
-
|
|
526
|
-
if isinstance(x, (float, int, str, Path, Enum)):
|
|
527
|
-
return x
|
|
528
|
-
|
|
529
|
-
raise NotImplementedError(f"Cannot handle a value of type {type(x)}")
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
def getqualattr(module, qualname):
|
|
533
|
-
"""Get a qualified attributed value"""
|
|
534
|
-
cls = module
|
|
535
|
-
for part in qualname.split("."):
|
|
536
|
-
cls = getattr(cls, part)
|
|
537
|
-
return cls
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
class ObjectStore:
|
|
541
|
-
def __init__(self):
|
|
542
|
-
self.store: Dict[int, Any] = {}
|
|
543
|
-
self.constructed: Set[int] = set()
|
|
544
|
-
|
|
545
|
-
def set_constructed(self, identifier: int):
|
|
546
|
-
self.constructed.add(identifier)
|
|
547
|
-
|
|
548
|
-
def is_constructed(self, identifier: int):
|
|
549
|
-
return identifier in self.constructed
|
|
550
|
-
|
|
551
|
-
def retrieve(self, identifier: int):
|
|
552
|
-
return self.store.get(identifier, None)
|
|
553
|
-
|
|
554
|
-
def add_stub(self, identifier: int, stub: Any):
|
|
555
|
-
self.store[identifier] = stub
|
|
106
|
+
#: The callback to call (with the output of the previous method)
|
|
107
|
+
callback: Callable
|
|
556
108
|
|
|
557
109
|
|
|
558
110
|
class ConfigInformation:
|
|
@@ -564,14 +116,14 @@ class ConfigInformation:
|
|
|
564
116
|
# Set to true when loading from JSON
|
|
565
117
|
LOADING: ClassVar[bool] = False
|
|
566
118
|
|
|
567
|
-
def __init__(self, pyobject: "
|
|
119
|
+
def __init__(self, pyobject: "ConfigMixin"):
|
|
568
120
|
# The underlying pyobject and XPM type
|
|
569
121
|
self.pyobject = pyobject
|
|
570
|
-
self.xpmtype = pyobject.__xpmtype__
|
|
122
|
+
self.xpmtype: "ObjectType" = pyobject.__xpmtype__
|
|
571
123
|
self.values = {}
|
|
572
124
|
|
|
573
125
|
# Meta-informations
|
|
574
|
-
self._tags = {}
|
|
126
|
+
self._tags: dict[str, Any] = {}
|
|
575
127
|
self._initinfo = ""
|
|
576
128
|
|
|
577
129
|
self._taskoutput = None
|
|
@@ -582,31 +134,61 @@ class ConfigInformation:
|
|
|
582
134
|
|
|
583
135
|
# State information
|
|
584
136
|
self.job = None
|
|
137
|
+
self._job_listener: "TaskEventListener" | None = None
|
|
585
138
|
|
|
586
139
|
#: True when this configuration was loaded from disk
|
|
587
140
|
self.loaded = False
|
|
588
141
|
|
|
589
|
-
#
|
|
142
|
+
# Explicitly added dependencies
|
|
590
143
|
self.dependencies = []
|
|
591
144
|
|
|
592
|
-
#
|
|
593
|
-
|
|
145
|
+
# Concrete type variables resolutions
|
|
146
|
+
# This is used to check typevars coherence
|
|
147
|
+
self.concrete_typevars: Dict[TypeVar, type] = {}
|
|
594
148
|
|
|
595
149
|
# Initialization tasks
|
|
596
150
|
self.init_tasks: List["LightweightTask"] = []
|
|
597
151
|
|
|
598
|
-
#
|
|
152
|
+
# Watched outputs
|
|
153
|
+
self.watched_outputs: List[WatchedOutput] = []
|
|
599
154
|
|
|
600
|
-
|
|
601
|
-
"""The full identifier (with pre-tasks)"""
|
|
155
|
+
# Cached information
|
|
602
156
|
|
|
603
|
-
self.
|
|
604
|
-
"""The identifier
|
|
157
|
+
self._identifier = None
|
|
158
|
+
"""The configuration identifier (cached when sealed)"""
|
|
605
159
|
|
|
606
160
|
self._validated = False
|
|
607
161
|
self._sealed = False
|
|
608
162
|
self._meta = None
|
|
609
163
|
|
|
164
|
+
# This contains the list of generated values (using context) in this
|
|
165
|
+
# configuration or any sub-configuration, is generated. This prevents
|
|
166
|
+
# problem when a configuration with generated values is re-used.
|
|
167
|
+
self._generated_values = []
|
|
168
|
+
|
|
169
|
+
def get_generated_paths(
|
|
170
|
+
self, path: list[str] = None, paths: list[str] = None
|
|
171
|
+
) -> list[str]:
|
|
172
|
+
"""Get the list of generated paths, useful to track down those
|
|
173
|
+
|
|
174
|
+
:param path: The current path
|
|
175
|
+
:param paths: The list of generated paths so far, defaults to None
|
|
176
|
+
:return: The full list of generated paths
|
|
177
|
+
"""
|
|
178
|
+
paths = [] if paths is None else paths
|
|
179
|
+
path = [] if path is None else path
|
|
180
|
+
|
|
181
|
+
for key in self._generated_values:
|
|
182
|
+
value = self.values[key]
|
|
183
|
+
if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
|
|
184
|
+
path.append(key)
|
|
185
|
+
value.__xpm__.get_generated_paths(path, paths)
|
|
186
|
+
path.pop()
|
|
187
|
+
else:
|
|
188
|
+
paths.append(".".join(path + [key]))
|
|
189
|
+
|
|
190
|
+
return paths
|
|
191
|
+
|
|
610
192
|
def set_meta(self, value: Optional[bool]):
|
|
611
193
|
"""Sets the meta flag"""
|
|
612
194
|
assert not self._sealed, "Configuration is sealed"
|
|
@@ -624,7 +206,34 @@ class ConfigInformation:
|
|
|
624
206
|
# Not an argument, bypass
|
|
625
207
|
return object.__getattribute__(self.pyobject, name)
|
|
626
208
|
|
|
209
|
+
@staticmethod
|
|
210
|
+
def is_generated_value(argument, value):
|
|
211
|
+
if argument.ignore_generated:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
if value is None:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
if isinstance(value, (int, str, float, bool, Path)):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
if isinstance(value, ConfigMixin):
|
|
221
|
+
return value.__xpm__._generated_values and value.__xpm__.task is None
|
|
222
|
+
|
|
223
|
+
if isinstance(value, list):
|
|
224
|
+
return any(ConfigInformation.is_generated_value(argument, x) for x in value)
|
|
225
|
+
|
|
226
|
+
if isinstance(value, dict):
|
|
227
|
+
return any(
|
|
228
|
+
ConfigInformation.is_generated_value(argument, x)
|
|
229
|
+
for x in value.values()
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return False
|
|
233
|
+
|
|
627
234
|
def set(self, k, v, bypass=False):
|
|
235
|
+
from experimaestro.generators import Generator
|
|
236
|
+
|
|
628
237
|
# Not an argument, bypass
|
|
629
238
|
if k not in self.xpmtype.arguments:
|
|
630
239
|
setattr(self.pyobject, k, v)
|
|
@@ -633,13 +242,34 @@ class ConfigInformation:
|
|
|
633
242
|
if self._sealed and not bypass:
|
|
634
243
|
raise AttributeError(f"Object is read-only (trying to set {k})")
|
|
635
244
|
|
|
245
|
+
if not isinstance(v, ConfigMixin) and isinstance(v, Config):
|
|
246
|
+
raise AttributeError(
|
|
247
|
+
"Configuration (and not objects) should be used. Consider using .C(...)"
|
|
248
|
+
)
|
|
249
|
+
|
|
636
250
|
try:
|
|
637
251
|
argument = self.xpmtype.arguments.get(k, None)
|
|
638
252
|
if argument:
|
|
639
|
-
if
|
|
253
|
+
if ConfigInformation.is_generated_value(argument, v):
|
|
254
|
+
raise AttributeError(
|
|
255
|
+
f"Cannot set {k} to a configuration with generated values. "
|
|
256
|
+
"Here is the list of paths to help you: "
|
|
257
|
+
f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if not bypass and (
|
|
261
|
+
(isinstance(argument.generator, Generator)) or argument.constant
|
|
262
|
+
):
|
|
640
263
|
raise AttributeError("Property %s is read-only" % (k))
|
|
641
264
|
if v is not None:
|
|
642
265
|
self.values[k] = argument.validate(v)
|
|
266
|
+
# Check for type variables
|
|
267
|
+
if type(argument.type) is TypeVarType:
|
|
268
|
+
self.check_typevar(argument.type.typevar, type(v))
|
|
269
|
+
if isinstance(v, Config):
|
|
270
|
+
# If the value is a Config, fuse type variables
|
|
271
|
+
v.__xpm__.fuse_concrete_typevars(self.concrete_typevars)
|
|
272
|
+
self.fuse_concrete_typevars(v.__xpm__.concrete_typevars)
|
|
643
273
|
elif argument.required:
|
|
644
274
|
raise AttributeError("Cannot set required attribute to None")
|
|
645
275
|
else:
|
|
@@ -652,6 +282,43 @@ class ConfigInformation:
|
|
|
652
282
|
logger.error("Error while setting value %s in %s", k, self.xpmtype)
|
|
653
283
|
raise
|
|
654
284
|
|
|
285
|
+
def fuse_concrete_typevars(self, typevars: Dict[TypeVar, type]):
|
|
286
|
+
"""Fuses concrete type variables with the current ones"""
|
|
287
|
+
for typevar, v in typevars.items():
|
|
288
|
+
self.check_typevar(typevar, v)
|
|
289
|
+
|
|
290
|
+
def check_typevar(self, typevar: TypeVar, v: type):
|
|
291
|
+
"""Check if a type variable is coherent with the current typevars bindings,
|
|
292
|
+
updates the bindings if necessary"""
|
|
293
|
+
if typevar not in self.concrete_typevars:
|
|
294
|
+
self.concrete_typevars[typevar] = v
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
concrete_typevar = self.concrete_typevars[typevar]
|
|
298
|
+
bound = typevar.__bound__
|
|
299
|
+
# Check that v is a subclass of the typevar OR that typevar is a subclass of v
|
|
300
|
+
# Then set the concrete type variable to the most generic type
|
|
301
|
+
|
|
302
|
+
# First, limiting to the specified bound
|
|
303
|
+
if bound is not None:
|
|
304
|
+
if not issubclass(v, bound):
|
|
305
|
+
raise TypeError(
|
|
306
|
+
f"Type variable {typevar} is bound to {bound}, but tried to set it to {v}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if issubclass(v, concrete_typevar):
|
|
310
|
+
# v is a subclass of the typevar, keep the typevar
|
|
311
|
+
return
|
|
312
|
+
if issubclass(concrete_typevar, v):
|
|
313
|
+
# typevar is a subclass of v, keep v
|
|
314
|
+
self.concrete_typevars[typevar] = v
|
|
315
|
+
return
|
|
316
|
+
raise TypeError(
|
|
317
|
+
f"Type variable {typevar} is already set to {self.concrete_typevars[typevar]}, "
|
|
318
|
+
f"but tried to set it to {v}"
|
|
319
|
+
f" (current typevars bindings: {self.concrete_typevars})"
|
|
320
|
+
)
|
|
321
|
+
|
|
655
322
|
def addtag(self, name, value):
|
|
656
323
|
self._tags[name] = value
|
|
657
324
|
|
|
@@ -691,10 +358,6 @@ class ConfigInformation:
|
|
|
691
358
|
% (k, self.xpmtype, self._initinfo)
|
|
692
359
|
)
|
|
693
360
|
|
|
694
|
-
# Validate pre-tasks
|
|
695
|
-
for pre_task in self.pre_tasks:
|
|
696
|
-
pre_task.__xpm__.validate()
|
|
697
|
-
|
|
698
361
|
# Validate init tasks
|
|
699
362
|
for init_task in self.init_tasks:
|
|
700
363
|
init_task.__xpm__.validate()
|
|
@@ -715,18 +378,62 @@ class ConfigInformation:
|
|
|
715
378
|
Arguments:
|
|
716
379
|
- context: the generation context
|
|
717
380
|
"""
|
|
381
|
+
if generated_keys := [
|
|
382
|
+
k
|
|
383
|
+
for k, v in self.values.items()
|
|
384
|
+
if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
|
|
385
|
+
]:
|
|
386
|
+
raise AttributeError(
|
|
387
|
+
"Cannot seal a configuration with generated values:"
|
|
388
|
+
f"""{",".join(generated_keys)} in {context.currentpath}"""
|
|
389
|
+
)
|
|
718
390
|
|
|
719
391
|
class Sealer(ConfigWalk):
|
|
720
|
-
def preprocess(self, config:
|
|
392
|
+
def preprocess(self, config: ConfigMixin):
|
|
721
393
|
return not config.__xpm__._sealed, config
|
|
722
394
|
|
|
723
|
-
def postprocess(self, stub, config:
|
|
395
|
+
def postprocess(self, stub, config: ConfigMixin, values):
|
|
724
396
|
# Generate values
|
|
397
|
+
from experimaestro.generators import Generator
|
|
398
|
+
|
|
725
399
|
for k, argument in config.__xpmtype__.arguments.items():
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
400
|
+
try:
|
|
401
|
+
if argument.generator:
|
|
402
|
+
if not isinstance(argument.generator, Generator):
|
|
403
|
+
# Don't set if already set
|
|
404
|
+
if config.__xpm__.values.get(k) is not None:
|
|
405
|
+
continue
|
|
406
|
+
value = argument.generator()
|
|
407
|
+
else:
|
|
408
|
+
# Generate a value
|
|
409
|
+
sig = inspect.signature(argument.generator)
|
|
410
|
+
if len(sig.parameters) == 0:
|
|
411
|
+
value = argument.generator()
|
|
412
|
+
elif len(sig.parameters) == 2:
|
|
413
|
+
# Only in that case do we need to flag this configuration
|
|
414
|
+
# as containing generated values
|
|
415
|
+
config.__xpm__._generated_values.append(k)
|
|
416
|
+
value = argument.generator(self.context, config)
|
|
417
|
+
else:
|
|
418
|
+
assert (
|
|
419
|
+
False
|
|
420
|
+
), "generator has either two parameters (context and config), or none"
|
|
421
|
+
config.__xpm__.set(k, value, bypass=True)
|
|
422
|
+
else:
|
|
423
|
+
value = config.__xpm__.values.get(k)
|
|
424
|
+
except Exception:
|
|
425
|
+
logger.error(
|
|
426
|
+
"While setting %s of %s", argument.name, config.__xpmtype__
|
|
729
427
|
)
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
# Propagate the generated value flag
|
|
431
|
+
if (
|
|
432
|
+
(value is not None)
|
|
433
|
+
and isinstance(value, ConfigMixin)
|
|
434
|
+
and value.__xpm__._generated_values
|
|
435
|
+
):
|
|
436
|
+
config.__xpm__._generated_values.append(k)
|
|
730
437
|
|
|
731
438
|
config.__xpm__._sealed = True
|
|
732
439
|
|
|
@@ -740,89 +447,29 @@ class ConfigInformation:
|
|
|
740
447
|
context = ConfigWalkContext()
|
|
741
448
|
|
|
742
449
|
class Unsealer(ConfigWalk):
|
|
743
|
-
def preprocess(self, config:
|
|
450
|
+
def preprocess(self, config: ConfigMixin):
|
|
744
451
|
return config.__xpm__._sealed, config
|
|
745
452
|
|
|
746
|
-
def postprocess(self, stub, config:
|
|
453
|
+
def postprocess(self, stub, config: ConfigMixin, values):
|
|
747
454
|
config.__xpm__._sealed = False
|
|
748
455
|
config.__xpm__._identifier = None
|
|
749
456
|
|
|
750
457
|
Unsealer(context, recurse_task=True)(self.pyobject)
|
|
751
458
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
pre_tasks: Dict[int, "Config"] = {}
|
|
755
|
-
|
|
756
|
-
class PreTaskCollect(ConfigWalk):
|
|
757
|
-
def preprocess(self, config: Config):
|
|
758
|
-
# Do not cross tasks
|
|
759
|
-
return not isinstance(config.__xpm__, Task), config
|
|
760
|
-
|
|
761
|
-
def postprocess(self, stub, config: Config, values):
|
|
762
|
-
pre_tasks.update(
|
|
763
|
-
{id(pre_task): pre_task for pre_task in config.__xpm__.pre_tasks}
|
|
764
|
-
)
|
|
765
|
-
|
|
766
|
-
PreTaskCollect(context, recurse_task=True)(self.pyobject)
|
|
767
|
-
return pre_tasks.values()
|
|
768
|
-
|
|
769
|
-
def identifiers(self, only_raw: bool):
|
|
459
|
+
@property
|
|
460
|
+
def identifier(self):
|
|
770
461
|
"""Computes the unique identifier"""
|
|
771
|
-
|
|
772
|
-
raw_identifier = self._raw_identifier
|
|
773
|
-
full_identifier = self._full_identifier
|
|
462
|
+
from ..identifier import IdentifierComputer
|
|
774
463
|
|
|
775
464
|
# Computes raw identifier if needed
|
|
776
|
-
if
|
|
777
|
-
|
|
778
|
-
raw_identifier = HashComputer.compute(self.pyobject)
|
|
779
|
-
if self._sealed:
|
|
780
|
-
self._raw_identifier = raw_identifier
|
|
781
|
-
|
|
782
|
-
if only_raw:
|
|
783
|
-
return raw_identifier, full_identifier
|
|
784
|
-
|
|
785
|
-
# OK, let's compute the full identifier
|
|
786
|
-
if full_identifier is None or not self._sealed:
|
|
787
|
-
# Compute the full identifier by including the pre-tasks
|
|
788
|
-
hasher = hashlib.sha256()
|
|
789
|
-
hasher.update(raw_identifier.all)
|
|
790
|
-
pre_tasks_ids = [
|
|
791
|
-
pre_task.__xpm__.raw_identifier.all
|
|
792
|
-
for pre_task in self.collect_pre_tasks()
|
|
793
|
-
]
|
|
794
|
-
for task_id in sorted(pre_tasks_ids):
|
|
795
|
-
hasher.update(task_id)
|
|
796
|
-
|
|
797
|
-
# Adds init tasks
|
|
798
|
-
if self.init_tasks:
|
|
799
|
-
hasher.update(HashComputer.INIT_TASKS)
|
|
800
|
-
for init_task in self.init_tasks:
|
|
801
|
-
hasher.update(init_task.__xpm__.raw_identifier.all)
|
|
802
|
-
|
|
803
|
-
full_identifier = Identifier(hasher.digest())
|
|
804
|
-
full_identifier.has_loops = raw_identifier.has_loops
|
|
805
|
-
|
|
806
|
-
# Only cache the identifier if sealed
|
|
807
|
-
if self._sealed:
|
|
808
|
-
self._full_identifier = full_identifier
|
|
809
|
-
|
|
810
|
-
return raw_identifier, full_identifier
|
|
811
|
-
|
|
812
|
-
@property
|
|
813
|
-
def raw_identifier(self) -> Identifier:
|
|
814
|
-
"""Computes the unique identifier (without task modifiers)"""
|
|
815
|
-
raw_identifier, _ = self.identifiers(True)
|
|
816
|
-
return raw_identifier
|
|
817
|
-
|
|
818
|
-
@property
|
|
819
|
-
def full_identifier(self) -> Identifier:
|
|
820
|
-
"""Computes the unique identifier (with task modifiers)"""
|
|
821
|
-
_, full_identifier = self.identifiers(False)
|
|
822
|
-
return full_identifier
|
|
465
|
+
if self._identifier is not None:
|
|
466
|
+
return self._identifier
|
|
823
467
|
|
|
824
|
-
|
|
825
|
-
|
|
468
|
+
# Get the main identifier
|
|
469
|
+
identifier = IdentifierComputer.compute(self.pyobject)
|
|
470
|
+
if self._sealed:
|
|
471
|
+
self._identifier = identifier
|
|
472
|
+
return identifier
|
|
826
473
|
|
|
827
474
|
def dependency(self):
|
|
828
475
|
"""Returns a dependency"""
|
|
@@ -837,12 +484,6 @@ class ConfigInformation:
|
|
|
837
484
|
path: List[str],
|
|
838
485
|
taskids: Set[int],
|
|
839
486
|
):
|
|
840
|
-
# Add pre-tasks
|
|
841
|
-
for pre_task in self.pre_tasks:
|
|
842
|
-
pre_task.__xpm__.updatedependencies(
|
|
843
|
-
dependencies, path + ["__pre_tasks__"], taskids
|
|
844
|
-
)
|
|
845
|
-
|
|
846
487
|
# Add initialization tasks
|
|
847
488
|
for init_task in self.init_tasks:
|
|
848
489
|
init_task.__xpm__.updatedependencies(
|
|
@@ -899,6 +540,28 @@ class ConfigInformation:
|
|
|
899
540
|
# Now, seal the object
|
|
900
541
|
self.seal(context)
|
|
901
542
|
|
|
543
|
+
def watch_output(self, method, callback):
|
|
544
|
+
"""Watch the task output linked with a given method
|
|
545
|
+
|
|
546
|
+
:param method: The method to watch
|
|
547
|
+
:param callback: The callback
|
|
548
|
+
"""
|
|
549
|
+
watched = WatchedOutput(
|
|
550
|
+
self, method.__self__, method.__name__, method, callback
|
|
551
|
+
)
|
|
552
|
+
self.watched_outputs.append(watched)
|
|
553
|
+
if self.job:
|
|
554
|
+
self.job.watch_output(watched)
|
|
555
|
+
|
|
556
|
+
def on_completed(self, callback: Callable[[], None]):
|
|
557
|
+
"""Call a method when the task is completed successfully
|
|
558
|
+
|
|
559
|
+
:param callback: _description_
|
|
560
|
+
"""
|
|
561
|
+
from ..callbacks import TaskEventListener
|
|
562
|
+
|
|
563
|
+
TaskEventListener.on_completed(self, callback)
|
|
564
|
+
|
|
902
565
|
def submit(
|
|
903
566
|
self,
|
|
904
567
|
workspace: "Workspace",
|
|
@@ -909,6 +572,7 @@ class ConfigInformation:
|
|
|
909
572
|
):
|
|
910
573
|
from experimaestro.scheduler import experiment, JobContext
|
|
911
574
|
from experimaestro.scheduler.workspace import RunMode
|
|
575
|
+
from ..callbacks import TaskEventListener
|
|
912
576
|
|
|
913
577
|
# --- Prepare the object
|
|
914
578
|
|
|
@@ -961,10 +625,12 @@ class ConfigInformation:
|
|
|
961
625
|
workspace.run_mode if run_mode is None else run_mode
|
|
962
626
|
) or RunMode.NORMAL
|
|
963
627
|
if run_mode == RunMode.NORMAL:
|
|
628
|
+
TaskEventListener.connect(experiment.CURRENT)
|
|
629
|
+
experiment.CURRENT.submit(self.job)
|
|
964
630
|
other = experiment.CURRENT.submit(self.job)
|
|
965
631
|
if other:
|
|
966
|
-
#
|
|
967
|
-
|
|
632
|
+
# Our job = previously submitted job
|
|
633
|
+
self.job = other
|
|
968
634
|
else:
|
|
969
635
|
# Show a warning
|
|
970
636
|
if run_mode == RunMode.GENERATE_ONLY:
|
|
@@ -1000,23 +666,22 @@ class ConfigInformation:
|
|
|
1000
666
|
|
|
1001
667
|
print(file=sys.stderr) # noqa: T201
|
|
1002
668
|
|
|
1003
|
-
# Handle an output configuration
|
|
1004
|
-
def mark_output(config: "Config"):
|
|
1005
|
-
"""Sets a dependency on the job"""
|
|
1006
|
-
assert not isinstance(config, Task), "Cannot set a dependency on a task"
|
|
1007
|
-
config.__xpm__.task = self.pyobject
|
|
1008
|
-
return config
|
|
1009
|
-
|
|
1010
669
|
# Mark this configuration also
|
|
1011
670
|
self.task = self.pyobject
|
|
1012
671
|
|
|
1013
672
|
if hasattr(self.pyobject, "task_outputs"):
|
|
1014
|
-
self._taskoutput = self.pyobject.task_outputs(mark_output)
|
|
673
|
+
self._taskoutput = self.pyobject.task_outputs(self.mark_output)
|
|
1015
674
|
else:
|
|
1016
675
|
self._taskoutput = self.task = self.pyobject
|
|
1017
676
|
|
|
1018
677
|
return self._taskoutput
|
|
1019
678
|
|
|
679
|
+
def mark_output(self, config: "Config"):
|
|
680
|
+
"""Sets a dependency on the job"""
|
|
681
|
+
assert not isinstance(config, Task), "Cannot set a dependency on a task"
|
|
682
|
+
config.__xpm__.task = self.pyobject
|
|
683
|
+
return config
|
|
684
|
+
|
|
1020
685
|
# --- Serialization
|
|
1021
686
|
|
|
1022
687
|
@staticmethod
|
|
@@ -1025,7 +690,7 @@ class ConfigInformation:
|
|
|
1025
690
|
if value is None:
|
|
1026
691
|
return None
|
|
1027
692
|
|
|
1028
|
-
elif isinstance(value, list):
|
|
693
|
+
elif isinstance(value, (list, tuple)):
|
|
1029
694
|
return [ConfigInformation._outputjsonvalue(el, context) for el in value]
|
|
1030
695
|
|
|
1031
696
|
elif isinstance(value, dict):
|
|
@@ -1089,9 +754,6 @@ class ConfigInformation:
|
|
|
1089
754
|
if self.task is not None and self.task is not self:
|
|
1090
755
|
ConfigInformation.__collect_objects__(self.task, objects, context)
|
|
1091
756
|
|
|
1092
|
-
# Serialize pre-tasks
|
|
1093
|
-
ConfigInformation.__collect_objects__(self.pre_tasks, objects, context)
|
|
1094
|
-
|
|
1095
757
|
# Serialize initialization tasks
|
|
1096
758
|
ConfigInformation.__collect_objects__(self.init_tasks, objects, context)
|
|
1097
759
|
|
|
@@ -1105,8 +767,6 @@ class ConfigInformation:
|
|
|
1105
767
|
}
|
|
1106
768
|
|
|
1107
769
|
# Add pre/init tasks
|
|
1108
|
-
if self.pre_tasks:
|
|
1109
|
-
state_dict["pre-tasks"] = [id(pre_task) for pre_task in self.pre_tasks]
|
|
1110
770
|
if self.init_tasks:
|
|
1111
771
|
state_dict["init-tasks"] = [id(init_task) for init_task in self.init_tasks]
|
|
1112
772
|
|
|
@@ -1140,9 +800,12 @@ class ConfigInformation:
|
|
|
1140
800
|
def __collect_objects__(value, objects: List[Dict], context: SerializationContext):
|
|
1141
801
|
"""Serialize all needed configuration objects, looking at sub
|
|
1142
802
|
configurations if necessary"""
|
|
803
|
+
if value is None:
|
|
804
|
+
return
|
|
805
|
+
|
|
1143
806
|
if isinstance(value, Config):
|
|
1144
807
|
value.__xpm__.__get_objects__(objects, context)
|
|
1145
|
-
elif isinstance(value, list):
|
|
808
|
+
elif isinstance(value, (list, tuple)):
|
|
1146
809
|
for el in value:
|
|
1147
810
|
ConfigInformation.__collect_objects__(el, objects, context)
|
|
1148
811
|
elif isinstance(value, dict):
|
|
@@ -1283,34 +946,31 @@ class ConfigInformation:
|
|
|
1283
946
|
|
|
1284
947
|
@overload
|
|
1285
948
|
@staticmethod
|
|
1286
|
-
def fromParameters(
|
|
949
|
+
def fromParameters( # noqa: E704
|
|
1287
950
|
definitions: List[Dict],
|
|
1288
951
|
as_instance=True,
|
|
1289
952
|
save_directory: Optional[Path] = None,
|
|
1290
953
|
discard_id: bool = False,
|
|
1291
|
-
) -> "
|
|
1292
|
-
...
|
|
954
|
+
) -> "ConfigMixin": ...
|
|
1293
955
|
|
|
1294
956
|
@overload
|
|
1295
957
|
@staticmethod
|
|
1296
|
-
def fromParameters(
|
|
958
|
+
def fromParameters( # noqa: E704
|
|
1297
959
|
definitions: List[Dict],
|
|
1298
960
|
as_instance=False,
|
|
1299
961
|
return_tasks=True,
|
|
1300
962
|
save_directory: Optional[Path] = None,
|
|
1301
963
|
discard_id: bool = False,
|
|
1302
|
-
) -> Tuple["Config", List["LightweightTask"]]:
|
|
1303
|
-
...
|
|
964
|
+
) -> Tuple["Config", List["LightweightTask"]]: ...
|
|
1304
965
|
|
|
1305
966
|
@overload
|
|
1306
967
|
@staticmethod
|
|
1307
|
-
def fromParameters(
|
|
968
|
+
def fromParameters( # noqa: E704
|
|
1308
969
|
definitions: List[Dict],
|
|
1309
970
|
as_instance=False,
|
|
1310
971
|
save_directory: Optional[Path] = None,
|
|
1311
972
|
discard_id: bool = False,
|
|
1312
|
-
) -> "Config":
|
|
1313
|
-
...
|
|
973
|
+
) -> "Config": ...
|
|
1314
974
|
|
|
1315
975
|
@staticmethod
|
|
1316
976
|
def load_objects( # noqa: C901
|
|
@@ -1323,6 +983,7 @@ class ConfigInformation:
|
|
|
1323
983
|
o = None
|
|
1324
984
|
objects = {}
|
|
1325
985
|
import experimaestro.taskglobals as taskglobals
|
|
986
|
+
from ..identifier import Identifier
|
|
1326
987
|
|
|
1327
988
|
# Loop over all the definitions and create objects
|
|
1328
989
|
for definition in definitions:
|
|
@@ -1345,7 +1006,9 @@ class ConfigInformation:
|
|
|
1345
1006
|
mod = importlib.import_module(module_name)
|
|
1346
1007
|
except ModuleNotFoundError:
|
|
1347
1008
|
# More hints on the nature of the error
|
|
1348
|
-
logging.warning(
|
|
1009
|
+
logging.warning(
|
|
1010
|
+
"(1) Either the python path is wrong – %s", ":".join(sys.path)
|
|
1011
|
+
)
|
|
1349
1012
|
logging.warning("(2) There is not __init__.py in your module")
|
|
1350
1013
|
raise
|
|
1351
1014
|
|
|
@@ -1432,12 +1095,6 @@ class ConfigInformation:
|
|
|
1432
1095
|
o.__post_init__()
|
|
1433
1096
|
|
|
1434
1097
|
else:
|
|
1435
|
-
# Sets pre-tasks
|
|
1436
|
-
o.__xpm__.pre_tasks = [
|
|
1437
|
-
objects[pre_task_id]
|
|
1438
|
-
for pre_task_id in definition.get("pre-tasks", [])
|
|
1439
|
-
]
|
|
1440
|
-
|
|
1441
1098
|
if task_id := definition.get("task", None):
|
|
1442
1099
|
o.__xpm__.task = objects[task_id]
|
|
1443
1100
|
|
|
@@ -1471,15 +1128,6 @@ class ConfigInformation:
|
|
|
1471
1128
|
|
|
1472
1129
|
# Run pre-task (or returns them)
|
|
1473
1130
|
if as_instance or return_tasks:
|
|
1474
|
-
# Collect pre-tasks (just once)
|
|
1475
|
-
completed_pretasks = set()
|
|
1476
|
-
pre_tasks = []
|
|
1477
|
-
for definition in definitions:
|
|
1478
|
-
for pre_task_id in definition.get("pre-tasks", []):
|
|
1479
|
-
if pre_task_id not in completed_pretasks:
|
|
1480
|
-
completed_pretasks.add(pre_task_id)
|
|
1481
|
-
pre_tasks.append(objects[pre_task_id])
|
|
1482
|
-
|
|
1483
1131
|
# Collect init tasks
|
|
1484
1132
|
init_tasks = []
|
|
1485
1133
|
for init_task_id in definitions[-1].get("init-tasks", []):
|
|
@@ -1487,14 +1135,11 @@ class ConfigInformation:
|
|
|
1487
1135
|
init_tasks.append(init_task)
|
|
1488
1136
|
|
|
1489
1137
|
if as_instance:
|
|
1490
|
-
for pre_task in pre_tasks:
|
|
1491
|
-
logger.info("Executing pre-task %s", type(pre_task))
|
|
1492
|
-
pre_task.execute()
|
|
1493
1138
|
for init_task in init_tasks:
|
|
1494
1139
|
logger.info("Executing init task %s", type(init_task))
|
|
1495
1140
|
init_task.execute()
|
|
1496
1141
|
else:
|
|
1497
|
-
return o,
|
|
1142
|
+
return o, init_tasks
|
|
1498
1143
|
|
|
1499
1144
|
return o
|
|
1500
1145
|
|
|
@@ -1502,7 +1147,6 @@ class ConfigInformation:
|
|
|
1502
1147
|
def __init__(self, context: ConfigWalkContext, *, objects: ObjectStore = None):
|
|
1503
1148
|
super().__init__(context)
|
|
1504
1149
|
self.objects = ObjectStore() if objects is None else objects
|
|
1505
|
-
self.pre_tasks = {}
|
|
1506
1150
|
|
|
1507
1151
|
def preprocess(self, config: "Config"):
|
|
1508
1152
|
if self.objects.is_constructed(id(config)):
|
|
@@ -1529,10 +1173,6 @@ class ConfigInformation:
|
|
|
1529
1173
|
# Call __post_init__
|
|
1530
1174
|
stub.__post_init__()
|
|
1531
1175
|
|
|
1532
|
-
# Gather pre-tasks
|
|
1533
|
-
for pre_task in config.__xpm__.pre_tasks:
|
|
1534
|
-
self.pre_tasks[id(pre_task)] = self.stub(pre_task)
|
|
1535
|
-
|
|
1536
1176
|
self.objects.set_constructed(id(config))
|
|
1537
1177
|
return stub
|
|
1538
1178
|
|
|
@@ -1546,10 +1186,6 @@ class ConfigInformation:
|
|
|
1546
1186
|
processor = ConfigInformation.FromPython(context, objects=objects)
|
|
1547
1187
|
last_object = processor(self.pyobject)
|
|
1548
1188
|
|
|
1549
|
-
# Execute pre-tasks
|
|
1550
|
-
for pre_task in processor.pre_tasks.values():
|
|
1551
|
-
pre_task.execute()
|
|
1552
|
-
|
|
1553
1189
|
return last_object
|
|
1554
1190
|
|
|
1555
1191
|
def add_dependencies(self, *dependencies):
|
|
@@ -1573,6 +1209,9 @@ def clone(v):
|
|
|
1573
1209
|
if isinstance(v, Enum):
|
|
1574
1210
|
return v
|
|
1575
1211
|
|
|
1212
|
+
if isinstance(v, tuple):
|
|
1213
|
+
return tuple(clone(x) for x in v)
|
|
1214
|
+
|
|
1576
1215
|
if isinstance(v, Config):
|
|
1577
1216
|
# Create a new instance
|
|
1578
1217
|
kwargs = {
|
|
@@ -1587,31 +1226,15 @@ def clone(v):
|
|
|
1587
1226
|
raise NotImplementedError("Clone not implemented for type %s" % type(v))
|
|
1588
1227
|
|
|
1589
1228
|
|
|
1590
|
-
|
|
1591
|
-
def __call__(config, *args, **kwargs):
|
|
1592
|
-
import experimaestro.taskglobals as taskglobals
|
|
1593
|
-
|
|
1594
|
-
# Get path and create directory if needed
|
|
1595
|
-
hexid = config.__xpmidentifier__ # type: Identifier
|
|
1596
|
-
typename = config.__xpmtypename__ # type: str
|
|
1597
|
-
dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
|
|
1598
|
-
|
|
1599
|
-
if not dir.exists():
|
|
1600
|
-
dir.mkdir(parents=True, exist_ok=True)
|
|
1601
|
-
|
|
1602
|
-
path = dir / name
|
|
1603
|
-
ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
|
|
1604
|
-
with ipc_lock:
|
|
1605
|
-
r = fn(config, path, *args, **kwargs)
|
|
1606
|
-
return r
|
|
1607
|
-
|
|
1608
|
-
return __call__
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
class TypeConfig:
|
|
1229
|
+
class ConfigMixin:
|
|
1612
1230
|
"""Class for configuration objects"""
|
|
1613
1231
|
|
|
1614
1232
|
__xpmtype__: ObjectType
|
|
1233
|
+
"""The associated XPM type"""
|
|
1234
|
+
|
|
1235
|
+
__xpm__: ConfigInformation
|
|
1236
|
+
"""The __xpm__ object contains all instance specific information about a
|
|
1237
|
+
configuration/task"""
|
|
1615
1238
|
|
|
1616
1239
|
def __init__(self, **kwargs):
|
|
1617
1240
|
"""Initialize the configuration with the given parameters"""
|
|
@@ -1748,29 +1371,7 @@ class TypeConfig:
|
|
|
1748
1371
|
attributes)"""
|
|
1749
1372
|
return clone(self)
|
|
1750
1373
|
|
|
1751
|
-
def
|
|
1752
|
-
assert all(
|
|
1753
|
-
[isinstance(task, LightweightTask) for task in tasks]
|
|
1754
|
-
), "One of the pre-tasks are not lightweight tasks"
|
|
1755
|
-
if self.__xpm__._sealed:
|
|
1756
|
-
raise SealedError("Cannot add pre-tasks to a sealed configuration")
|
|
1757
|
-
self.__xpm__.pre_tasks.extend(tasks)
|
|
1758
|
-
return self
|
|
1759
|
-
|
|
1760
|
-
def add_pretasks_from(self, *configs: "Config"):
|
|
1761
|
-
assert all(
|
|
1762
|
-
[isinstance(config, TypeConfig) for config in configs]
|
|
1763
|
-
), "One of the parameters is not a configuration object"
|
|
1764
|
-
for config in configs:
|
|
1765
|
-
self.add_pretasks(*config.__xpm__.pre_tasks)
|
|
1766
|
-
return self
|
|
1767
|
-
|
|
1768
|
-
@property
|
|
1769
|
-
def pre_tasks(self) -> List["LightweightTask"]:
|
|
1770
|
-
"""Access pre-tasks"""
|
|
1771
|
-
return self.__xpm__.pre_tasks
|
|
1772
|
-
|
|
1773
|
-
def copy_dependencies(self, other: "Config"):
|
|
1374
|
+
def copy_dependencies(self, other: "ConfigMixin"):
|
|
1774
1375
|
"""Add all the dependencies from other configuration"""
|
|
1775
1376
|
|
|
1776
1377
|
# Add task dependency
|
|
@@ -1782,42 +1383,33 @@ class TypeConfig:
|
|
|
1782
1383
|
self.__xpm__.add_dependencies(*other.__xpm__.dependencies)
|
|
1783
1384
|
|
|
1784
1385
|
|
|
1785
|
-
class classproperty(property):
|
|
1786
|
-
def __get__(self, owner_self, owner_cls):
|
|
1787
|
-
return self.fget(owner_cls)
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
1386
|
class Config:
|
|
1791
1387
|
"""Base type for all objects in python interface"""
|
|
1792
1388
|
|
|
1389
|
+
__xpmid__: ClassVar[Optional[str]]
|
|
1390
|
+
"""Optional configuration ID, mostly useful when moving a class to another
|
|
1391
|
+
package to avoid changes in computed task identifiers"""
|
|
1392
|
+
|
|
1793
1393
|
__xpmtype__: ClassVar[ObjectType]
|
|
1794
1394
|
"""The object type holds all the information about a specific subclass
|
|
1795
1395
|
experimaestro metadata"""
|
|
1796
1396
|
|
|
1797
|
-
__xpm__: ConfigInformation
|
|
1798
|
-
"""The __xpm__ object contains all instance specific information about a
|
|
1799
|
-
configuration/task"""
|
|
1800
|
-
|
|
1801
1397
|
@classproperty
|
|
1802
1398
|
def XPMConfig(cls):
|
|
1803
|
-
if issubclass(cls,
|
|
1399
|
+
if issubclass(cls, ConfigMixin):
|
|
1804
1400
|
return cls
|
|
1805
1401
|
return cls.__getxpmtype__().configtype
|
|
1806
1402
|
|
|
1807
|
-
@classproperty
|
|
1808
|
-
def C(cls):
|
|
1809
|
-
return cls.XPMConfig
|
|
1810
|
-
|
|
1811
1403
|
@classproperty
|
|
1812
1404
|
def XPMValue(cls):
|
|
1813
1405
|
"""Returns the value object for this configuration"""
|
|
1814
|
-
if issubclass(cls,
|
|
1406
|
+
if issubclass(cls, ConfigMixin):
|
|
1815
1407
|
return cls.__xpmtype__.objecttype
|
|
1816
1408
|
|
|
1817
1409
|
if value_cls := cls.__dict__.get("__XPMValue__", None):
|
|
1818
1410
|
pass
|
|
1819
1411
|
else:
|
|
1820
|
-
from
|
|
1412
|
+
from ..types import XPMValue
|
|
1821
1413
|
|
|
1822
1414
|
__objectbases__ = tuple(
|
|
1823
1415
|
s.XPMValue
|
|
@@ -1834,9 +1426,20 @@ class Config:
|
|
|
1834
1426
|
|
|
1835
1427
|
return value_cls
|
|
1836
1428
|
|
|
1429
|
+
@classproperty
|
|
1430
|
+
def C(cls):
|
|
1431
|
+
"""Alias for XPMConfig"""
|
|
1432
|
+
return cls.XPMConfig
|
|
1433
|
+
|
|
1434
|
+
@classproperty
|
|
1435
|
+
def V(cls):
|
|
1436
|
+
"""Alias for XPMValue"""
|
|
1437
|
+
return cls.XPMValue
|
|
1438
|
+
|
|
1837
1439
|
@classmethod
|
|
1838
1440
|
def __getxpmtype__(cls) -> "ObjectType":
|
|
1839
|
-
"""Get (and create if necessary) the Object type
|
|
1441
|
+
"""Get (and create if necessary) the Object type associated
|
|
1442
|
+
with thie Config object"""
|
|
1840
1443
|
xpmtype = cls.__dict__.get("__xpmtype__", None)
|
|
1841
1444
|
if xpmtype is None:
|
|
1842
1445
|
from experimaestro.core.types import ObjectType
|
|
@@ -1850,9 +1453,12 @@ class Config:
|
|
|
1850
1453
|
return xpmtype
|
|
1851
1454
|
|
|
1852
1455
|
def __new__(cls: Type[T], *args, **kwargs) -> T:
|
|
1853
|
-
"""Returns an instance of a
|
|
1854
|
-
or C if possible)
|
|
1456
|
+
"""Returns an instance of a ConfigMixin (for compatibility, use XPMConfig
|
|
1457
|
+
or C if possible)
|
|
1855
1458
|
|
|
1459
|
+
:deprecated: Use Config.C or Config.XPMConfig to construct a new
|
|
1460
|
+
configuration, and Config.V (or Config.XPMValue) for a new value
|
|
1461
|
+
"""
|
|
1856
1462
|
# If this is an XPMValue, just return a new instance
|
|
1857
1463
|
from experimaestro.core.types import XPMValue
|
|
1858
1464
|
|
|
@@ -1861,15 +1467,24 @@ class Config:
|
|
|
1861
1467
|
|
|
1862
1468
|
# If this is the XPMConfig, just return a new instance
|
|
1863
1469
|
# __init__ will be called
|
|
1864
|
-
if issubclass(cls,
|
|
1470
|
+
if issubclass(cls, ConfigMixin):
|
|
1865
1471
|
return object.__new__(cls)
|
|
1866
1472
|
|
|
1473
|
+
# Log a deprecation warning for this way of creating a configuration
|
|
1474
|
+
caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
1475
|
+
logger.warning(
|
|
1476
|
+
"Creating a configuration using Config.__new__ is deprecated, and will be removed in a future version. "
|
|
1477
|
+
"Use Config.C or Config.XPMConfig to create a new configuration. "
|
|
1478
|
+
"Issue created at %s:%s",
|
|
1479
|
+
str(Path(caller.filename).absolute()),
|
|
1480
|
+
caller.lineno,
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1867
1483
|
# otherwise, we use the configuration type
|
|
1868
|
-
o:
|
|
1484
|
+
o: ConfigMixin = object.__new__(cls.__getxpmtype__().configtype)
|
|
1869
1485
|
try:
|
|
1870
1486
|
o.__init__(*args, **kwargs)
|
|
1871
1487
|
except Exception:
|
|
1872
|
-
caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
1873
1488
|
logger.error(
|
|
1874
1489
|
"Init error in %s:%s"
|
|
1875
1490
|
% (str(Path(caller.filename).absolute()), caller.lineno)
|
|
@@ -1890,29 +1505,30 @@ class Config:
|
|
|
1890
1505
|
"""Returns a JSON version of the object (if possible)"""
|
|
1891
1506
|
return self.__xpm__.__json__()
|
|
1892
1507
|
|
|
1893
|
-
def __identifier__(self) -> Identifier:
|
|
1508
|
+
def __identifier__(self) -> "Identifier":
|
|
1894
1509
|
return self.__xpm__.identifier
|
|
1895
1510
|
|
|
1896
|
-
def add_pretasks(self, *tasks: "LightweightTask"):
|
|
1897
|
-
"""Add pre-tasks"""
|
|
1898
|
-
raise AssertionError("This method can only be used during configuration")
|
|
1899
|
-
|
|
1900
|
-
def add_pretasks_from(self, *configs: "Config"):
|
|
1901
|
-
"""Add pre-tasks from the listed configurations"""
|
|
1902
|
-
raise AssertionError(
|
|
1903
|
-
"The 'add_pretasks_from' can only be used during configuration"
|
|
1904
|
-
)
|
|
1905
|
-
|
|
1906
1511
|
def copy_dependencies(self, other: "Config"):
|
|
1907
1512
|
"""Add pre-tasks from the listed configurations"""
|
|
1908
1513
|
raise AssertionError(
|
|
1909
1514
|
"The 'copy_dependencies' method can only be used during configuration"
|
|
1910
1515
|
)
|
|
1911
1516
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
"
|
|
1915
|
-
|
|
1517
|
+
def register_task_output(self, method, *args, **kwargs):
|
|
1518
|
+
# Determine the path for this...
|
|
1519
|
+
path = taskglobals.Env.instance().xpm_path / "task-outputs.jsonl"
|
|
1520
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1521
|
+
|
|
1522
|
+
data = json.dumps(
|
|
1523
|
+
{
|
|
1524
|
+
"key": f"{self.__xpmidentifier__}/{method.__name__}",
|
|
1525
|
+
"args": args,
|
|
1526
|
+
"kwargs": kwargs,
|
|
1527
|
+
}
|
|
1528
|
+
)
|
|
1529
|
+
with path.open("at") as fp:
|
|
1530
|
+
fp.writelines([data, "\n"])
|
|
1531
|
+
fp.flush()
|
|
1916
1532
|
|
|
1917
1533
|
|
|
1918
1534
|
class LightweightTask(Config):
|
|
@@ -1931,6 +1547,17 @@ class Task(LightweightTask):
|
|
|
1931
1547
|
def submit(self):
|
|
1932
1548
|
raise AssertionError("This method can only be used during configuration")
|
|
1933
1549
|
|
|
1550
|
+
def watch_output(self, method, callback):
|
|
1551
|
+
"""Sets up a callback
|
|
1552
|
+
|
|
1553
|
+
:param method: a method within a configuration
|
|
1554
|
+
:param callback: the callback
|
|
1555
|
+
"""
|
|
1556
|
+
self.__xpm__.watch_output(method, callback)
|
|
1557
|
+
|
|
1558
|
+
def on_completed(self, callback: Callable[[], None]):
|
|
1559
|
+
self.__xpm__.on_completed(callback)
|
|
1560
|
+
|
|
1934
1561
|
|
|
1935
1562
|
# --- Utility functions
|
|
1936
1563
|
|
|
@@ -1969,3 +1596,24 @@ def setmeta(config: Config, flag: bool):
|
|
|
1969
1596
|
"""Flags the configuration as a meta-parameter"""
|
|
1970
1597
|
config.__xpm__.set_meta(flag)
|
|
1971
1598
|
return config
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def cache(fn, name: str):
|
|
1602
|
+
def __call__(config, *args, **kwargs):
|
|
1603
|
+
import experimaestro.taskglobals as taskglobals
|
|
1604
|
+
|
|
1605
|
+
# Get path and create directory if needed
|
|
1606
|
+
hexid = config.__xpmidentifier__ # type: Identifier
|
|
1607
|
+
typename = config.__xpmtypename__ # type: str
|
|
1608
|
+
dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
|
|
1609
|
+
|
|
1610
|
+
if not dir.exists():
|
|
1611
|
+
dir.mkdir(parents=True, exist_ok=True)
|
|
1612
|
+
|
|
1613
|
+
path = dir / name
|
|
1614
|
+
ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
|
|
1615
|
+
with ipc_lock:
|
|
1616
|
+
r = fn(config, path, *args, **kwargs)
|
|
1617
|
+
return r
|
|
1618
|
+
|
|
1619
|
+
return __call__
|