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
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# Tests for identifier stability across versions
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
from experimaestro import (
|
|
9
|
+
Param,
|
|
10
|
+
Config,
|
|
11
|
+
field,
|
|
12
|
+
InstanceConfig,
|
|
13
|
+
Task,
|
|
14
|
+
LightweightTask,
|
|
15
|
+
Option,
|
|
16
|
+
)
|
|
17
|
+
from experimaestro.scheduler.workspace import RunMode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- Basic types ---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigInt(Config):
|
|
24
|
+
"""Config with int parameter"""
|
|
25
|
+
|
|
26
|
+
__xpmid__ = "test.stability.ConfigInt"
|
|
27
|
+
x: Param[int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigStr(Config):
|
|
31
|
+
"""Config with str parameter"""
|
|
32
|
+
|
|
33
|
+
__xpmid__ = "test.stability.ConfigStr"
|
|
34
|
+
s: Param[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConfigBool(Config):
|
|
38
|
+
"""Config with bool parameter"""
|
|
39
|
+
|
|
40
|
+
__xpmid__ = "test.stability.ConfigBool"
|
|
41
|
+
b: Param[bool]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConfigFloat(Config):
|
|
45
|
+
"""Config with float parameter"""
|
|
46
|
+
|
|
47
|
+
__xpmid__ = "test.stability.ConfigFloat"
|
|
48
|
+
f: Param[float]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- Enum ---
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MyEnum(Enum):
|
|
55
|
+
"""Test enum"""
|
|
56
|
+
|
|
57
|
+
VALUE_A = 1
|
|
58
|
+
VALUE_B = 2
|
|
59
|
+
VALUE_C = 3
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Override module name to ensure stable identifiers across different import methods
|
|
63
|
+
MyEnum.__module__ = "test_identifier_stability"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ConfigEnum(Config):
|
|
67
|
+
"""Config with enum parameter"""
|
|
68
|
+
|
|
69
|
+
__xpmid__ = "test.stability.ConfigEnum"
|
|
70
|
+
e: Param[MyEnum]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- Collections ---
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ConfigList(Config):
|
|
77
|
+
"""Config with list parameter"""
|
|
78
|
+
|
|
79
|
+
__xpmid__ = "test.stability.ConfigList"
|
|
80
|
+
items: Param[List[int]]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ConfigDict(Config):
|
|
84
|
+
"""Config with dict parameter"""
|
|
85
|
+
|
|
86
|
+
__xpmid__ = "test.stability.ConfigDict"
|
|
87
|
+
mapping: Param[Dict[str, int]]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ConfigNestedList(Config):
|
|
91
|
+
"""Config with nested list of configs"""
|
|
92
|
+
|
|
93
|
+
__xpmid__ = "test.stability.ConfigNestedList"
|
|
94
|
+
configs: Param[List[ConfigInt]]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ConfigNestedDict(Config):
|
|
98
|
+
"""Config with nested dict of configs"""
|
|
99
|
+
|
|
100
|
+
__xpmid__ = "test.stability.ConfigNestedDict"
|
|
101
|
+
configs: Param[Dict[str, ConfigInt]]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --- Nested configs ---
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ConfigNested(Config):
|
|
108
|
+
"""Config with nested config parameter"""
|
|
109
|
+
|
|
110
|
+
__xpmid__ = "test.stability.ConfigNested"
|
|
111
|
+
inner: Param[ConfigInt]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ConfigMultiNested(Config):
|
|
115
|
+
"""Config with multiple nested configs"""
|
|
116
|
+
|
|
117
|
+
__xpmid__ = "test.stability.ConfigMultiNested"
|
|
118
|
+
a: Param[ConfigInt]
|
|
119
|
+
b: Param[ConfigStr]
|
|
120
|
+
c: Param[ConfigNested]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# --- Options and defaults ---
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ConfigWithOption(Config):
|
|
127
|
+
"""Config with option parameter"""
|
|
128
|
+
|
|
129
|
+
__xpmid__ = "test.stability.ConfigWithOption"
|
|
130
|
+
required: Param[int]
|
|
131
|
+
optional: Option[int] = field(ignore_default=42)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ConfigWithDefault(Config):
|
|
135
|
+
"""Config with default parameter"""
|
|
136
|
+
|
|
137
|
+
__xpmid__ = "test.stability.ConfigWithDefault"
|
|
138
|
+
x: Param[int] = field(ignore_default=10)
|
|
139
|
+
y: Param[int]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# --- Tasks ---
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SimpleTask(Task):
|
|
146
|
+
"""Simple task"""
|
|
147
|
+
|
|
148
|
+
__xpmid__ = "test.stability.SimpleTask"
|
|
149
|
+
x: Param[int]
|
|
150
|
+
|
|
151
|
+
def execute(self):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TaskWithConfig(Task):
|
|
156
|
+
"""Task with config parameter"""
|
|
157
|
+
|
|
158
|
+
__xpmid__ = "test.stability.TaskWithConfig"
|
|
159
|
+
config: Param[ConfigInt]
|
|
160
|
+
|
|
161
|
+
def execute(self):
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TaskWithOutput(Task):
|
|
166
|
+
"""Task that outputs a config"""
|
|
167
|
+
|
|
168
|
+
__xpmid__ = "test.stability.TaskWithOutput"
|
|
169
|
+
value: Param[int]
|
|
170
|
+
|
|
171
|
+
def task_outputs(self, dep):
|
|
172
|
+
return ConfigInt.C(x=self.value)
|
|
173
|
+
|
|
174
|
+
def execute(self):
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class MyLightweightTask(LightweightTask):
|
|
179
|
+
"""Lightweight task for init_tasks"""
|
|
180
|
+
|
|
181
|
+
__xpmid__ = "test.stability.MyLightweightTask"
|
|
182
|
+
param: Param[int]
|
|
183
|
+
|
|
184
|
+
def execute(self):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# --- Cycles ---
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CycleA(Config):
|
|
192
|
+
"""Config that can reference CycleB"""
|
|
193
|
+
|
|
194
|
+
__xpmid__ = "test.stability.CycleA"
|
|
195
|
+
b: Param["CycleB"]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class CycleB(Config):
|
|
199
|
+
"""Config that can reference CycleA"""
|
|
200
|
+
|
|
201
|
+
__xpmid__ = "test.stability.CycleB"
|
|
202
|
+
a: Param["CycleA"]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# --- InstanceConfig ---
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class SubModel(InstanceConfig):
|
|
209
|
+
"""InstanceConfig for testing instance identity"""
|
|
210
|
+
|
|
211
|
+
__xpmid__ = "test.stability.SubModel"
|
|
212
|
+
value: Param[int] = field(ignore_default=100)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class ModelContainer(Config):
|
|
216
|
+
"""Config that contains SubModel instances"""
|
|
217
|
+
|
|
218
|
+
__xpmid__ = "test.stability.ModelContainer"
|
|
219
|
+
m1: Param[SubModel]
|
|
220
|
+
m2: Param[SubModel]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_configurations():
|
|
224
|
+
"""Return all test configurations with their identifiers
|
|
225
|
+
|
|
226
|
+
Returns a dict mapping test case names to configuration objects
|
|
227
|
+
"""
|
|
228
|
+
configs = {}
|
|
229
|
+
|
|
230
|
+
# Basic types
|
|
231
|
+
configs["int_positive"] = ConfigInt.C(x=42)
|
|
232
|
+
configs["int_negative"] = ConfigInt.C(x=-10)
|
|
233
|
+
configs["int_zero"] = ConfigInt.C(x=0)
|
|
234
|
+
configs["str_simple"] = ConfigStr.C(s="hello")
|
|
235
|
+
configs["str_empty"] = ConfigStr.C(s="")
|
|
236
|
+
configs["str_unicode"] = ConfigStr.C(s="héllo wörld 🌍")
|
|
237
|
+
configs["bool_true"] = ConfigBool.C(b=True)
|
|
238
|
+
configs["bool_false"] = ConfigBool.C(b=False)
|
|
239
|
+
configs["float_simple"] = ConfigFloat.C(f=3.14)
|
|
240
|
+
configs["float_negative"] = ConfigFloat.C(f=-2.5)
|
|
241
|
+
configs["float_zero"] = ConfigFloat.C(f=0.0)
|
|
242
|
+
|
|
243
|
+
# Enum
|
|
244
|
+
configs["enum_value_a"] = ConfigEnum.C(e=MyEnum.VALUE_A)
|
|
245
|
+
configs["enum_value_b"] = ConfigEnum.C(e=MyEnum.VALUE_B)
|
|
246
|
+
configs["enum_value_c"] = ConfigEnum.C(e=MyEnum.VALUE_C)
|
|
247
|
+
|
|
248
|
+
# Lists
|
|
249
|
+
configs["list_empty"] = ConfigList.C(items=[])
|
|
250
|
+
configs["list_single"] = ConfigList.C(items=[1])
|
|
251
|
+
configs["list_multiple"] = ConfigList.C(items=[1, 2, 3, 4, 5])
|
|
252
|
+
configs["list_nested_empty"] = ConfigNestedList.C(configs=[])
|
|
253
|
+
configs["list_nested_single"] = ConfigNestedList.C(configs=[ConfigInt.C(x=1)])
|
|
254
|
+
configs["list_nested_multiple"] = ConfigNestedList.C(
|
|
255
|
+
configs=[ConfigInt.C(x=1), ConfigInt.C(x=2), ConfigInt.C(x=3)]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Dicts
|
|
259
|
+
configs["dict_empty"] = ConfigDict.C(mapping={})
|
|
260
|
+
configs["dict_single"] = ConfigDict.C(mapping={"a": 1})
|
|
261
|
+
configs["dict_multiple"] = ConfigDict.C(mapping={"a": 1, "b": 2, "c": 3})
|
|
262
|
+
configs["dict_nested_empty"] = ConfigNestedDict.C(configs={})
|
|
263
|
+
configs["dict_nested_single"] = ConfigNestedDict.C(configs={"x": ConfigInt.C(x=1)})
|
|
264
|
+
configs["dict_nested_multiple"] = ConfigNestedDict.C(
|
|
265
|
+
configs={"a": ConfigInt.C(x=1), "b": ConfigInt.C(x=2), "c": ConfigInt.C(x=3)}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Nested configs
|
|
269
|
+
configs["nested_simple"] = ConfigNested.C(inner=ConfigInt.C(x=100))
|
|
270
|
+
configs["nested_multi"] = ConfigMultiNested.C(
|
|
271
|
+
a=ConfigInt.C(x=1),
|
|
272
|
+
b=ConfigStr.C(s="test"),
|
|
273
|
+
c=ConfigNested.C(inner=ConfigInt.C(x=2)),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Options and defaults
|
|
277
|
+
configs["option_with_default"] = ConfigWithOption.C(required=5)
|
|
278
|
+
configs["option_override"] = ConfigWithOption.C(required=5, optional=100)
|
|
279
|
+
configs["default_with_default"] = ConfigWithDefault.C(y=20)
|
|
280
|
+
configs["default_override"] = ConfigWithDefault.C(x=99, y=20)
|
|
281
|
+
|
|
282
|
+
# Tasks (without submission)
|
|
283
|
+
configs["task_simple"] = SimpleTask.C(x=10)
|
|
284
|
+
configs["task_with_config"] = TaskWithConfig.C(config=ConfigInt.C(x=5))
|
|
285
|
+
|
|
286
|
+
# Tasks with submission (creates task outputs)
|
|
287
|
+
configs["task_submitted"] = SimpleTask.C(x=15).submit(run_mode=RunMode.DRY_RUN)
|
|
288
|
+
configs["task_with_output"] = TaskWithOutput.C(value=25).submit(
|
|
289
|
+
run_mode=RunMode.DRY_RUN
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Task using output from another task
|
|
293
|
+
task_output = TaskWithOutput.C(value=30).submit(run_mode=RunMode.DRY_RUN)
|
|
294
|
+
configs["task_using_output"] = TaskWithConfig.C(config=task_output)
|
|
295
|
+
|
|
296
|
+
# Tasks with init_tasks
|
|
297
|
+
configs["task_with_init"] = SimpleTask.C(x=20).submit(
|
|
298
|
+
run_mode=RunMode.DRY_RUN, init_tasks=[MyLightweightTask.C(param=1)]
|
|
299
|
+
)
|
|
300
|
+
configs["task_with_multiple_init"] = SimpleTask.C(x=25).submit(
|
|
301
|
+
run_mode=RunMode.DRY_RUN,
|
|
302
|
+
init_tasks=[MyLightweightTask.C(param=1), MyLightweightTask.C(param=2)],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Cycles
|
|
306
|
+
cycle_a = CycleA.C()
|
|
307
|
+
cycle_b = CycleB.C(a=cycle_a)
|
|
308
|
+
cycle_a.b = cycle_b
|
|
309
|
+
configs["cycle_simple"] = cycle_a
|
|
310
|
+
|
|
311
|
+
# InstanceConfig - test instance identity
|
|
312
|
+
# Single instance used twice (shared) - backwards compatible with regular Config
|
|
313
|
+
sm_single = SubModel.C(value=100)
|
|
314
|
+
configs["instance_shared"] = ModelContainer.C(m1=sm_single, m2=sm_single)
|
|
315
|
+
|
|
316
|
+
# Two separate instances with same parameters - different identifiers
|
|
317
|
+
sm1 = SubModel.C(value=100)
|
|
318
|
+
sm2 = SubModel.C(value=100)
|
|
319
|
+
configs["instance_separate"] = ModelContainer.C(m1=sm1, m2=sm2)
|
|
320
|
+
|
|
321
|
+
# InstanceConfig with different parameter values
|
|
322
|
+
sm3 = SubModel.C(value=200)
|
|
323
|
+
sm4 = SubModel.C(value=300)
|
|
324
|
+
configs["instance_different_values"] = ModelContainer.C(m1=sm3, m2=sm4)
|
|
325
|
+
|
|
326
|
+
return configs
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def save_reference(output_file: Path, overwrite: bool = False):
|
|
330
|
+
"""Generate and save reference identifiers
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
output_file: Path to the JSON file to save
|
|
334
|
+
overwrite: If True, overwrite existing file even if there are changes
|
|
335
|
+
"""
|
|
336
|
+
configs = get_configurations()
|
|
337
|
+
reference = {}
|
|
338
|
+
|
|
339
|
+
for name, config in configs.items():
|
|
340
|
+
identifier = config.__xpm__.identifier.all.hex()
|
|
341
|
+
reference[name] = identifier
|
|
342
|
+
|
|
343
|
+
# Check if file exists and compare
|
|
344
|
+
if output_file.exists() and not overwrite:
|
|
345
|
+
existing = load_reference(output_file)
|
|
346
|
+
|
|
347
|
+
changes = []
|
|
348
|
+
for name in sorted(set(existing.keys()) | set(reference.keys())):
|
|
349
|
+
old_id = existing.get(name)
|
|
350
|
+
new_id = reference.get(name)
|
|
351
|
+
|
|
352
|
+
if old_id is None:
|
|
353
|
+
changes.append(f" + {name}: NEW")
|
|
354
|
+
elif new_id is None:
|
|
355
|
+
changes.append(f" - {name}: REMOVED")
|
|
356
|
+
elif old_id != new_id:
|
|
357
|
+
changes.append(f" ! {name}: CHANGED")
|
|
358
|
+
changes.append(f" Old: {old_id}")
|
|
359
|
+
changes.append(f" New: {new_id}")
|
|
360
|
+
|
|
361
|
+
if changes:
|
|
362
|
+
print(
|
|
363
|
+
f"⚠️ WARNING: Reference file has {len([c for c in changes if c.startswith(' ')])} change(s):"
|
|
364
|
+
)
|
|
365
|
+
print("\n".join(changes))
|
|
366
|
+
print("\nTo overwrite, run with --overwrite flag")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
# Save to JSON file
|
|
370
|
+
with output_file.open("w") as f:
|
|
371
|
+
json.dump(reference, f, indent=2, sort_keys=True)
|
|
372
|
+
|
|
373
|
+
print(f"✓ Saved {len(reference)} reference identifiers to {output_file}")
|
|
374
|
+
return reference
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def load_reference(reference_file: Path) -> Dict[str, str]:
|
|
378
|
+
"""Load reference identifiers from JSON file
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
reference_file: Path to the JSON file
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Dictionary mapping test case names to identifier hex strings
|
|
385
|
+
"""
|
|
386
|
+
with reference_file.open("r") as f:
|
|
387
|
+
return json.load(f)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_identifier_stability():
|
|
391
|
+
"""Test that identifiers are stable across experimaestro versions"""
|
|
392
|
+
|
|
393
|
+
# Get the reference file path (same directory as this test file)
|
|
394
|
+
reference_file = Path(__file__).parent / "identifier_stability.json"
|
|
395
|
+
|
|
396
|
+
if not reference_file.exists():
|
|
397
|
+
raise FileNotFoundError(
|
|
398
|
+
f"Reference file {reference_file} not found. "
|
|
399
|
+
f"Run 'python {__file__}' to generate it."
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Load reference identifiers
|
|
403
|
+
reference = load_reference(reference_file)
|
|
404
|
+
|
|
405
|
+
# Get current configurations
|
|
406
|
+
configs = get_configurations()
|
|
407
|
+
|
|
408
|
+
# Check each configuration
|
|
409
|
+
mismatches = []
|
|
410
|
+
for name, config in configs.items():
|
|
411
|
+
current_id = config.__xpm__.identifier.all.hex()
|
|
412
|
+
expected_id = reference.get(name)
|
|
413
|
+
|
|
414
|
+
if expected_id is None:
|
|
415
|
+
mismatches.append(
|
|
416
|
+
f" - {name}: NEW (not in reference file)\n Current: {current_id}"
|
|
417
|
+
)
|
|
418
|
+
elif current_id != expected_id:
|
|
419
|
+
mismatches.append(
|
|
420
|
+
f" - {name}: MISMATCH\n"
|
|
421
|
+
f" Expected: {expected_id}\n"
|
|
422
|
+
f" Current: {current_id}"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Check for removed configurations
|
|
426
|
+
for name in reference:
|
|
427
|
+
if name not in configs:
|
|
428
|
+
mismatches.append(f" - {name}: REMOVED (no longer in test suite)")
|
|
429
|
+
|
|
430
|
+
# Report results
|
|
431
|
+
if mismatches:
|
|
432
|
+
error_msg = (
|
|
433
|
+
f"Identifier stability test failed! {len(mismatches)} mismatch(es):\n"
|
|
434
|
+
+ "\n".join(mismatches)
|
|
435
|
+
)
|
|
436
|
+
raise AssertionError(error_msg)
|
|
437
|
+
|
|
438
|
+
print(f"✓ All {len(configs)} identifiers are stable")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if __name__ == "__main__":
|
|
442
|
+
import sys
|
|
443
|
+
|
|
444
|
+
# Parse command-line arguments
|
|
445
|
+
overwrite = "--overwrite" in sys.argv
|
|
446
|
+
|
|
447
|
+
# Generate the reference file
|
|
448
|
+
reference_file = Path(__file__).parent / "identifier_stability.json"
|
|
449
|
+
reference = save_reference(reference_file, overwrite=overwrite)
|
|
450
|
+
|
|
451
|
+
if reference is None:
|
|
452
|
+
# Changes detected but not overwriting
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
|
|
455
|
+
# Print summary
|
|
456
|
+
print(f"\nGenerated {len(reference)} reference identifiers:")
|
|
457
|
+
for name in sorted(reference.keys()):
|
|
458
|
+
print(f" - {name}")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
|
-
from experimaestro import Param, Config
|
|
2
|
+
from experimaestro import field, Param, Config
|
|
3
3
|
from experimaestro.core.objects import ConfigMixin
|
|
4
4
|
from experimaestro.core.serializers import SerializationLWTask
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class A(Config):
|
|
8
|
-
x: Param[int] = 1
|
|
8
|
+
x: Param[int] = field(ignore_default=1)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class A1(A):
|
|
@@ -47,7 +47,7 @@ class LoadModel(SerializationLWTask):
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class ConfigWithOptional(Config):
|
|
50
|
-
x: Param[int] = 1
|
|
50
|
+
x: Param[int] = field(ignore_default=1)
|
|
51
51
|
y: Param[Optional[int]]
|
|
52
52
|
|
|
53
53
|
|