experimaestro 2.0.0a8__py3-none-any.whl → 2.0.0b4__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 +10 -11
- experimaestro/annotations.py +167 -206
- experimaestro/cli/__init__.py +130 -5
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +20 -1
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +182 -46
- experimaestro/core/identifier.py +107 -6
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +542 -25
- experimaestro/core/objects/config_walk.py +20 -0
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +175 -38
- experimaestro/exceptions.py +26 -0
- experimaestro/experiments/cli.py +107 -25
- experimaestro/generators.py +50 -9
- experimaestro/huggingface.py +3 -1
- experimaestro/launcherfinder/parser.py +29 -0
- experimaestro/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +63 -13
- experimaestro/progress.py +0 -2
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/base.py +489 -125
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +225 -30
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +216 -206
- experimaestro/scheduler/services.py +186 -12
- experimaestro/scheduler/state_db.py +388 -0
- experimaestro/scheduler/state_provider.py +2345 -0
- experimaestro/scheduler/state_sync.py +834 -0
- experimaestro/scheduler/workspace.py +52 -10
- experimaestro/scriptbuilder.py +7 -0
- experimaestro/server/__init__.py +147 -57
- experimaestro/server/data/index.css +0 -125
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +194 -58
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +44 -5
- experimaestro/sphinx/__init__.py +3 -3
- experimaestro/taskglobals.py +20 -0
- experimaestro/tests/conftest.py +80 -0
- experimaestro/tests/core/test_generics.py +2 -2
- experimaestro/tests/identifier_stability.json +45 -0
- experimaestro/tests/launchers/bin/sacct +6 -2
- experimaestro/tests/launchers/bin/sbatch +4 -2
- experimaestro/tests/launchers/test_slurm.py +80 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_file_progress_integration.py +1 -1
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_identifier.py +372 -41
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +3 -3
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +312 -5
- experimaestro/tests/test_outputs.py +2 -2
- experimaestro/tests/test_param.py +8 -12
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +0 -48
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -1
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +136 -0
- experimaestro/tests/test_tasks.py +107 -121
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +17 -13
- experimaestro/tests/test_types.py +123 -1
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +4 -2
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +1 -1
- experimaestro/tui/__init__.py +8 -0
- experimaestro/tui/app.py +2303 -0
- experimaestro/tui/app.tcss +353 -0
- experimaestro/tui/log_viewer.py +228 -0
- experimaestro/utils/__init__.py +23 -0
- experimaestro/utils/environment.py +148 -0
- experimaestro/utils/git.py +129 -0
- experimaestro/utils/resources.py +1 -1
- experimaestro/version.py +34 -0
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +68 -38
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
- experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
- experimaestro/compat.py +0 -6
- experimaestro/core/objects.pyi +0 -221
- experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
- experimaestro/server/data/219aa9140e099e6c72ed.woff2 +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/c380809fd3677d7d6903.woff2 +0 -0
- experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
- experimaestro-2.0.0a8.dist-info/RECORD +0 -166
- experimaestro-2.0.0a8.dist-info/entry_points.txt +0 -17
- {experimaestro-2.0.0a8.dist-info → experimaestro-2.0.0b4.dist-info}/licenses/LICENSE +0 -0
experimaestro/core/arguments.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""Management of the arguments (params, options, etc) associated with the XPM objects"""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
3
4
|
from typing import Optional, TypeVar, TYPE_CHECKING, Callable, Any
|
|
4
5
|
from experimaestro.typingutils import get_optional
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
import
|
|
7
|
+
from typing import Annotated
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
|
-
from typing_extensions import Annotated
|
|
10
10
|
import experimaestro.core.types
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
from experimaestro.core.subparameters import ParameterGroup
|
|
12
|
+
|
|
13
|
+
# Track deprecation warnings per module (max 10 per module)
|
|
14
|
+
_deprecation_warning_counts: dict[str, int] = {}
|
|
15
|
+
_MAX_WARNINGS_PER_MODULE = 10
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class Argument:
|
|
@@ -29,10 +29,12 @@ class Argument:
|
|
|
29
29
|
help=None,
|
|
30
30
|
generator=None,
|
|
31
31
|
ignored=None,
|
|
32
|
-
|
|
32
|
+
field_or_default=None,
|
|
33
33
|
checker=None,
|
|
34
34
|
constant=False,
|
|
35
35
|
is_data=False,
|
|
36
|
+
overrides=False,
|
|
37
|
+
groups: set["ParameterGroup"] = None,
|
|
36
38
|
):
|
|
37
39
|
"""Creates a new argument
|
|
38
40
|
|
|
@@ -52,7 +54,8 @@ class Argument:
|
|
|
52
54
|
ignored (bool, optional): True if ignored (if None, computed
|
|
53
55
|
automatically). Defaults to None.
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
field_or_default (any | field, optional): Default value or field
|
|
58
|
+
object containing default information. Defaults to None.
|
|
56
59
|
|
|
57
60
|
checker (any, optional): Value checker. Defaults to None.
|
|
58
61
|
|
|
@@ -61,9 +64,17 @@ class Argument:
|
|
|
61
64
|
|
|
62
65
|
is_data (bool, optional): Flag for paths that are data path (to be
|
|
63
66
|
serialized). Defaults to False.
|
|
67
|
+
|
|
68
|
+
overrides (bool, optional): If True, this argument intentionally
|
|
69
|
+
overrides a parent argument. Suppresses the warning that would
|
|
70
|
+
otherwise be issued. Defaults to False.
|
|
71
|
+
|
|
72
|
+
groups (set[ParameterGroup], optional): Set of groups this parameter
|
|
73
|
+
belongs to. Used with subparameters to compute partial identifiers.
|
|
74
|
+
Defaults to None (empty set).
|
|
64
75
|
"""
|
|
65
|
-
required = (
|
|
66
|
-
if
|
|
76
|
+
required = (field_or_default is None) if required is None else required
|
|
77
|
+
if field_or_default is not None and required is not None and required:
|
|
67
78
|
raise Exception(
|
|
68
79
|
"argument '%s' is required but default value is given" % name
|
|
69
80
|
)
|
|
@@ -77,22 +88,42 @@ class Argument:
|
|
|
77
88
|
self.required = required
|
|
78
89
|
self.objecttype = None
|
|
79
90
|
self.is_data = is_data
|
|
91
|
+
self.overrides = overrides
|
|
80
92
|
|
|
81
93
|
self.generator = generator
|
|
82
94
|
self.default = None
|
|
83
95
|
self.ignore_generated = False
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
self.ignore_default_in_identifier = False
|
|
97
|
+
self.groups = groups if groups else set()
|
|
98
|
+
|
|
99
|
+
if field_or_default is not None:
|
|
100
|
+
assert (
|
|
101
|
+
self.generator is None
|
|
102
|
+
), "generator and field_or_default are exclusive options"
|
|
103
|
+
if isinstance(field_or_default, field):
|
|
104
|
+
self.ignore_generated = field_or_default.ignore_generated
|
|
105
|
+
# Allow field to override the overrides flag
|
|
106
|
+
if field_or_default.overrides:
|
|
107
|
+
self.overrides = True
|
|
108
|
+
|
|
109
|
+
# Process groups from field
|
|
110
|
+
if field_or_default.groups:
|
|
111
|
+
self.groups = field_or_default.groups
|
|
112
|
+
|
|
113
|
+
if field_or_default.ignore_default is not None:
|
|
114
|
+
# ignore_default: value is default AND ignored in identifier
|
|
115
|
+
self.default = field_or_default.ignore_default
|
|
116
|
+
self.ignore_default_in_identifier = True
|
|
117
|
+
elif field_or_default.default is not None:
|
|
118
|
+
# default: value is default but NOT ignored in identifier
|
|
119
|
+
self.default = field_or_default.default
|
|
120
|
+
self.ignore_default_in_identifier = False
|
|
121
|
+
elif field_or_default.default_factory is not None:
|
|
122
|
+
self.generator = field_or_default.default_factory
|
|
94
123
|
else:
|
|
95
|
-
|
|
124
|
+
# Bare default: backwards compatible, ignore in identifier
|
|
125
|
+
self.default = field_or_default
|
|
126
|
+
self.ignore_default_in_identifier = True
|
|
96
127
|
|
|
97
128
|
assert (
|
|
98
129
|
not self.constant or self.default is not None
|
|
@@ -137,12 +168,44 @@ class ArgumentOptions:
|
|
|
137
168
|
optionaltype = get_optional(typehint)
|
|
138
169
|
type = Type.fromType(optionaltype or typehint)
|
|
139
170
|
|
|
140
|
-
if
|
|
171
|
+
if (
|
|
172
|
+
"field_or_default" not in self.kwargs
|
|
173
|
+
or self.kwargs["field_or_default"] is None
|
|
174
|
+
):
|
|
141
175
|
defaultvalue = getattr(originaltype, name, None)
|
|
142
|
-
self.kwargs["
|
|
176
|
+
self.kwargs["field_or_default"] = defaultvalue
|
|
177
|
+
|
|
178
|
+
# Emit deprecation warning for bare default values (not wrapped in field)
|
|
179
|
+
# Skip warning for Constant parameters - they are inherently constant, not defaults
|
|
180
|
+
if (
|
|
181
|
+
defaultvalue is not None
|
|
182
|
+
and not isinstance(defaultvalue, field)
|
|
183
|
+
and not self.kwargs.get("constant")
|
|
184
|
+
):
|
|
185
|
+
module = originaltype.__module__
|
|
186
|
+
count = _deprecation_warning_counts.get(module, 0)
|
|
187
|
+
if count < _MAX_WARNINGS_PER_MODULE:
|
|
188
|
+
_deprecation_warning_counts[module] = count + 1
|
|
189
|
+
class_name = originaltype.__qualname__
|
|
190
|
+
remaining = _MAX_WARNINGS_PER_MODULE - count - 1
|
|
191
|
+
limit_msg = (
|
|
192
|
+
f" ({remaining} more warnings for this module)"
|
|
193
|
+
if remaining > 0
|
|
194
|
+
else " (no more warnings for this module)"
|
|
195
|
+
)
|
|
196
|
+
warnings.warn(
|
|
197
|
+
f"Deprecated: parameter `{name}` in {module}.{class_name} "
|
|
198
|
+
f"has an ambiguous default value. Use `field(ignore_default=...)` "
|
|
199
|
+
f"to keep current behavior (default ignored in identifier) or "
|
|
200
|
+
f"`field(default=...)` to include default in identifier. "
|
|
201
|
+
f"Run `experimaestro refactor default-values` to fix automatically."
|
|
202
|
+
f"{limit_msg}",
|
|
203
|
+
DeprecationWarning,
|
|
204
|
+
stacklevel=6,
|
|
205
|
+
)
|
|
143
206
|
|
|
144
207
|
self.kwargs["required"] = (optionaltype is None) and (
|
|
145
|
-
self.kwargs["
|
|
208
|
+
self.kwargs["field_or_default"] is None
|
|
146
209
|
)
|
|
147
210
|
|
|
148
211
|
return Argument(name, type, **self.kwargs)
|
|
@@ -174,42 +237,116 @@ T = TypeVar("T")
|
|
|
174
237
|
|
|
175
238
|
paramHint = _Param()
|
|
176
239
|
Param = Annotated[T, paramHint]
|
|
240
|
+
"""Type annotation for configuration parameters.
|
|
241
|
+
|
|
242
|
+
Parameters annotated with ``Param[T]`` are included in the configuration
|
|
243
|
+
identifier computation and must be set before the configuration is sealed.
|
|
244
|
+
|
|
245
|
+
Example::
|
|
246
|
+
|
|
247
|
+
class MyConfig(Config):
|
|
248
|
+
name: Param[str]
|
|
249
|
+
count: Param[int] = field(default=10)
|
|
250
|
+
threshold: Param[float] = field(ignore_default=0.5)
|
|
251
|
+
"""
|
|
177
252
|
|
|
178
253
|
optionHint = _Param(ignored=True)
|
|
179
254
|
Option = Annotated[T, optionHint]
|
|
255
|
+
"""Deprecated alias for Meta. Use Meta instead."""
|
|
256
|
+
|
|
180
257
|
Meta = Annotated[T, optionHint]
|
|
258
|
+
"""Type annotation for meta-parameters (ignored in identifier computation).
|
|
259
|
+
|
|
260
|
+
Use ``Meta[T]`` for parameters that should not affect the task identity,
|
|
261
|
+
such as output paths or runtime configuration.
|
|
262
|
+
|
|
263
|
+
Example::
|
|
264
|
+
|
|
265
|
+
class MyTask(Task):
|
|
266
|
+
# This affects the task identity
|
|
267
|
+
learning_rate: Param[float]
|
|
268
|
+
|
|
269
|
+
# This does not affect the identity
|
|
270
|
+
checkpoint_path: Meta[Path] = field(default_factory=PathGenerator("model.pt"))
|
|
271
|
+
"""
|
|
181
272
|
|
|
182
273
|
dataHint = _Param(ignored=True, is_data=True)
|
|
183
274
|
DataPath = Annotated[Path, dataHint]
|
|
184
|
-
"""
|
|
275
|
+
"""Type annotation for data paths that should be serialized.
|
|
276
|
+
|
|
277
|
+
Use ``DataPath`` for paths that point to data files that should be
|
|
278
|
+
preserved when serializing/deserializing a configuration. The path
|
|
279
|
+
is copied during serialization.
|
|
280
|
+
|
|
281
|
+
Example::
|
|
282
|
+
|
|
283
|
+
class MyConfig(Config):
|
|
284
|
+
model_weights: DataPath
|
|
285
|
+
"""
|
|
185
286
|
|
|
186
287
|
|
|
187
288
|
class field:
|
|
188
|
-
"""
|
|
289
|
+
"""Specify additional properties for a configuration parameter.
|
|
290
|
+
|
|
291
|
+
Use ``field()`` to control default value behavior and parameter grouping.
|
|
292
|
+
|
|
293
|
+
Example::
|
|
294
|
+
|
|
295
|
+
class MyConfig(Config):
|
|
296
|
+
# Default included in identifier
|
|
297
|
+
count: Param[int] = field(default=10)
|
|
298
|
+
|
|
299
|
+
# Default ignored in identifier (backwards compatible)
|
|
300
|
+
threshold: Param[float] = field(ignore_default=0.5)
|
|
301
|
+
|
|
302
|
+
# Generated path
|
|
303
|
+
output: Meta[Path] = field(default_factory=PathGenerator("out.txt"))
|
|
304
|
+
|
|
305
|
+
# Parameter in a group (for partial identifiers)
|
|
306
|
+
lr: Param[float] = field(groups=[training_group])
|
|
307
|
+
"""
|
|
189
308
|
|
|
190
309
|
def __init__(
|
|
191
310
|
self,
|
|
192
311
|
*,
|
|
193
312
|
default: Any = None,
|
|
194
313
|
default_factory: Callable = None,
|
|
314
|
+
ignore_default: Any = None,
|
|
195
315
|
ignore_generated=False,
|
|
316
|
+
overrides=False,
|
|
317
|
+
groups: list["ParameterGroup"] = None,
|
|
196
318
|
):
|
|
197
|
-
"""
|
|
198
|
-
|
|
199
|
-
:param default:
|
|
200
|
-
:param default_factory:
|
|
201
|
-
:param
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
319
|
+
"""Create a field specification.
|
|
320
|
+
|
|
321
|
+
:param default: Default value (included in identifier computation)
|
|
322
|
+
:param default_factory: Callable that generates the default value
|
|
323
|
+
:param ignore_default: Default value that is ignored in identifier computation
|
|
324
|
+
when the actual value equals this default. Use for backwards-compatible
|
|
325
|
+
behavior with bare default values.
|
|
326
|
+
:param ignore_generated: If True, the generated value is hidden from tasks.
|
|
327
|
+
Useful for adding a field that changes the identifier but won't be used.
|
|
328
|
+
:param overrides: If True, suppress warning when overriding parent parameter
|
|
329
|
+
:param groups: List of ParameterGroup objects for partial identifiers.
|
|
330
|
+
Used with subparameters to compute identifiers that exclude certain groups.
|
|
205
331
|
"""
|
|
206
332
|
assert not (
|
|
207
333
|
(default is not None) and (default_factory is not None)
|
|
208
334
|
), "default and default_factory are mutually exclusive options"
|
|
209
335
|
|
|
336
|
+
assert not (
|
|
337
|
+
(default is not None) and (ignore_default is not None)
|
|
338
|
+
), "default and ignore_default are mutually exclusive options"
|
|
339
|
+
|
|
340
|
+
assert not (
|
|
341
|
+
(ignore_default is not None) and (default_factory is not None)
|
|
342
|
+
), "ignore_default and default_factory are mutually exclusive options"
|
|
343
|
+
|
|
210
344
|
self.default_factory = default_factory
|
|
211
345
|
self.default = default
|
|
346
|
+
self.ignore_default = ignore_default
|
|
212
347
|
self.ignore_generated = ignore_generated
|
|
348
|
+
self.overrides = overrides
|
|
349
|
+
self.groups = set(groups) if groups else set()
|
|
213
350
|
|
|
214
351
|
|
|
215
352
|
class help(TypeAnnotation):
|
|
@@ -220,17 +357,6 @@ class help(TypeAnnotation):
|
|
|
220
357
|
options.kwargs["help"] = self.text
|
|
221
358
|
|
|
222
359
|
|
|
223
|
-
class default(TypeAnnotation):
|
|
224
|
-
"""Adds a default value (useful when we have problems with setattr and class
|
|
225
|
-
properties)"""
|
|
226
|
-
|
|
227
|
-
def __init__(self, value):
|
|
228
|
-
self.value = value
|
|
229
|
-
|
|
230
|
-
def annotate(self, options: ArgumentOptions):
|
|
231
|
-
options.kwargs["default"] = self.value
|
|
232
|
-
|
|
233
|
-
|
|
234
360
|
class ConstantHint(TypeAnnotation):
|
|
235
361
|
def annotate(self, options: ArgumentOptions):
|
|
236
362
|
options.kwargs["constant"] = True
|
|
@@ -238,3 +364,13 @@ class ConstantHint(TypeAnnotation):
|
|
|
238
364
|
|
|
239
365
|
constantHint = ConstantHint()
|
|
240
366
|
Constant = Annotated[T, constantHint]
|
|
367
|
+
"""Type annotation for constant (read-only) parameters.
|
|
368
|
+
|
|
369
|
+
Constants must have a default value and cannot be modified after creation.
|
|
370
|
+
They are included in the identifier computation.
|
|
371
|
+
|
|
372
|
+
Example::
|
|
373
|
+
|
|
374
|
+
class MyConfig(Config):
|
|
375
|
+
version: Constant[str] = "1.0"
|
|
376
|
+
"""
|
experimaestro/core/identifier.py
CHANGED
|
@@ -5,9 +5,12 @@ import hashlib
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import struct
|
|
8
|
-
from typing import Optional
|
|
8
|
+
from typing import Optional, TYPE_CHECKING
|
|
9
9
|
from experimaestro.core.objects import Config, ConfigMixin
|
|
10
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from experimaestro.core.subparameters import Subparameters
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
class ConfigPath:
|
|
13
16
|
"""Used to keep track of cycles when computing a hash"""
|
|
@@ -19,6 +22,9 @@ class ConfigPath:
|
|
|
19
22
|
self.config2index = {}
|
|
20
23
|
"""Associate an index in the list with a configuration"""
|
|
21
24
|
|
|
25
|
+
self.instance_order: dict[bytes, list[int]] = {}
|
|
26
|
+
"""Maps InstanceConfig base identifier to list of object ids in encounter order"""
|
|
27
|
+
|
|
22
28
|
def detect_loop(self, config) -> Optional[int]:
|
|
23
29
|
"""If there is a loop, return the relative index and update the path"""
|
|
24
30
|
index = self.config2index.get(id(config), None)
|
|
@@ -100,7 +106,15 @@ class Identifier:
|
|
|
100
106
|
|
|
101
107
|
|
|
102
108
|
class IdentifierComputer:
|
|
103
|
-
"""This class is in charge of computing a config/task identifier
|
|
109
|
+
"""This class is in charge of computing a config/task identifier
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
config: The configuration to compute the identifier for
|
|
113
|
+
config_path: Used to track cycles when computing identifiers
|
|
114
|
+
version: Hash computation version (defaults to XPM_HASH_COMPUTER env var or 2)
|
|
115
|
+
subparameters: If provided, only include parameters that are not excluded
|
|
116
|
+
by this Subparameters instance (for partial identifier computation)
|
|
117
|
+
"""
|
|
104
118
|
|
|
105
119
|
OBJECT_ID = b"\x00"
|
|
106
120
|
INT_ID = b"\x01"
|
|
@@ -115,13 +129,22 @@ class IdentifierComputer:
|
|
|
115
129
|
ENUM_ID = b"\x0a"
|
|
116
130
|
CYCLE_REFERENCE = b"\x0b"
|
|
117
131
|
INIT_TASKS = b"\x0c"
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
INSTANCE_ID = b"\x0d"
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
config: "ConfigMixin",
|
|
137
|
+
config_path: ConfigPath,
|
|
138
|
+
*,
|
|
139
|
+
version=None,
|
|
140
|
+
subparameters: "Subparameters" = None,
|
|
141
|
+
):
|
|
120
142
|
# Hasher for parameters
|
|
121
143
|
self._hasher = hashlib.sha256()
|
|
122
144
|
self.config = config
|
|
123
145
|
self.config_path = config_path
|
|
124
146
|
self.version = version or int(os.environ.get("XPM_HASH_COMPUTER", 2))
|
|
147
|
+
self.subparameters = subparameters
|
|
125
148
|
if hash_logger.isEnabledFor(logging.DEBUG):
|
|
126
149
|
hash_logger.debug(
|
|
127
150
|
"starting hash (%s): %s", hash(str(self.config)), self.config
|
|
@@ -184,6 +207,19 @@ class IdentifierComputer:
|
|
|
184
207
|
|
|
185
208
|
# Handles configurations
|
|
186
209
|
elif isinstance(value, ConfigMixin):
|
|
210
|
+
# For deprecated configs with __convert__, compute identifier from converted config
|
|
211
|
+
# This must happen BEFORE hashing OBJECT_ID to ensure identical identifiers
|
|
212
|
+
if myself:
|
|
213
|
+
xpmtype = value.__xpmtype__
|
|
214
|
+
if xpmtype.deprecated and hasattr(value, "__convert__"):
|
|
215
|
+
hash_logger.debug(
|
|
216
|
+
"Computing hash via __convert__ for deprecated %s", xpmtype
|
|
217
|
+
)
|
|
218
|
+
converted = value.__convert__()
|
|
219
|
+
# Delegate entirely to the converted config
|
|
220
|
+
self.update(converted, myself=True)
|
|
221
|
+
return
|
|
222
|
+
|
|
187
223
|
# Encodes the identifier
|
|
188
224
|
self._hashupdate(IdentifierComputer.OBJECT_ID)
|
|
189
225
|
|
|
@@ -201,6 +237,33 @@ class IdentifierComputer:
|
|
|
201
237
|
)
|
|
202
238
|
self._hashupdate(value_id.all)
|
|
203
239
|
|
|
240
|
+
# Handle InstanceConfig: add instance order only if duplicate detected
|
|
241
|
+
from experimaestro.core.objects import InstanceConfig
|
|
242
|
+
|
|
243
|
+
if isinstance(value, InstanceConfig):
|
|
244
|
+
# Use the base identifier to track instances
|
|
245
|
+
base_id = value_id.all
|
|
246
|
+
obj_id = id(value)
|
|
247
|
+
|
|
248
|
+
# Track instances with this base identifier
|
|
249
|
+
if base_id not in self.config_path.instance_order:
|
|
250
|
+
# First occurrence - just record it (NO-OP for backwards compatibility)
|
|
251
|
+
self.config_path.instance_order[base_id] = [obj_id]
|
|
252
|
+
else:
|
|
253
|
+
# Check if this specific object was already seen
|
|
254
|
+
instances = self.config_path.instance_order[base_id]
|
|
255
|
+
if obj_id not in instances:
|
|
256
|
+
# New instance with same params - add to list
|
|
257
|
+
instances.append(obj_id)
|
|
258
|
+
|
|
259
|
+
# Get position in the list
|
|
260
|
+
position = instances.index(obj_id)
|
|
261
|
+
|
|
262
|
+
# Only add instance order if not the first (position > 0)
|
|
263
|
+
if position > 0:
|
|
264
|
+
self._hashupdate(IdentifierComputer.INSTANCE_ID)
|
|
265
|
+
self._hashupdate(struct.pack("!q", position))
|
|
266
|
+
|
|
204
267
|
# And that's it!
|
|
205
268
|
return
|
|
206
269
|
|
|
@@ -216,6 +279,11 @@ class IdentifierComputer:
|
|
|
216
279
|
# Process arguments (sort by name to ensure uniqueness)
|
|
217
280
|
arguments = sorted(xpmtype.arguments.values(), key=lambda a: a.name)
|
|
218
281
|
for argument in arguments:
|
|
282
|
+
# Skip arguments excluded by subparameters (for partial identifiers)
|
|
283
|
+
if self.subparameters is not None:
|
|
284
|
+
if self.subparameters.is_excluded(argument.groups):
|
|
285
|
+
continue
|
|
286
|
+
|
|
219
287
|
# Ignored argument
|
|
220
288
|
if argument.ignored:
|
|
221
289
|
argvalue = value.__xpm__.values.get(argument.name, None)
|
|
@@ -234,7 +302,8 @@ class IdentifierComputer:
|
|
|
234
302
|
# Argument value
|
|
235
303
|
# Skip if the argument is not a constant, and
|
|
236
304
|
# - optional argument: both value and default are None
|
|
237
|
-
# - the argument value is equal to the default value
|
|
305
|
+
# - the argument value is equal to the default value AND
|
|
306
|
+
# ignore_default_in_identifier is True
|
|
238
307
|
try:
|
|
239
308
|
argvalue = getattr(value, argument.name, None)
|
|
240
309
|
except KeyError:
|
|
@@ -252,7 +321,8 @@ class IdentifierComputer:
|
|
|
252
321
|
and argvalue is None
|
|
253
322
|
)
|
|
254
323
|
or (
|
|
255
|
-
argument.
|
|
324
|
+
argument.ignore_default_in_identifier
|
|
325
|
+
and argument.default is not None
|
|
256
326
|
and argument.default == remove_meta(argvalue)
|
|
257
327
|
)
|
|
258
328
|
):
|
|
@@ -308,3 +378,34 @@ class IdentifierComputer:
|
|
|
308
378
|
identifier.has_loops = config_path.has_loop()
|
|
309
379
|
|
|
310
380
|
return identifier
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def compute_partial(
|
|
384
|
+
config: "ConfigMixin",
|
|
385
|
+
subparameters: "Subparameters",
|
|
386
|
+
config_path: ConfigPath | None = None,
|
|
387
|
+
version=None,
|
|
388
|
+
) -> Identifier:
|
|
389
|
+
"""Compute a partial identifier for a configuration
|
|
390
|
+
|
|
391
|
+
A partial identifier excludes certain parameter groups, allowing
|
|
392
|
+
configurations that differ only in those groups to share the same
|
|
393
|
+
partial identifier (and thus the same partial directory).
|
|
394
|
+
|
|
395
|
+
:param config: the configuration for which we compute the identifier
|
|
396
|
+
:param subparameters: the Subparameters instance defining which groups
|
|
397
|
+
to include/exclude
|
|
398
|
+
:param config_path: used to track down cycles between configurations
|
|
399
|
+
:param version: version for the hash computation (None for the last one)
|
|
400
|
+
"""
|
|
401
|
+
config_path = config_path or ConfigPath()
|
|
402
|
+
|
|
403
|
+
with config_path.push(config):
|
|
404
|
+
computer = IdentifierComputer(
|
|
405
|
+
config, config_path, version=version, subparameters=subparameters
|
|
406
|
+
)
|
|
407
|
+
computer.update(config, myself=True)
|
|
408
|
+
identifier = computer.identifier()
|
|
409
|
+
identifier.has_loops = config_path.has_loop()
|
|
410
|
+
|
|
411
|
+
return identifier
|
|
@@ -2,8 +2,11 @@ from .config_walk import ConfigWalkContext, ConfigWalk
|
|
|
2
2
|
from .config import (
|
|
3
3
|
ConfigMixin,
|
|
4
4
|
Config,
|
|
5
|
+
InstanceConfig,
|
|
5
6
|
ConfigInformation,
|
|
6
7
|
Task,
|
|
8
|
+
TaskStub,
|
|
9
|
+
ResumableTask,
|
|
7
10
|
LightweightTask,
|
|
8
11
|
WatchedOutput,
|
|
9
12
|
DependentMarker,
|
|
@@ -25,10 +28,13 @@ from .config_utils import (
|
|
|
25
28
|
__all__ = [
|
|
26
29
|
"ConfigMixin",
|
|
27
30
|
"Config",
|
|
31
|
+
"InstanceConfig",
|
|
28
32
|
"ConfigInformation",
|
|
29
33
|
"ConfigWalkContext",
|
|
30
34
|
"ConfigWalk",
|
|
31
35
|
"Task",
|
|
36
|
+
"TaskStub",
|
|
37
|
+
"ResumableTask",
|
|
32
38
|
"LightweightTask",
|
|
33
39
|
"ObjectStore",
|
|
34
40
|
"WatchedOutput",
|