experimaestro 1.5.1__py3-none-any.whl → 2.0.0a8__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/__init__.py +14 -4
- experimaestro/__main__.py +3 -423
- experimaestro/annotations.py +14 -4
- experimaestro/cli/__init__.py +311 -0
- experimaestro/{filter.py → cli/filter.py} +23 -9
- experimaestro/cli/jobs.py +268 -0
- experimaestro/cli/progress.py +269 -0
- experimaestro/click.py +0 -35
- experimaestro/commandline.py +3 -7
- experimaestro/connectors/__init__.py +29 -14
- experimaestro/connectors/local.py +19 -10
- experimaestro/connectors/ssh.py +27 -8
- experimaestro/core/arguments.py +45 -3
- experimaestro/core/callbacks.py +52 -0
- experimaestro/core/context.py +8 -9
- experimaestro/core/identifier.py +310 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +399 -772
- 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 +104 -66
- experimaestro/experiments/cli.py +154 -72
- experimaestro/experiments/configuration.py +10 -1
- experimaestro/generators.py +6 -1
- experimaestro/ipc.py +4 -1
- experimaestro/launcherfinder/__init__.py +1 -1
- experimaestro/launcherfinder/base.py +2 -18
- experimaestro/launcherfinder/parser.py +8 -3
- experimaestro/launcherfinder/registry.py +52 -140
- experimaestro/launcherfinder/specs.py +49 -10
- experimaestro/launchers/direct.py +0 -47
- experimaestro/launchers/slurm/base.py +54 -14
- experimaestro/mkdocs/__init__.py +1 -1
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/notifications.py +38 -12
- experimaestro/progress.py +406 -0
- experimaestro/run.py +24 -3
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +108 -808
- experimaestro/scheduler/dynamic_outputs.py +184 -0
- experimaestro/scheduler/experiment.py +387 -0
- experimaestro/scheduler/jobs.py +475 -0
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +75 -0
- experimaestro/scheduler/workspace.py +27 -8
- experimaestro/scriptbuilder.py +18 -3
- experimaestro/server/__init__.py +36 -5
- experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
- experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
- experimaestro/server/data/2582b0e4bcf85eceead0.ttf +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/index.css +5187 -5068
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +68887 -68064
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +45 -5
- 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 +17 -5
- experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
- experimaestro/tests/restart.py +10 -5
- experimaestro/tests/tasks/all.py +23 -10
- experimaestro/tests/tasks/foreign.py +2 -4
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_experiment.py +73 -0
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_findlauncher.py +12 -5
- experimaestro/tests/test_forward.py +5 -5
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +182 -158
- experimaestro/tests/test_instance.py +19 -27
- experimaestro/tests/test_objects.py +13 -20
- experimaestro/tests/test_outputs.py +6 -6
- experimaestro/tests/test_param.py +68 -30
- experimaestro/tests/test_progress.py +4 -4
- experimaestro/tests/test_serializers.py +24 -64
- experimaestro/tests/test_ssh.py +7 -0
- experimaestro/tests/test_tags.py +50 -21
- experimaestro/tests/test_tasks.py +42 -51
- experimaestro/tests/test_tokens.py +11 -8
- experimaestro/tests/test_types.py +24 -21
- experimaestro/tests/test_validation.py +67 -110
- experimaestro/tests/token_reschedule.py +1 -1
- experimaestro/tokens.py +24 -13
- experimaestro/tools/diff.py +8 -1
- experimaestro/typingutils.py +20 -11
- experimaestro/utils/asyncio.py +6 -2
- experimaestro/utils/multiprocessing.py +44 -0
- experimaestro/utils/resources.py +11 -3
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/METADATA +28 -36
- experimaestro-2.0.0a8.dist-info/RECORD +166 -0
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/WHEEL +1 -1
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/entry_points.txt +0 -4
- experimaestro/launchers/slurm/cli.py +0 -29
- experimaestro/launchers/slurm/configuration.py +0 -597
- experimaestro/scheduler/environment.py +0 -94
- experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
- experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
- experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
- experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
- experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
- experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
- experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
- experimaestro/utils/yaml.py +0 -202
- experimaestro-1.5.1.dist-info/RECORD +0 -148
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,34 +1,28 @@
|
|
|
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,
|
|
30
25
|
Tuple,
|
|
31
|
-
Type,
|
|
32
26
|
TypeVar,
|
|
33
27
|
Union,
|
|
34
28
|
overload,
|
|
@@ -37,301 +31,30 @@ from typing import (
|
|
|
37
31
|
import sys
|
|
38
32
|
import experimaestro
|
|
39
33
|
from experimaestro.utils import logger
|
|
40
|
-
from
|
|
41
|
-
from
|
|
42
|
-
from .context import SerializationContext, SerializedPath, SerializedPathLoader
|
|
34
|
+
from experimaestro.core.types import DeprecatedAttribute, ObjectType, TypeVarType
|
|
35
|
+
from ..context import SerializationContext, SerializedPath, SerializedPathLoader
|
|
43
36
|
|
|
44
37
|
if TYPE_CHECKING:
|
|
38
|
+
from ..callbacks import TaskEventListener
|
|
39
|
+
from ..identifier import Identifier
|
|
45
40
|
from experimaestro.scheduler.base import Job
|
|
46
41
|
from experimaestro.scheduler.workspace import RunMode
|
|
47
42
|
from experimaestro.launchers import Launcher
|
|
48
43
|
from experimaestro.scheduler import Workspace
|
|
49
44
|
|
|
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
|
|
45
|
+
from .config_walk import ConfigWalk, ConfigWalkContext
|
|
46
|
+
from .config_utils import (
|
|
47
|
+
getqualattr,
|
|
48
|
+
add_to_path,
|
|
49
|
+
TaggedValue,
|
|
50
|
+
ObjectStore,
|
|
51
|
+
classproperty,
|
|
52
|
+
)
|
|
325
53
|
|
|
326
|
-
|
|
54
|
+
T = TypeVar("T", bound="Config")
|
|
327
55
|
|
|
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
56
|
|
|
334
|
-
|
|
57
|
+
DependentMarker = Callable[["Config"], None]
|
|
335
58
|
|
|
336
59
|
|
|
337
60
|
def updatedependencies(
|
|
@@ -362,197 +85,61 @@ def updatedependencies(
|
|
|
362
85
|
raise NotImplementedError("update dependencies for type %s" % type(value))
|
|
363
86
|
|
|
364
87
|
|
|
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
88
|
NOT_SET = object()
|
|
423
89
|
|
|
424
90
|
|
|
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)
|
|
494
|
-
|
|
495
|
-
if info.init_tasks:
|
|
496
|
-
with self.map("__init_tasks__"):
|
|
497
|
-
self(info.init_tasks)
|
|
498
|
-
|
|
499
|
-
# Process task if different
|
|
500
|
-
if (
|
|
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)
|
|
506
|
-
|
|
507
|
-
processed = self.postprocess(stub, x, result)
|
|
508
|
-
self.visited[xid] = processed
|
|
509
|
-
return processed
|
|
510
|
-
|
|
511
|
-
if isinstance(x, list):
|
|
512
|
-
result = []
|
|
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
|
|
91
|
+
@define()
|
|
92
|
+
class WatchedOutput:
|
|
93
|
+
#: The enclosing job
|
|
94
|
+
job: "Job"
|
|
528
95
|
|
|
529
|
-
|
|
96
|
+
#: The configuration containing the watched output
|
|
97
|
+
config: "ConfigInformation"
|
|
530
98
|
|
|
99
|
+
#: The watched output (name)
|
|
100
|
+
method_name: str
|
|
531
101
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
cls = module
|
|
535
|
-
for part in qualname.split("."):
|
|
536
|
-
cls = getattr(cls, part)
|
|
537
|
-
return cls
|
|
102
|
+
#: The watched output method (called with the JSON event)
|
|
103
|
+
method: Callable
|
|
538
104
|
|
|
105
|
+
#: The callback to call (with the output of the previous method)
|
|
106
|
+
callback: Callable
|
|
539
107
|
|
|
540
|
-
class ObjectStore:
|
|
541
|
-
def __init__(self):
|
|
542
|
-
self.store: Dict[int, Any] = {}
|
|
543
|
-
self.constructed: Set[int] = set()
|
|
544
108
|
|
|
545
|
-
|
|
546
|
-
|
|
109
|
+
def get_generated_paths(
|
|
110
|
+
v: Union["ConfigMixin", list, dict],
|
|
111
|
+
path: list[str] | None = None,
|
|
112
|
+
paths: list[str] | None = None,
|
|
113
|
+
) -> list[str]:
|
|
114
|
+
"""Get the list of generated paths, useful to track down those
|
|
547
115
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
116
|
+
:param path: The current path
|
|
117
|
+
:param paths: The list of generated paths so far, defaults to None
|
|
118
|
+
:return: The full list of generated paths
|
|
119
|
+
"""
|
|
120
|
+
paths = [] if paths is None else paths
|
|
121
|
+
path = [] if path is None else path
|
|
553
122
|
|
|
554
|
-
|
|
555
|
-
|
|
123
|
+
if isinstance(v, list):
|
|
124
|
+
for ix, element in enumerate(v):
|
|
125
|
+
get_generated_paths(element, path + [f"[{ix}]"], paths)
|
|
126
|
+
|
|
127
|
+
elif isinstance(v, dict):
|
|
128
|
+
for key, element in v.items():
|
|
129
|
+
get_generated_paths(element, path + [f"[{key}]"], paths)
|
|
130
|
+
|
|
131
|
+
elif isinstance(v, ConfigMixin):
|
|
132
|
+
for key in v.__xpm__._generated_values:
|
|
133
|
+
value = v.__xpm__.values[key]
|
|
134
|
+
if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
|
|
135
|
+
path.append(key)
|
|
136
|
+
get_generated_paths(value, path, paths)
|
|
137
|
+
path.pop()
|
|
138
|
+
else:
|
|
139
|
+
paths.append(".".join(path + [key]))
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(f"Cannot handle type {type(v)}")
|
|
142
|
+
return paths
|
|
556
143
|
|
|
557
144
|
|
|
558
145
|
class ConfigInformation:
|
|
@@ -564,14 +151,14 @@ class ConfigInformation:
|
|
|
564
151
|
# Set to true when loading from JSON
|
|
565
152
|
LOADING: ClassVar[bool] = False
|
|
566
153
|
|
|
567
|
-
def __init__(self, pyobject: "
|
|
154
|
+
def __init__(self, pyobject: "ConfigMixin"):
|
|
568
155
|
# The underlying pyobject and XPM type
|
|
569
156
|
self.pyobject = pyobject
|
|
570
|
-
self.xpmtype = pyobject.__xpmtype__
|
|
157
|
+
self.xpmtype: "ObjectType" = pyobject.__xpmtype__
|
|
571
158
|
self.values = {}
|
|
572
159
|
|
|
573
160
|
# Meta-informations
|
|
574
|
-
self._tags = {}
|
|
161
|
+
self._tags: dict[str, Any] = {}
|
|
575
162
|
self._initinfo = ""
|
|
576
163
|
|
|
577
164
|
self._taskoutput = None
|
|
@@ -582,28 +169,38 @@ class ConfigInformation:
|
|
|
582
169
|
|
|
583
170
|
# State information
|
|
584
171
|
self.job = None
|
|
172
|
+
self._job_listener: "TaskEventListener" | None = None
|
|
585
173
|
|
|
586
|
-
|
|
174
|
+
#: True when this configuration was loaded from disk
|
|
175
|
+
self.loaded = False
|
|
176
|
+
|
|
177
|
+
# Explicitly added dependencies
|
|
587
178
|
self.dependencies = []
|
|
588
179
|
|
|
589
|
-
#
|
|
590
|
-
|
|
180
|
+
# Concrete type variables resolutions
|
|
181
|
+
# This is used to check typevars coherence
|
|
182
|
+
self.concrete_typevars: Dict[TypeVar, type] = {}
|
|
591
183
|
|
|
592
184
|
# Initialization tasks
|
|
593
185
|
self.init_tasks: List["LightweightTask"] = []
|
|
594
186
|
|
|
595
|
-
#
|
|
187
|
+
# Watched outputs
|
|
188
|
+
self.watched_outputs: List[WatchedOutput] = []
|
|
596
189
|
|
|
597
|
-
|
|
598
|
-
"""The full identifier (with pre-tasks)"""
|
|
190
|
+
# Cached information
|
|
599
191
|
|
|
600
|
-
self.
|
|
601
|
-
"""The identifier
|
|
192
|
+
self._identifier = None
|
|
193
|
+
"""The configuration identifier (cached when sealed)"""
|
|
602
194
|
|
|
603
195
|
self._validated = False
|
|
604
196
|
self._sealed = False
|
|
605
197
|
self._meta = None
|
|
606
198
|
|
|
199
|
+
# This contains the list of generated values (using context) in this
|
|
200
|
+
# configuration or any sub-configuration, is generated. This prevents
|
|
201
|
+
# problem when a configuration with generated values is re-used.
|
|
202
|
+
self._generated_values = []
|
|
203
|
+
|
|
607
204
|
def set_meta(self, value: Optional[bool]):
|
|
608
205
|
"""Sets the meta flag"""
|
|
609
206
|
assert not self._sealed, "Configuration is sealed"
|
|
@@ -621,7 +218,34 @@ class ConfigInformation:
|
|
|
621
218
|
# Not an argument, bypass
|
|
622
219
|
return object.__getattribute__(self.pyobject, name)
|
|
623
220
|
|
|
221
|
+
@staticmethod
|
|
222
|
+
def is_generated_value(argument, value):
|
|
223
|
+
if argument.ignore_generated:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
if value is None:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
if isinstance(value, (int, str, float, bool, Enum, Path)):
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
if isinstance(value, ConfigMixin):
|
|
233
|
+
return value.__xpm__._generated_values and value.__xpm__.task is None
|
|
234
|
+
|
|
235
|
+
if isinstance(value, list):
|
|
236
|
+
return any(ConfigInformation.is_generated_value(argument, x) for x in value)
|
|
237
|
+
|
|
238
|
+
if isinstance(value, dict):
|
|
239
|
+
return any(
|
|
240
|
+
ConfigInformation.is_generated_value(argument, x)
|
|
241
|
+
for x in value.values()
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return False
|
|
245
|
+
|
|
624
246
|
def set(self, k, v, bypass=False):
|
|
247
|
+
from experimaestro.generators import Generator
|
|
248
|
+
|
|
625
249
|
# Not an argument, bypass
|
|
626
250
|
if k not in self.xpmtype.arguments:
|
|
627
251
|
setattr(self.pyobject, k, v)
|
|
@@ -630,13 +254,34 @@ class ConfigInformation:
|
|
|
630
254
|
if self._sealed and not bypass:
|
|
631
255
|
raise AttributeError(f"Object is read-only (trying to set {k})")
|
|
632
256
|
|
|
257
|
+
if not isinstance(v, ConfigMixin) and isinstance(v, Config):
|
|
258
|
+
raise AttributeError(
|
|
259
|
+
"Configuration (and not objects) should be used. Consider using .C(...)"
|
|
260
|
+
)
|
|
261
|
+
|
|
633
262
|
try:
|
|
634
263
|
argument = self.xpmtype.arguments.get(k, None)
|
|
635
264
|
if argument:
|
|
636
|
-
if
|
|
265
|
+
if ConfigInformation.is_generated_value(argument, v):
|
|
266
|
+
raise AttributeError(
|
|
267
|
+
f"Cannot set {k} to a configuration with generated values. "
|
|
268
|
+
"Here is the list of paths to help you: "
|
|
269
|
+
f"""{', '.join(get_generated_paths(v, [k]))}"""
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if not bypass and (
|
|
273
|
+
(isinstance(argument.generator, Generator)) or argument.constant
|
|
274
|
+
):
|
|
637
275
|
raise AttributeError("Property %s is read-only" % (k))
|
|
638
276
|
if v is not None:
|
|
639
277
|
self.values[k] = argument.validate(v)
|
|
278
|
+
# Check for type variables
|
|
279
|
+
if type(argument.type) is TypeVarType:
|
|
280
|
+
self.check_typevar(argument.type.typevar, type(v))
|
|
281
|
+
if isinstance(v, Config):
|
|
282
|
+
# If the value is a Config, fuse type variables
|
|
283
|
+
v.__xpm__.fuse_concrete_typevars(self.concrete_typevars)
|
|
284
|
+
self.fuse_concrete_typevars(v.__xpm__.concrete_typevars)
|
|
640
285
|
elif argument.required:
|
|
641
286
|
raise AttributeError("Cannot set required attribute to None")
|
|
642
287
|
else:
|
|
@@ -649,6 +294,43 @@ class ConfigInformation:
|
|
|
649
294
|
logger.error("Error while setting value %s in %s", k, self.xpmtype)
|
|
650
295
|
raise
|
|
651
296
|
|
|
297
|
+
def fuse_concrete_typevars(self, typevars: Dict[TypeVar, type]):
|
|
298
|
+
"""Fuses concrete type variables with the current ones"""
|
|
299
|
+
for typevar, v in typevars.items():
|
|
300
|
+
self.check_typevar(typevar, v)
|
|
301
|
+
|
|
302
|
+
def check_typevar(self, typevar: TypeVar, v: type):
|
|
303
|
+
"""Check if a type variable is coherent with the current typevars bindings,
|
|
304
|
+
updates the bindings if necessary"""
|
|
305
|
+
if typevar not in self.concrete_typevars:
|
|
306
|
+
self.concrete_typevars[typevar] = v
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
concrete_typevar = self.concrete_typevars[typevar]
|
|
310
|
+
bound = typevar.__bound__
|
|
311
|
+
# Check that v is a subclass of the typevar OR that typevar is a subclass of v
|
|
312
|
+
# Then set the concrete type variable to the most generic type
|
|
313
|
+
|
|
314
|
+
# First, limiting to the specified bound
|
|
315
|
+
if bound is not None:
|
|
316
|
+
if not issubclass(v, bound):
|
|
317
|
+
raise TypeError(
|
|
318
|
+
f"Type variable {typevar} is bound to {bound}, but tried to set it to {v}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if issubclass(v, concrete_typevar):
|
|
322
|
+
# v is a subclass of the typevar, keep the typevar
|
|
323
|
+
return
|
|
324
|
+
if issubclass(concrete_typevar, v):
|
|
325
|
+
# typevar is a subclass of v, keep v
|
|
326
|
+
self.concrete_typevars[typevar] = v
|
|
327
|
+
return
|
|
328
|
+
raise TypeError(
|
|
329
|
+
f"Type variable {typevar} is already set to {self.concrete_typevars[typevar]}, "
|
|
330
|
+
f"but tried to set it to {v}"
|
|
331
|
+
f" (current typevars bindings: {self.concrete_typevars})"
|
|
332
|
+
)
|
|
333
|
+
|
|
652
334
|
def addtag(self, name, value):
|
|
653
335
|
self._tags[name] = value
|
|
654
336
|
|
|
@@ -688,10 +370,6 @@ class ConfigInformation:
|
|
|
688
370
|
% (k, self.xpmtype, self._initinfo)
|
|
689
371
|
)
|
|
690
372
|
|
|
691
|
-
# Validate pre-tasks
|
|
692
|
-
for pre_task in self.pre_tasks:
|
|
693
|
-
pre_task.__xpm__.validate()
|
|
694
|
-
|
|
695
373
|
# Validate init tasks
|
|
696
374
|
for init_task in self.init_tasks:
|
|
697
375
|
init_task.__xpm__.validate()
|
|
@@ -712,18 +390,68 @@ class ConfigInformation:
|
|
|
712
390
|
Arguments:
|
|
713
391
|
- context: the generation context
|
|
714
392
|
"""
|
|
393
|
+
if generated_keys := [
|
|
394
|
+
k
|
|
395
|
+
for k, v in self.values.items()
|
|
396
|
+
if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
|
|
397
|
+
]:
|
|
398
|
+
raise AttributeError(
|
|
399
|
+
"Cannot seal a configuration with generated values:"
|
|
400
|
+
f"""{",".join(generated_keys)} in {context.currentpath}"""
|
|
401
|
+
)
|
|
715
402
|
|
|
716
403
|
class Sealer(ConfigWalk):
|
|
717
|
-
def preprocess(self, config:
|
|
404
|
+
def preprocess(self, config: ConfigMixin):
|
|
718
405
|
return not config.__xpm__._sealed, config
|
|
719
406
|
|
|
720
|
-
def postprocess(self, stub, config:
|
|
407
|
+
def postprocess(self, stub, config: ConfigMixin, values):
|
|
721
408
|
# Generate values
|
|
409
|
+
from experimaestro.generators import Generator
|
|
410
|
+
|
|
722
411
|
for k, argument in config.__xpmtype__.arguments.items():
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
412
|
+
try:
|
|
413
|
+
if argument.generator:
|
|
414
|
+
if not isinstance(argument.generator, Generator):
|
|
415
|
+
# Don't set if already set
|
|
416
|
+
if config.__xpm__.values.get(k) is not None:
|
|
417
|
+
continue
|
|
418
|
+
value = argument.generator()
|
|
419
|
+
else:
|
|
420
|
+
# Generate a value
|
|
421
|
+
sig = inspect.signature(argument.generator)
|
|
422
|
+
if len(sig.parameters) == 0:
|
|
423
|
+
value = argument.generator()
|
|
424
|
+
elif len(sig.parameters) == 2:
|
|
425
|
+
# Only in that case do we need to flag this configuration
|
|
426
|
+
# as containing generated values
|
|
427
|
+
if not argument.ignore_generated:
|
|
428
|
+
config.__xpm__._generated_values.append(k)
|
|
429
|
+
else:
|
|
430
|
+
logging.warning("Ignoring %s", k)
|
|
431
|
+
value = argument.generator(self.context, config)
|
|
432
|
+
else:
|
|
433
|
+
assert (
|
|
434
|
+
False
|
|
435
|
+
), "generator has either two parameters (context and config), or none"
|
|
436
|
+
config.__xpm__.set(k, value, bypass=True)
|
|
437
|
+
else:
|
|
438
|
+
value = config.__xpm__.values.get(k)
|
|
439
|
+
except Exception:
|
|
440
|
+
logger.error(
|
|
441
|
+
"While setting %s of %s", argument.name, config.__xpmtype__
|
|
726
442
|
)
|
|
443
|
+
raise
|
|
444
|
+
|
|
445
|
+
# Propagate the generated value flag
|
|
446
|
+
if (
|
|
447
|
+
value is not None
|
|
448
|
+
and isinstance(value, ConfigMixin)
|
|
449
|
+
and value.__xpm__._generated_values
|
|
450
|
+
):
|
|
451
|
+
if not argument.ignore_generated:
|
|
452
|
+
config.__xpm__._generated_values.append(k)
|
|
453
|
+
else:
|
|
454
|
+
logging.warning("Ignoring %s", k)
|
|
727
455
|
|
|
728
456
|
config.__xpm__._sealed = True
|
|
729
457
|
|
|
@@ -737,89 +465,29 @@ class ConfigInformation:
|
|
|
737
465
|
context = ConfigWalkContext()
|
|
738
466
|
|
|
739
467
|
class Unsealer(ConfigWalk):
|
|
740
|
-
def preprocess(self, config:
|
|
468
|
+
def preprocess(self, config: ConfigMixin):
|
|
741
469
|
return config.__xpm__._sealed, config
|
|
742
470
|
|
|
743
|
-
def postprocess(self, stub, config:
|
|
471
|
+
def postprocess(self, stub, config: ConfigMixin, values):
|
|
744
472
|
config.__xpm__._sealed = False
|
|
745
473
|
config.__xpm__._identifier = None
|
|
746
474
|
|
|
747
475
|
Unsealer(context, recurse_task=True)(self.pyobject)
|
|
748
476
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
pre_tasks: Dict[int, "Config"] = {}
|
|
752
|
-
|
|
753
|
-
class PreTaskCollect(ConfigWalk):
|
|
754
|
-
def preprocess(self, config: Config):
|
|
755
|
-
# Do not cross tasks
|
|
756
|
-
return not isinstance(config.__xpm__, Task), config
|
|
757
|
-
|
|
758
|
-
def postprocess(self, stub, config: Config, values):
|
|
759
|
-
pre_tasks.update(
|
|
760
|
-
{id(pre_task): pre_task for pre_task in config.__xpm__.pre_tasks}
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
PreTaskCollect(context, recurse_task=True)(self.pyobject)
|
|
764
|
-
return pre_tasks.values()
|
|
765
|
-
|
|
766
|
-
def identifiers(self, only_raw: bool):
|
|
477
|
+
@property
|
|
478
|
+
def identifier(self):
|
|
767
479
|
"""Computes the unique identifier"""
|
|
768
|
-
|
|
769
|
-
raw_identifier = self._raw_identifier
|
|
770
|
-
full_identifier = self._full_identifier
|
|
480
|
+
from ..identifier import IdentifierComputer
|
|
771
481
|
|
|
772
482
|
# Computes raw identifier if needed
|
|
773
|
-
if
|
|
774
|
-
|
|
775
|
-
raw_identifier = HashComputer.compute(self.pyobject)
|
|
776
|
-
if self._sealed:
|
|
777
|
-
self._raw_identifier = raw_identifier
|
|
778
|
-
|
|
779
|
-
if only_raw:
|
|
780
|
-
return raw_identifier, full_identifier
|
|
781
|
-
|
|
782
|
-
# OK, let's compute the full identifier
|
|
783
|
-
if full_identifier is None or not self._sealed:
|
|
784
|
-
# Compute the full identifier by including the pre-tasks
|
|
785
|
-
hasher = hashlib.sha256()
|
|
786
|
-
hasher.update(raw_identifier.all)
|
|
787
|
-
pre_tasks_ids = [
|
|
788
|
-
pre_task.__xpm__.raw_identifier.all
|
|
789
|
-
for pre_task in self.collect_pre_tasks()
|
|
790
|
-
]
|
|
791
|
-
for task_id in sorted(pre_tasks_ids):
|
|
792
|
-
hasher.update(task_id)
|
|
793
|
-
|
|
794
|
-
# Adds init tasks
|
|
795
|
-
if self.init_tasks:
|
|
796
|
-
hasher.update(HashComputer.INIT_TASKS)
|
|
797
|
-
for init_task in self.init_tasks:
|
|
798
|
-
hasher.update(init_task.__xpm__.raw_identifier.all)
|
|
483
|
+
if self._identifier is not None:
|
|
484
|
+
return self._identifier
|
|
799
485
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
self._full_identifier = full_identifier
|
|
806
|
-
|
|
807
|
-
return raw_identifier, full_identifier
|
|
808
|
-
|
|
809
|
-
@property
|
|
810
|
-
def raw_identifier(self) -> Identifier:
|
|
811
|
-
"""Computes the unique identifier (without task modifiers)"""
|
|
812
|
-
raw_identifier, _ = self.identifiers(True)
|
|
813
|
-
return raw_identifier
|
|
814
|
-
|
|
815
|
-
@property
|
|
816
|
-
def full_identifier(self) -> Identifier:
|
|
817
|
-
"""Computes the unique identifier (with task modifiers)"""
|
|
818
|
-
_, full_identifier = self.identifiers(False)
|
|
819
|
-
return full_identifier
|
|
820
|
-
|
|
821
|
-
identifier = full_identifier
|
|
822
|
-
"""Deprecated: use full_identifier"""
|
|
486
|
+
# Get the main identifier
|
|
487
|
+
identifier = IdentifierComputer.compute(self.pyobject)
|
|
488
|
+
if self._sealed:
|
|
489
|
+
self._identifier = identifier
|
|
490
|
+
return identifier
|
|
823
491
|
|
|
824
492
|
def dependency(self):
|
|
825
493
|
"""Returns a dependency"""
|
|
@@ -834,20 +502,14 @@ class ConfigInformation:
|
|
|
834
502
|
path: List[str],
|
|
835
503
|
taskids: Set[int],
|
|
836
504
|
):
|
|
837
|
-
# Add pre-tasks
|
|
838
|
-
for pre_task in self.pre_tasks:
|
|
839
|
-
pre_task.__xpm__.updatedependencies(
|
|
840
|
-
dependencies, path + ["__pre_tasks__"], taskids
|
|
841
|
-
)
|
|
842
|
-
|
|
843
505
|
# Add initialization tasks
|
|
844
506
|
for init_task in self.init_tasks:
|
|
845
507
|
init_task.__xpm__.updatedependencies(
|
|
846
508
|
dependencies, path + ["__init_tasks__"], taskids
|
|
847
509
|
)
|
|
848
510
|
|
|
849
|
-
# Check for an associated task
|
|
850
|
-
if self.task:
|
|
511
|
+
# Check for an associated task (and not loaded)
|
|
512
|
+
if self.task and not self.loaded:
|
|
851
513
|
if id(self.task) not in taskids:
|
|
852
514
|
taskids.add(id(self.task))
|
|
853
515
|
dependencies.add(self.task.__xpm__.dependency())
|
|
@@ -896,6 +558,28 @@ class ConfigInformation:
|
|
|
896
558
|
# Now, seal the object
|
|
897
559
|
self.seal(context)
|
|
898
560
|
|
|
561
|
+
def watch_output(self, method, callback):
|
|
562
|
+
"""Watch the task output linked with a given method
|
|
563
|
+
|
|
564
|
+
:param method: The method to watch
|
|
565
|
+
:param callback: The callback
|
|
566
|
+
"""
|
|
567
|
+
watched = WatchedOutput(
|
|
568
|
+
self, method.__self__, method.__name__, method, callback
|
|
569
|
+
)
|
|
570
|
+
self.watched_outputs.append(watched)
|
|
571
|
+
if self.job:
|
|
572
|
+
self.job.watch_output(watched)
|
|
573
|
+
|
|
574
|
+
def on_completed(self, callback: Callable[[], None]):
|
|
575
|
+
"""Call a method when the task is completed successfully
|
|
576
|
+
|
|
577
|
+
:param callback: _description_
|
|
578
|
+
"""
|
|
579
|
+
from ..callbacks import TaskEventListener
|
|
580
|
+
|
|
581
|
+
TaskEventListener.on_completed(self, callback)
|
|
582
|
+
|
|
899
583
|
def submit(
|
|
900
584
|
self,
|
|
901
585
|
workspace: "Workspace",
|
|
@@ -906,6 +590,7 @@ class ConfigInformation:
|
|
|
906
590
|
):
|
|
907
591
|
from experimaestro.scheduler import experiment, JobContext
|
|
908
592
|
from experimaestro.scheduler.workspace import RunMode
|
|
593
|
+
from ..callbacks import TaskEventListener
|
|
909
594
|
|
|
910
595
|
# --- Prepare the object
|
|
911
596
|
|
|
@@ -916,14 +601,14 @@ class ConfigInformation:
|
|
|
916
601
|
|
|
917
602
|
# --- Submit the job
|
|
918
603
|
|
|
604
|
+
# Sets the init tasks
|
|
605
|
+
self.init_tasks = init_tasks
|
|
606
|
+
|
|
919
607
|
# Creates a new job
|
|
920
608
|
self.job = self.xpmtype.task(
|
|
921
609
|
self.pyobject, launcher=launcher, workspace=workspace, run_mode=run_mode
|
|
922
610
|
)
|
|
923
611
|
|
|
924
|
-
# Sets the init tasks
|
|
925
|
-
self.init_tasks = init_tasks
|
|
926
|
-
|
|
927
612
|
# Validate the object
|
|
928
613
|
job_context = JobContext(self.job)
|
|
929
614
|
self.validate_and_seal(job_context)
|
|
@@ -958,10 +643,12 @@ class ConfigInformation:
|
|
|
958
643
|
workspace.run_mode if run_mode is None else run_mode
|
|
959
644
|
) or RunMode.NORMAL
|
|
960
645
|
if run_mode == RunMode.NORMAL:
|
|
646
|
+
TaskEventListener.connect(experiment.CURRENT)
|
|
647
|
+
experiment.CURRENT.submit(self.job)
|
|
961
648
|
other = experiment.CURRENT.submit(self.job)
|
|
962
649
|
if other:
|
|
963
|
-
#
|
|
964
|
-
|
|
650
|
+
# Our job = previously submitted job
|
|
651
|
+
self.job = other
|
|
965
652
|
else:
|
|
966
653
|
# Show a warning
|
|
967
654
|
if run_mode == RunMode.GENERATE_ONLY:
|
|
@@ -979,6 +666,9 @@ class ConfigInformation:
|
|
|
979
666
|
elif self.job.failedpath.is_file():
|
|
980
667
|
color = "light_red"
|
|
981
668
|
cprint(f"[failed] {s}", color, file=sys.stderr)
|
|
669
|
+
elif self.job.pidpath.is_file():
|
|
670
|
+
color = "blue"
|
|
671
|
+
cprint(f"[running] {s}", color, file=sys.stderr)
|
|
982
672
|
else:
|
|
983
673
|
color = "light_blue"
|
|
984
674
|
cprint(f"[not run] {s}", color, file=sys.stderr)
|
|
@@ -994,23 +684,25 @@ class ConfigInformation:
|
|
|
994
684
|
|
|
995
685
|
print(file=sys.stderr) # noqa: T201
|
|
996
686
|
|
|
997
|
-
# Handle an output configuration
|
|
998
|
-
def mark_output(config: "Config"):
|
|
999
|
-
"""Sets a dependency on the job"""
|
|
1000
|
-
assert not isinstance(config, Task), "Cannot set a dependency on a task"
|
|
1001
|
-
config.__xpm__.task = self.pyobject
|
|
1002
|
-
return config
|
|
1003
|
-
|
|
1004
687
|
# Mark this configuration also
|
|
1005
688
|
self.task = self.pyobject
|
|
1006
689
|
|
|
1007
690
|
if hasattr(self.pyobject, "task_outputs"):
|
|
1008
|
-
self._taskoutput = self.pyobject.task_outputs(mark_output)
|
|
691
|
+
self._taskoutput = self.pyobject.task_outputs(self.mark_output)
|
|
1009
692
|
else:
|
|
1010
693
|
self._taskoutput = self.task = self.pyobject
|
|
1011
694
|
|
|
1012
695
|
return self._taskoutput
|
|
1013
696
|
|
|
697
|
+
def mark_output(self, config: "Config"):
|
|
698
|
+
"""Sets a dependency on the job"""
|
|
699
|
+
assert not isinstance(config, Task), "Cannot set a dependency on a task"
|
|
700
|
+
assert isinstance(
|
|
701
|
+
config, ConfigMixin
|
|
702
|
+
), "Only configurations can be marked as dependent on a task"
|
|
703
|
+
config.__xpm__.task = self.pyobject
|
|
704
|
+
return config
|
|
705
|
+
|
|
1014
706
|
# --- Serialization
|
|
1015
707
|
|
|
1016
708
|
@staticmethod
|
|
@@ -1019,7 +711,7 @@ class ConfigInformation:
|
|
|
1019
711
|
if value is None:
|
|
1020
712
|
return None
|
|
1021
713
|
|
|
1022
|
-
elif isinstance(value, list):
|
|
714
|
+
elif isinstance(value, (list, tuple)):
|
|
1023
715
|
return [ConfigInformation._outputjsonvalue(el, context) for el in value]
|
|
1024
716
|
|
|
1025
717
|
elif isinstance(value, dict):
|
|
@@ -1083,9 +775,6 @@ class ConfigInformation:
|
|
|
1083
775
|
if self.task is not None and self.task is not self:
|
|
1084
776
|
ConfigInformation.__collect_objects__(self.task, objects, context)
|
|
1085
777
|
|
|
1086
|
-
# Serialize pre-tasks
|
|
1087
|
-
ConfigInformation.__collect_objects__(self.pre_tasks, objects, context)
|
|
1088
|
-
|
|
1089
778
|
# Serialize initialization tasks
|
|
1090
779
|
ConfigInformation.__collect_objects__(self.init_tasks, objects, context)
|
|
1091
780
|
|
|
@@ -1093,14 +782,12 @@ class ConfigInformation:
|
|
|
1093
782
|
state_dict = {
|
|
1094
783
|
"id": id(self.pyobject),
|
|
1095
784
|
"module": self.xpmtype._module,
|
|
1096
|
-
"type": self.xpmtype.
|
|
785
|
+
"type": self.xpmtype.value_type.__qualname__,
|
|
1097
786
|
"typename": self.xpmtype.name(),
|
|
1098
787
|
"identifier": self.identifier.state_dict(),
|
|
1099
788
|
}
|
|
1100
789
|
|
|
1101
790
|
# Add pre/init tasks
|
|
1102
|
-
if self.pre_tasks:
|
|
1103
|
-
state_dict["pre-tasks"] = [id(pre_task) for pre_task in self.pre_tasks]
|
|
1104
791
|
if self.init_tasks:
|
|
1105
792
|
state_dict["init-tasks"] = [id(init_task) for init_task in self.init_tasks]
|
|
1106
793
|
|
|
@@ -1134,9 +821,12 @@ class ConfigInformation:
|
|
|
1134
821
|
def __collect_objects__(value, objects: List[Dict], context: SerializationContext):
|
|
1135
822
|
"""Serialize all needed configuration objects, looking at sub
|
|
1136
823
|
configurations if necessary"""
|
|
824
|
+
if value is None:
|
|
825
|
+
return
|
|
826
|
+
|
|
1137
827
|
if isinstance(value, Config):
|
|
1138
828
|
value.__xpm__.__get_objects__(objects, context)
|
|
1139
|
-
elif isinstance(value, list):
|
|
829
|
+
elif isinstance(value, (list, tuple)):
|
|
1140
830
|
for el in value:
|
|
1141
831
|
ConfigInformation.__collect_objects__(el, objects, context)
|
|
1142
832
|
elif isinstance(value, dict):
|
|
@@ -1183,6 +873,7 @@ class ConfigInformation:
|
|
|
1183
873
|
"workspace": str(context.workspace.path.absolute()),
|
|
1184
874
|
"tags": {key: value for key, value in self.tags().items()},
|
|
1185
875
|
"version": 2,
|
|
876
|
+
"experimaestro": experimaestro.__version__,
|
|
1186
877
|
"objects": self.__get_objects__([], context),
|
|
1187
878
|
},
|
|
1188
879
|
out,
|
|
@@ -1277,34 +968,31 @@ class ConfigInformation:
|
|
|
1277
968
|
|
|
1278
969
|
@overload
|
|
1279
970
|
@staticmethod
|
|
1280
|
-
def fromParameters(
|
|
971
|
+
def fromParameters( # noqa: E704
|
|
1281
972
|
definitions: List[Dict],
|
|
1282
973
|
as_instance=True,
|
|
1283
974
|
save_directory: Optional[Path] = None,
|
|
1284
975
|
discard_id: bool = False,
|
|
1285
|
-
) -> "
|
|
1286
|
-
...
|
|
976
|
+
) -> "ConfigMixin": ...
|
|
1287
977
|
|
|
1288
978
|
@overload
|
|
1289
979
|
@staticmethod
|
|
1290
|
-
def fromParameters(
|
|
980
|
+
def fromParameters( # noqa: E704
|
|
1291
981
|
definitions: List[Dict],
|
|
1292
982
|
as_instance=False,
|
|
1293
983
|
return_tasks=True,
|
|
1294
984
|
save_directory: Optional[Path] = None,
|
|
1295
985
|
discard_id: bool = False,
|
|
1296
|
-
) -> Tuple["Config", List["LightweightTask"]]:
|
|
1297
|
-
...
|
|
986
|
+
) -> Tuple["Config", List["LightweightTask"]]: ...
|
|
1298
987
|
|
|
1299
988
|
@overload
|
|
1300
989
|
@staticmethod
|
|
1301
|
-
def fromParameters(
|
|
990
|
+
def fromParameters( # noqa: E704
|
|
1302
991
|
definitions: List[Dict],
|
|
1303
992
|
as_instance=False,
|
|
1304
993
|
save_directory: Optional[Path] = None,
|
|
1305
994
|
discard_id: bool = False,
|
|
1306
|
-
) -> "Config":
|
|
1307
|
-
...
|
|
995
|
+
) -> "Config": ...
|
|
1308
996
|
|
|
1309
997
|
@staticmethod
|
|
1310
998
|
def load_objects( # noqa: C901
|
|
@@ -1317,6 +1005,7 @@ class ConfigInformation:
|
|
|
1317
1005
|
o = None
|
|
1318
1006
|
objects = {}
|
|
1319
1007
|
import experimaestro.taskglobals as taskglobals
|
|
1008
|
+
from ..identifier import Identifier
|
|
1320
1009
|
|
|
1321
1010
|
# Loop over all the definitions and create objects
|
|
1322
1011
|
for definition in definitions:
|
|
@@ -1334,14 +1023,22 @@ class ConfigInformation:
|
|
|
1334
1023
|
sys.modules[module_name] = mod
|
|
1335
1024
|
spec.loader.exec_module(mod)
|
|
1336
1025
|
else:
|
|
1337
|
-
|
|
1338
|
-
|
|
1026
|
+
try:
|
|
1027
|
+
logger.debug("Importing module %s", definition["module"])
|
|
1028
|
+
mod = importlib.import_module(module_name)
|
|
1029
|
+
except ModuleNotFoundError:
|
|
1030
|
+
# More hints on the nature of the error
|
|
1031
|
+
logging.warning(
|
|
1032
|
+
"(1) Either the python path is wrong – %s", ":".join(sys.path)
|
|
1033
|
+
)
|
|
1034
|
+
logging.warning("(2) There is not __init__.py in your module")
|
|
1035
|
+
raise
|
|
1339
1036
|
|
|
1340
1037
|
cls = getqualattr(mod, definition["type"])
|
|
1341
1038
|
|
|
1342
1039
|
# Creates an object (or a config)
|
|
1343
1040
|
if as_instance:
|
|
1344
|
-
o = cls.
|
|
1041
|
+
o = cls.__new__(cls)
|
|
1345
1042
|
else:
|
|
1346
1043
|
o = cls.XPMConfig.__new__(cls.XPMConfig)
|
|
1347
1044
|
assert definition["id"] not in objects, "Duplicate id %s" % definition["id"]
|
|
@@ -1382,12 +1079,13 @@ class ConfigInformation:
|
|
|
1382
1079
|
else:
|
|
1383
1080
|
o.__init__()
|
|
1384
1081
|
xpminfo = o.__xpm__ # type: ConfigInformation
|
|
1082
|
+
xpminfo.loaded = True
|
|
1385
1083
|
|
|
1386
1084
|
meta = definition.get("meta", None)
|
|
1387
1085
|
if meta:
|
|
1388
1086
|
xpminfo._meta = meta
|
|
1389
1087
|
if xpminfo.xpmtype.task is not None:
|
|
1390
|
-
|
|
1088
|
+
xpminfo.job = object()
|
|
1391
1089
|
|
|
1392
1090
|
# Set the fields
|
|
1393
1091
|
for name, value in definition["fields"].items():
|
|
@@ -1419,12 +1117,6 @@ class ConfigInformation:
|
|
|
1419
1117
|
o.__post_init__()
|
|
1420
1118
|
|
|
1421
1119
|
else:
|
|
1422
|
-
# Sets pre-tasks
|
|
1423
|
-
o.__xpm__.pre_tasks = [
|
|
1424
|
-
objects[pre_task_id]
|
|
1425
|
-
for pre_task_id in definition.get("pre-tasks", [])
|
|
1426
|
-
]
|
|
1427
|
-
|
|
1428
1120
|
if task_id := definition.get("task", None):
|
|
1429
1121
|
o.__xpm__.task = objects[task_id]
|
|
1430
1122
|
|
|
@@ -1458,15 +1150,6 @@ class ConfigInformation:
|
|
|
1458
1150
|
|
|
1459
1151
|
# Run pre-task (or returns them)
|
|
1460
1152
|
if as_instance or return_tasks:
|
|
1461
|
-
# Collect pre-tasks (just once)
|
|
1462
|
-
completed_pretasks = set()
|
|
1463
|
-
pre_tasks = []
|
|
1464
|
-
for definition in definitions:
|
|
1465
|
-
for pre_task_id in definition.get("pre-tasks", []):
|
|
1466
|
-
if pre_task_id not in completed_pretasks:
|
|
1467
|
-
completed_pretasks.add(pre_task_id)
|
|
1468
|
-
pre_tasks.append(objects[pre_task_id])
|
|
1469
|
-
|
|
1470
1153
|
# Collect init tasks
|
|
1471
1154
|
init_tasks = []
|
|
1472
1155
|
for init_task_id in definitions[-1].get("init-tasks", []):
|
|
@@ -1474,14 +1157,11 @@ class ConfigInformation:
|
|
|
1474
1157
|
init_tasks.append(init_task)
|
|
1475
1158
|
|
|
1476
1159
|
if as_instance:
|
|
1477
|
-
for pre_task in pre_tasks:
|
|
1478
|
-
logger.info("Executing pre-task %s", type(pre_task))
|
|
1479
|
-
pre_task.execute()
|
|
1480
1160
|
for init_task in init_tasks:
|
|
1481
1161
|
logger.info("Executing init task %s", type(init_task))
|
|
1482
1162
|
init_task.execute()
|
|
1483
1163
|
else:
|
|
1484
|
-
return o,
|
|
1164
|
+
return o, init_tasks
|
|
1485
1165
|
|
|
1486
1166
|
return o
|
|
1487
1167
|
|
|
@@ -1489,7 +1169,6 @@ class ConfigInformation:
|
|
|
1489
1169
|
def __init__(self, context: ConfigWalkContext, *, objects: ObjectStore = None):
|
|
1490
1170
|
super().__init__(context)
|
|
1491
1171
|
self.objects = ObjectStore() if objects is None else objects
|
|
1492
|
-
self.pre_tasks = {}
|
|
1493
1172
|
|
|
1494
1173
|
def preprocess(self, config: "Config"):
|
|
1495
1174
|
if self.objects.is_constructed(id(config)):
|
|
@@ -1501,7 +1180,7 @@ class ConfigInformation:
|
|
|
1501
1180
|
|
|
1502
1181
|
if o is None:
|
|
1503
1182
|
# Creates an object (and not a config)
|
|
1504
|
-
o = config.
|
|
1183
|
+
o = config.__xpmtype__.value_type()
|
|
1505
1184
|
|
|
1506
1185
|
# Store in cache
|
|
1507
1186
|
self.objects.add_stub(id(config), o)
|
|
@@ -1516,10 +1195,6 @@ class ConfigInformation:
|
|
|
1516
1195
|
# Call __post_init__
|
|
1517
1196
|
stub.__post_init__()
|
|
1518
1197
|
|
|
1519
|
-
# Gather pre-tasks
|
|
1520
|
-
for pre_task in config.__xpm__.pre_tasks:
|
|
1521
|
-
self.pre_tasks[id(pre_task)] = self.stub(pre_task)
|
|
1522
|
-
|
|
1523
1198
|
self.objects.set_constructed(id(config))
|
|
1524
1199
|
return stub
|
|
1525
1200
|
|
|
@@ -1533,10 +1208,6 @@ class ConfigInformation:
|
|
|
1533
1208
|
processor = ConfigInformation.FromPython(context, objects=objects)
|
|
1534
1209
|
last_object = processor(self.pyobject)
|
|
1535
1210
|
|
|
1536
|
-
# Execute pre-tasks
|
|
1537
|
-
for pre_task in processor.pre_tasks.values():
|
|
1538
|
-
pre_task.execute()
|
|
1539
|
-
|
|
1540
1211
|
return last_object
|
|
1541
1212
|
|
|
1542
1213
|
def add_dependencies(self, *dependencies):
|
|
@@ -1560,6 +1231,9 @@ def clone(v):
|
|
|
1560
1231
|
if isinstance(v, Enum):
|
|
1561
1232
|
return v
|
|
1562
1233
|
|
|
1234
|
+
if isinstance(v, tuple):
|
|
1235
|
+
return tuple(clone(x) for x in v)
|
|
1236
|
+
|
|
1563
1237
|
if isinstance(v, Config):
|
|
1564
1238
|
# Create a new instance
|
|
1565
1239
|
kwargs = {
|
|
@@ -1574,31 +1248,15 @@ def clone(v):
|
|
|
1574
1248
|
raise NotImplementedError("Clone not implemented for type %s" % type(v))
|
|
1575
1249
|
|
|
1576
1250
|
|
|
1577
|
-
|
|
1578
|
-
def __call__(config, *args, **kwargs):
|
|
1579
|
-
import experimaestro.taskglobals as taskglobals
|
|
1580
|
-
|
|
1581
|
-
# Get path and create directory if needed
|
|
1582
|
-
hexid = config.__xpmidentifier__ # type: Identifier
|
|
1583
|
-
typename = config.__xpmtypename__ # type: str
|
|
1584
|
-
dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
|
|
1585
|
-
|
|
1586
|
-
if not dir.exists():
|
|
1587
|
-
dir.mkdir(parents=True, exist_ok=True)
|
|
1588
|
-
|
|
1589
|
-
path = dir / name
|
|
1590
|
-
ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
|
|
1591
|
-
with ipc_lock:
|
|
1592
|
-
r = fn(config, path, *args, **kwargs)
|
|
1593
|
-
return r
|
|
1594
|
-
|
|
1595
|
-
return __call__
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
class TypeConfig:
|
|
1251
|
+
class ConfigMixin:
|
|
1599
1252
|
"""Class for configuration objects"""
|
|
1600
1253
|
|
|
1601
1254
|
__xpmtype__: ObjectType
|
|
1255
|
+
"""The associated XPM type"""
|
|
1256
|
+
|
|
1257
|
+
__xpm__: ConfigInformation
|
|
1258
|
+
"""The __xpm__ object contains all instance specific information about a
|
|
1259
|
+
configuration/task"""
|
|
1602
1260
|
|
|
1603
1261
|
def __init__(self, **kwargs):
|
|
1604
1262
|
"""Initialize the configuration with the given parameters"""
|
|
@@ -1649,8 +1307,8 @@ class TypeConfig:
|
|
|
1649
1307
|
[f"{key}={value}" for key, value in self.__xpm__.values.items()]
|
|
1650
1308
|
)
|
|
1651
1309
|
return (
|
|
1652
|
-
f"{self.__xpmtype__.
|
|
1653
|
-
f"{self.__xpmtype__.
|
|
1310
|
+
f"{self.__xpmtype__.value_type.__module__}."
|
|
1311
|
+
f"{self.__xpmtype__.value_type.__qualname__}({params})"
|
|
1654
1312
|
)
|
|
1655
1313
|
|
|
1656
1314
|
def tag(self, name, value):
|
|
@@ -1679,9 +1337,20 @@ class TypeConfig:
|
|
|
1679
1337
|
return self
|
|
1680
1338
|
|
|
1681
1339
|
def instance(
|
|
1682
|
-
self,
|
|
1340
|
+
self,
|
|
1341
|
+
context: ConfigWalkContext = None,
|
|
1342
|
+
*,
|
|
1343
|
+
objects: ObjectStore = None,
|
|
1344
|
+
keep: bool = True,
|
|
1683
1345
|
) -> T:
|
|
1684
|
-
"""Return an instance with the current values
|
|
1346
|
+
"""Return an instance with the current values
|
|
1347
|
+
|
|
1348
|
+
:param context: The context when computing the instance
|
|
1349
|
+
:param objects: The previously built objects (so that we avoid
|
|
1350
|
+
re-creating instances of past configurations)
|
|
1351
|
+
:param keep: register a configuration in the __config__ field of the
|
|
1352
|
+
instance
|
|
1353
|
+
"""
|
|
1685
1354
|
if context is None:
|
|
1686
1355
|
from experimaestro.xpmutils import EmptyContext
|
|
1687
1356
|
|
|
@@ -1690,7 +1359,11 @@ class TypeConfig:
|
|
|
1690
1359
|
assert isinstance(
|
|
1691
1360
|
context, ConfigWalkContext
|
|
1692
1361
|
), f"{context.__class__} is not an instance of ConfigWalkContext"
|
|
1693
|
-
|
|
1362
|
+
|
|
1363
|
+
instance = self.__xpm__.fromConfig(context, objects=objects) # type: ignore
|
|
1364
|
+
if keep:
|
|
1365
|
+
object.__setattr__(instance, "__config__", self)
|
|
1366
|
+
return instance
|
|
1694
1367
|
|
|
1695
1368
|
def submit(
|
|
1696
1369
|
self,
|
|
@@ -1735,29 +1408,7 @@ class TypeConfig:
|
|
|
1735
1408
|
attributes)"""
|
|
1736
1409
|
return clone(self)
|
|
1737
1410
|
|
|
1738
|
-
def
|
|
1739
|
-
assert all(
|
|
1740
|
-
[isinstance(task, LightweightTask) for task in tasks]
|
|
1741
|
-
), "One of the pre-tasks are not lightweight tasks"
|
|
1742
|
-
if self.__xpm__._sealed:
|
|
1743
|
-
raise SealedError("Cannot add pre-tasks to a sealed configuration")
|
|
1744
|
-
self.__xpm__.pre_tasks.extend(tasks)
|
|
1745
|
-
return self
|
|
1746
|
-
|
|
1747
|
-
def add_pretasks_from(self, *configs: "Config"):
|
|
1748
|
-
assert all(
|
|
1749
|
-
[isinstance(config, TypeConfig) for config in configs]
|
|
1750
|
-
), "One of the parameters is not a configuration object"
|
|
1751
|
-
for config in configs:
|
|
1752
|
-
self.add_pretasks(*config.__xpm__.pre_tasks)
|
|
1753
|
-
return self
|
|
1754
|
-
|
|
1755
|
-
@property
|
|
1756
|
-
def pre_tasks(self) -> List["LightweightTask"]:
|
|
1757
|
-
"""Access pre-tasks"""
|
|
1758
|
-
return self.__xpm__.pre_tasks
|
|
1759
|
-
|
|
1760
|
-
def copy_dependencies(self, other: "Config"):
|
|
1411
|
+
def copy_dependencies(self, other: "ConfigMixin"):
|
|
1761
1412
|
"""Add all the dependencies from other configuration"""
|
|
1762
1413
|
|
|
1763
1414
|
# Add task dependency
|
|
@@ -1769,61 +1420,32 @@ class TypeConfig:
|
|
|
1769
1420
|
self.__xpm__.add_dependencies(*other.__xpm__.dependencies)
|
|
1770
1421
|
|
|
1771
1422
|
|
|
1772
|
-
class classproperty(property):
|
|
1773
|
-
def __get__(self, owner_self, owner_cls):
|
|
1774
|
-
return self.fget(owner_cls)
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
1423
|
class Config:
|
|
1778
1424
|
"""Base type for all objects in python interface"""
|
|
1779
1425
|
|
|
1426
|
+
__xpmid__: ClassVar[Optional[str]]
|
|
1427
|
+
"""Optional configuration ID, mostly useful when moving a class to another
|
|
1428
|
+
package to avoid changes in computed task identifiers"""
|
|
1429
|
+
|
|
1780
1430
|
__xpmtype__: ClassVar[ObjectType]
|
|
1781
1431
|
"""The object type holds all the information about a specific subclass
|
|
1782
1432
|
experimaestro metadata"""
|
|
1783
1433
|
|
|
1784
|
-
__xpm__: ConfigInformation
|
|
1785
|
-
"""The __xpm__ object contains all instance specific information about a
|
|
1786
|
-
configuration/task"""
|
|
1787
|
-
|
|
1788
1434
|
@classproperty
|
|
1789
1435
|
def XPMConfig(cls):
|
|
1790
|
-
if issubclass(cls,
|
|
1436
|
+
if issubclass(cls, ConfigMixin):
|
|
1791
1437
|
return cls
|
|
1792
|
-
return cls.__getxpmtype__().
|
|
1438
|
+
return cls.__getxpmtype__().config_type
|
|
1793
1439
|
|
|
1794
1440
|
@classproperty
|
|
1795
1441
|
def C(cls):
|
|
1442
|
+
"""Alias for XPMConfig"""
|
|
1796
1443
|
return cls.XPMConfig
|
|
1797
1444
|
|
|
1798
|
-
@classproperty
|
|
1799
|
-
def XPMValue(cls):
|
|
1800
|
-
"""Returns the value object for this configuration"""
|
|
1801
|
-
if issubclass(cls, TypeConfig):
|
|
1802
|
-
return cls.__xpmtype__.objecttype
|
|
1803
|
-
|
|
1804
|
-
if value_cls := cls.__dict__.get("__XPMValue__", None):
|
|
1805
|
-
pass
|
|
1806
|
-
else:
|
|
1807
|
-
from .types import XPMValue
|
|
1808
|
-
|
|
1809
|
-
__objectbases__ = tuple(
|
|
1810
|
-
s.XPMValue
|
|
1811
|
-
for s in cls.__bases__
|
|
1812
|
-
if issubclass(s, Config) and (s is not Config)
|
|
1813
|
-
) or (XPMValue,)
|
|
1814
|
-
|
|
1815
|
-
*tp_qual, tp_name = cls.__qualname__.split(".")
|
|
1816
|
-
value_cls = type(f"{tp_name}.XPMValue", (cls,) + __objectbases__, {})
|
|
1817
|
-
value_cls.__qualname__ = ".".join(tp_qual + [value_cls.__name__])
|
|
1818
|
-
value_cls.__module__ = cls.__module__
|
|
1819
|
-
|
|
1820
|
-
setattr(cls, "__XPMValue__", value_cls)
|
|
1821
|
-
|
|
1822
|
-
return value_cls
|
|
1823
|
-
|
|
1824
1445
|
@classmethod
|
|
1825
1446
|
def __getxpmtype__(cls) -> "ObjectType":
|
|
1826
|
-
"""Get (and create if necessary) the Object type
|
|
1447
|
+
"""Get (and create if necessary) the Object type associated
|
|
1448
|
+
with thie Config object"""
|
|
1827
1449
|
xpmtype = cls.__dict__.get("__xpmtype__", None)
|
|
1828
1450
|
if xpmtype is None:
|
|
1829
1451
|
from experimaestro.core.types import ObjectType
|
|
@@ -1836,34 +1458,6 @@ class Config:
|
|
|
1836
1458
|
raise
|
|
1837
1459
|
return xpmtype
|
|
1838
1460
|
|
|
1839
|
-
def __new__(cls: Type[T], *args, **kwargs) -> T:
|
|
1840
|
-
"""Returns an instance of a TypeConfig (for compatibility, use XPMConfig
|
|
1841
|
-
or C if possible)"""
|
|
1842
|
-
|
|
1843
|
-
# If this is an XPMValue, just return a new instance
|
|
1844
|
-
from experimaestro.core.types import XPMValue
|
|
1845
|
-
|
|
1846
|
-
if issubclass(cls, XPMValue):
|
|
1847
|
-
return object.__new__(cls)
|
|
1848
|
-
|
|
1849
|
-
# If this is the XPMConfig, just return a new instance
|
|
1850
|
-
# __init__ will be called
|
|
1851
|
-
if issubclass(cls, TypeConfig):
|
|
1852
|
-
return object.__new__(cls)
|
|
1853
|
-
|
|
1854
|
-
# otherwise, we use the configuration type
|
|
1855
|
-
o: TypeConfig = object.__new__(cls.__getxpmtype__().configtype)
|
|
1856
|
-
try:
|
|
1857
|
-
o.__init__(*args, **kwargs)
|
|
1858
|
-
except Exception:
|
|
1859
|
-
caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
1860
|
-
logger.error(
|
|
1861
|
-
"Init error in %s:%s"
|
|
1862
|
-
% (str(Path(caller.filename).absolute()), caller.lineno)
|
|
1863
|
-
)
|
|
1864
|
-
raise
|
|
1865
|
-
return o
|
|
1866
|
-
|
|
1867
1461
|
def __validate__(self):
|
|
1868
1462
|
"""Validate the values"""
|
|
1869
1463
|
pass
|
|
@@ -1877,29 +1471,30 @@ class Config:
|
|
|
1877
1471
|
"""Returns a JSON version of the object (if possible)"""
|
|
1878
1472
|
return self.__xpm__.__json__()
|
|
1879
1473
|
|
|
1880
|
-
def __identifier__(self) -> Identifier:
|
|
1474
|
+
def __identifier__(self) -> "Identifier":
|
|
1881
1475
|
return self.__xpm__.identifier
|
|
1882
1476
|
|
|
1883
|
-
def add_pretasks(self, *tasks: "LightweightTask"):
|
|
1884
|
-
"""Add pre-tasks"""
|
|
1885
|
-
raise AssertionError("This method can only be used during configuration")
|
|
1886
|
-
|
|
1887
|
-
def add_pretasks_from(self, *configs: "Config"):
|
|
1888
|
-
"""Add pre-tasks from the listed configurations"""
|
|
1889
|
-
raise AssertionError(
|
|
1890
|
-
"The 'add_pretasks_from' can only be used during configuration"
|
|
1891
|
-
)
|
|
1892
|
-
|
|
1893
1477
|
def copy_dependencies(self, other: "Config"):
|
|
1894
1478
|
"""Add pre-tasks from the listed configurations"""
|
|
1895
1479
|
raise AssertionError(
|
|
1896
1480
|
"The 'copy_dependencies' method can only be used during configuration"
|
|
1897
1481
|
)
|
|
1898
1482
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
"
|
|
1902
|
-
|
|
1483
|
+
def register_task_output(self, method, *args, **kwargs):
|
|
1484
|
+
# Determine the path for this...
|
|
1485
|
+
path = taskglobals.Env.instance().xpm_path / "task-outputs.jsonl"
|
|
1486
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1487
|
+
|
|
1488
|
+
data = json.dumps(
|
|
1489
|
+
{
|
|
1490
|
+
"key": f"{self.__xpmidentifier__}/{method.__name__}",
|
|
1491
|
+
"args": args,
|
|
1492
|
+
"kwargs": kwargs,
|
|
1493
|
+
}
|
|
1494
|
+
)
|
|
1495
|
+
with path.open("at") as fp:
|
|
1496
|
+
fp.writelines([data, "\n"])
|
|
1497
|
+
fp.flush()
|
|
1903
1498
|
|
|
1904
1499
|
|
|
1905
1500
|
class LightweightTask(Config):
|
|
@@ -1918,6 +1513,17 @@ class Task(LightweightTask):
|
|
|
1918
1513
|
def submit(self):
|
|
1919
1514
|
raise AssertionError("This method can only be used during configuration")
|
|
1920
1515
|
|
|
1516
|
+
def watch_output(self, method, callback):
|
|
1517
|
+
"""Sets up a callback
|
|
1518
|
+
|
|
1519
|
+
:param method: a method within a configuration
|
|
1520
|
+
:param callback: the callback
|
|
1521
|
+
"""
|
|
1522
|
+
self.__xpm__.watch_output(method, callback)
|
|
1523
|
+
|
|
1524
|
+
def on_completed(self, callback: Callable[[], None]):
|
|
1525
|
+
self.__xpm__.on_completed(callback)
|
|
1526
|
+
|
|
1921
1527
|
|
|
1922
1528
|
# --- Utility functions
|
|
1923
1529
|
|
|
@@ -1956,3 +1562,24 @@ def setmeta(config: Config, flag: bool):
|
|
|
1956
1562
|
"""Flags the configuration as a meta-parameter"""
|
|
1957
1563
|
config.__xpm__.set_meta(flag)
|
|
1958
1564
|
return config
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def cache(fn, name: str):
|
|
1568
|
+
def __call__(config, *args, **kwargs):
|
|
1569
|
+
import experimaestro.taskglobals as taskglobals
|
|
1570
|
+
|
|
1571
|
+
# Get path and create directory if needed
|
|
1572
|
+
hexid = config.__xpmidentifier__ # type: Identifier
|
|
1573
|
+
typename = config.__xpmtypename__ # type: str
|
|
1574
|
+
dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
|
|
1575
|
+
|
|
1576
|
+
if not dir.exists():
|
|
1577
|
+
dir.mkdir(parents=True, exist_ok=True)
|
|
1578
|
+
|
|
1579
|
+
path = dir / name
|
|
1580
|
+
ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
|
|
1581
|
+
with ipc_lock:
|
|
1582
|
+
r = fn(config, path, *args, **kwargs)
|
|
1583
|
+
return r
|
|
1584
|
+
|
|
1585
|
+
return __call__
|