experimaestro 1.11.1__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 +140 -16
- experimaestro/cli/filter.py +42 -74
- experimaestro/cli/jobs.py +157 -106
- experimaestro/cli/progress.py +269 -0
- experimaestro/cli/refactor.py +249 -0
- experimaestro/click.py +0 -1
- experimaestro/commandline.py +19 -3
- experimaestro/connectors/__init__.py +22 -3
- experimaestro/connectors/local.py +12 -0
- experimaestro/core/arguments.py +192 -37
- experimaestro/core/identifier.py +127 -12
- experimaestro/core/objects/__init__.py +6 -0
- experimaestro/core/objects/config.py +702 -285
- experimaestro/core/objects/config_walk.py +24 -6
- experimaestro/core/serialization.py +91 -34
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/subparameters.py +164 -0
- experimaestro/core/types.py +198 -83
- 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/launcherfinder/registry.py +3 -3
- experimaestro/launchers/__init__.py +26 -1
- experimaestro/launchers/direct.py +12 -0
- experimaestro/launchers/slurm/base.py +154 -2
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/mkdocs/metaloader.py +0 -1
- experimaestro/mypy.py +452 -7
- experimaestro/notifications.py +75 -16
- experimaestro/progress.py +404 -0
- experimaestro/rpyc.py +0 -1
- experimaestro/run.py +19 -6
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +504 -959
- experimaestro/scheduler/dependencies.py +43 -28
- experimaestro/scheduler/dynamic_outputs.py +259 -130
- experimaestro/scheduler/experiment.py +582 -0
- experimaestro/scheduler/interfaces.py +474 -0
- experimaestro/scheduler/jobs.py +485 -0
- experimaestro/scheduler/services.py +186 -12
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +1 -1
- 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 +153 -32
- 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 +47 -6
- 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/common.py +2 -2
- experimaestro/tests/launchers/test_slurm.py +80 -0
- experimaestro/tests/restart.py +1 -1
- experimaestro/tests/tasks/all.py +7 -0
- experimaestro/tests/tasks/test_dynamic.py +231 -0
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_cli_jobs.py +615 -0
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_deprecated.py +630 -0
- experimaestro/tests/test_environment.py +200 -0
- experimaestro/tests/test_experiment.py +3 -3
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_forward.py +3 -3
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +520 -169
- experimaestro/tests/test_identifier_stability.py +458 -0
- experimaestro/tests/test_instance.py +16 -21
- experimaestro/tests/test_multitoken.py +442 -0
- experimaestro/tests/test_mypy.py +433 -0
- experimaestro/tests/test_objects.py +314 -30
- experimaestro/tests/test_outputs.py +8 -8
- experimaestro/tests/test_param.py +22 -26
- experimaestro/tests/test_partial_paths.py +231 -0
- experimaestro/tests/test_progress.py +2 -50
- experimaestro/tests/test_resumable_task.py +480 -0
- experimaestro/tests/test_serializers.py +141 -60
- experimaestro/tests/test_state_db.py +434 -0
- experimaestro/tests/test_subparameters.py +160 -0
- experimaestro/tests/test_tags.py +151 -15
- experimaestro/tests/test_tasks.py +137 -160
- experimaestro/tests/test_token_locking.py +252 -0
- experimaestro/tests/test_tokens.py +25 -19
- experimaestro/tests/test_types.py +133 -11
- experimaestro/tests/test_validation.py +19 -19
- experimaestro/tests/test_workspace_triggers.py +158 -0
- experimaestro/tests/token_reschedule.py +5 -3
- experimaestro/tests/utils.py +2 -2
- experimaestro/tokens.py +154 -57
- experimaestro/tools/diff.py +8 -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/typingutils.py +11 -2
- 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-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
- experimaestro-2.0.0b4.dist-info/RECORD +181 -0
- {experimaestro-1.11.1.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 -225
- 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-1.11.1.dist-info/RECORD +0 -158
- experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""Tests for deprecation mechanism.
|
|
2
|
+
|
|
3
|
+
Tests cover:
|
|
4
|
+
- Legacy @deprecate pattern (deprecated class inherits from new class)
|
|
5
|
+
- New @deprecate(Target) pattern with explicit target
|
|
6
|
+
- @deprecate(Target, replace=True) for immediate conversion
|
|
7
|
+
- Deprecated attributes
|
|
8
|
+
- fix_deprecated CLI for migrating job directories
|
|
9
|
+
- Task deprecation with fix_deprecated symlink creation
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from experimaestro import field, Config, Param, Task, deprecate
|
|
16
|
+
from experimaestro.core.identifier import IdentifierComputer
|
|
17
|
+
from experimaestro.scheduler.workspace import RunMode
|
|
18
|
+
from experimaestro.tools.jobs import fix_deprecated
|
|
19
|
+
|
|
20
|
+
from .utils import TemporaryExperiment
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def assert_equal(a, b, message=""):
|
|
24
|
+
"""Assert two configs have the same identifier."""
|
|
25
|
+
a_id = IdentifierComputer.compute(a)
|
|
26
|
+
b_id = IdentifierComputer.compute(b)
|
|
27
|
+
assert a_id == b_id, f"{message}: {a_id} != {b_id}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def assert_notequal(a, b, message=""):
|
|
31
|
+
"""Assert two configs have different identifiers."""
|
|
32
|
+
a_id = IdentifierComputer.compute(a)
|
|
33
|
+
b_id = IdentifierComputer.compute(b)
|
|
34
|
+
assert a_id != b_id, f"{message}: {a_id} == {b_id}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --- Legacy deprecation tests ---
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_deprecated_class_legacy():
|
|
41
|
+
"""Test legacy @deprecate pattern where deprecated class inherits from new class."""
|
|
42
|
+
|
|
43
|
+
class NewConfig(Config):
|
|
44
|
+
__xpmid__ = "test.deprecated.legacy.new"
|
|
45
|
+
|
|
46
|
+
@deprecate
|
|
47
|
+
class OldConfig(NewConfig):
|
|
48
|
+
__xpmid__ = "test.deprecated.legacy.old"
|
|
49
|
+
|
|
50
|
+
class DerivedConfig(NewConfig):
|
|
51
|
+
__xpmid__ = "test.deprecated.legacy.derived"
|
|
52
|
+
|
|
53
|
+
assert_notequal(
|
|
54
|
+
NewConfig.C(), DerivedConfig.C(), "A derived configuration has another ID"
|
|
55
|
+
)
|
|
56
|
+
assert_equal(
|
|
57
|
+
NewConfig.C(),
|
|
58
|
+
OldConfig.C(),
|
|
59
|
+
"Deprecated and new configuration have the same ID",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_deprecated_attribute():
|
|
64
|
+
"""Test deprecating a parameter with a conversion method."""
|
|
65
|
+
|
|
66
|
+
class Values(Config):
|
|
67
|
+
__xpmid__ = "test.deprecated.attribute"
|
|
68
|
+
values: Param[List[int]] = field(ignore_default=[])
|
|
69
|
+
|
|
70
|
+
@deprecate
|
|
71
|
+
def value(self, x):
|
|
72
|
+
self.values = [x]
|
|
73
|
+
|
|
74
|
+
assert_equal(Values.C(values=[1]), Values.C(value=1))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- New @deprecate(Target) pattern tests ---
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_deprecate_with_explicit_target():
|
|
81
|
+
"""Test @deprecate(TargetConfig) with explicit target class."""
|
|
82
|
+
|
|
83
|
+
class NewConfig(Config):
|
|
84
|
+
__xpmid__ = "test.deprecate.explicit.new"
|
|
85
|
+
value: Param[int]
|
|
86
|
+
|
|
87
|
+
@deprecate(NewConfig)
|
|
88
|
+
class OldConfig(Config):
|
|
89
|
+
__xpmid__ = "test.deprecate.explicit.old"
|
|
90
|
+
value: Param[int]
|
|
91
|
+
|
|
92
|
+
def __convert__(self):
|
|
93
|
+
return NewConfig.C(value=self.value)
|
|
94
|
+
|
|
95
|
+
# Creating OldConfig returns OldConfig (not NewConfig)
|
|
96
|
+
old = OldConfig.C(value=42)
|
|
97
|
+
assert type(old).__name__ == "OldConfig.XPMConfig"
|
|
98
|
+
|
|
99
|
+
# But identifiers should match because __convert__ is used for ID computation
|
|
100
|
+
new = NewConfig.C(value=42)
|
|
101
|
+
assert_equal(old, new)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_deprecate_with_parameter_transformation():
|
|
105
|
+
"""Test __convert__ that transforms parameters."""
|
|
106
|
+
|
|
107
|
+
class NewConfig(Config):
|
|
108
|
+
__xpmid__ = "test.deprecate.transform.new"
|
|
109
|
+
values: Param[List[int]]
|
|
110
|
+
|
|
111
|
+
@deprecate(NewConfig)
|
|
112
|
+
class OldConfig(Config):
|
|
113
|
+
__xpmid__ = "test.deprecate.transform.old"
|
|
114
|
+
value: Param[int]
|
|
115
|
+
|
|
116
|
+
def __convert__(self):
|
|
117
|
+
# Convert single value to list
|
|
118
|
+
return NewConfig.C(values=[self.value])
|
|
119
|
+
|
|
120
|
+
# Create old config - stays as OldConfig
|
|
121
|
+
old = OldConfig.C(value=42)
|
|
122
|
+
assert type(old).__name__ == "OldConfig.XPMConfig"
|
|
123
|
+
assert old.value == 42
|
|
124
|
+
|
|
125
|
+
# Identifier should match the equivalent NewConfig
|
|
126
|
+
new = NewConfig.C(values=[42])
|
|
127
|
+
assert_equal(old, new)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_deprecate_chained_versions():
|
|
131
|
+
"""Test chained deprecation: A_v0 -> A_v1 -> A
|
|
132
|
+
|
|
133
|
+
Each deprecated version computes its identifier via __convert__ chain.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
class A(Config):
|
|
137
|
+
__xpmid__ = "test.deprecate.chained.a"
|
|
138
|
+
data: Param[List[str]]
|
|
139
|
+
|
|
140
|
+
@deprecate(A)
|
|
141
|
+
class A_v1(Config):
|
|
142
|
+
__xpmid__ = "test.deprecate.chained.a_v1"
|
|
143
|
+
items: Param[List[str]]
|
|
144
|
+
|
|
145
|
+
def __convert__(self):
|
|
146
|
+
return A.C(data=self.items)
|
|
147
|
+
|
|
148
|
+
@deprecate(A_v1)
|
|
149
|
+
class A_v0(Config):
|
|
150
|
+
__xpmid__ = "test.deprecate.chained.a_v0"
|
|
151
|
+
item: Param[str]
|
|
152
|
+
|
|
153
|
+
def __convert__(self):
|
|
154
|
+
return A_v1.C(items=[self.item])
|
|
155
|
+
|
|
156
|
+
# Create configs - each stays as its own type
|
|
157
|
+
v0 = A_v0.C(item="hello")
|
|
158
|
+
v1 = A_v1.C(items=["hello"])
|
|
159
|
+
current = A.C(data=["hello"])
|
|
160
|
+
|
|
161
|
+
assert type(v0).__name__ == "A_v0.XPMConfig"
|
|
162
|
+
assert type(v1).__name__ == "A_v1.XPMConfig"
|
|
163
|
+
assert type(current).__name__ == "A.XPMConfig"
|
|
164
|
+
|
|
165
|
+
# All should have the same identifier (computed via conversion chain)
|
|
166
|
+
assert_equal(v0, v1)
|
|
167
|
+
assert_equal(v1, current)
|
|
168
|
+
assert_equal(v0, current)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_deprecate_preserves_original_identifier():
|
|
172
|
+
"""Test that deprecated identifier is preserved for migration (fix_deprecated)."""
|
|
173
|
+
|
|
174
|
+
class NewStyle(Config):
|
|
175
|
+
__xpmid__ = "test.deprecate.preserve.new"
|
|
176
|
+
x: Param[int]
|
|
177
|
+
|
|
178
|
+
@deprecate(NewStyle)
|
|
179
|
+
class OldStyle(Config):
|
|
180
|
+
__xpmid__ = "test.deprecate.preserve.old"
|
|
181
|
+
x: Param[int]
|
|
182
|
+
|
|
183
|
+
def __convert__(self):
|
|
184
|
+
return NewStyle.C(x=self.x)
|
|
185
|
+
|
|
186
|
+
xpmtype = OldStyle.__getxpmtype__()
|
|
187
|
+
|
|
188
|
+
# The deprecated identifier should be preserved for migration
|
|
189
|
+
assert xpmtype._deprecated_identifier is not None
|
|
190
|
+
assert xpmtype._deprecated_identifier.name == "test.deprecate.preserve.old"
|
|
191
|
+
|
|
192
|
+
# The target identifier should be accessible
|
|
193
|
+
assert xpmtype.identifier.name == "test.deprecate.preserve.new"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# --- @deprecate(Target, replace=True) tests ---
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_deprecate_replace_basic():
|
|
200
|
+
"""Test @deprecate(Target, replace=True) immediately converts to new config."""
|
|
201
|
+
|
|
202
|
+
class NewConfig(Config):
|
|
203
|
+
__xpmid__ = "test.deprecate.replace.basic.new"
|
|
204
|
+
values: Param[list[int]]
|
|
205
|
+
|
|
206
|
+
@deprecate(NewConfig, replace=True)
|
|
207
|
+
class OldConfig(Config):
|
|
208
|
+
__xpmid__ = "test.deprecate.replace.basic.old"
|
|
209
|
+
value: Param[int]
|
|
210
|
+
|
|
211
|
+
def __convert__(self):
|
|
212
|
+
return NewConfig.C(values=[self.value])
|
|
213
|
+
|
|
214
|
+
# Creating OldConfig should return a NewConfig instance
|
|
215
|
+
result = OldConfig.C(value=42)
|
|
216
|
+
|
|
217
|
+
# The result should be the new config type
|
|
218
|
+
assert type(result).__name__ == "NewConfig.XPMConfig"
|
|
219
|
+
|
|
220
|
+
# The values should be converted
|
|
221
|
+
assert result.values == [42]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_deprecate_replace_identifier():
|
|
225
|
+
"""Test that replaced configs have the same identifier as direct creation."""
|
|
226
|
+
|
|
227
|
+
class NewConfig(Config):
|
|
228
|
+
__xpmid__ = "test.deprecate.replace.id.new"
|
|
229
|
+
values: Param[list[int]]
|
|
230
|
+
|
|
231
|
+
@deprecate(NewConfig, replace=True)
|
|
232
|
+
class OldConfig(Config):
|
|
233
|
+
__xpmid__ = "test.deprecate.replace.id.old"
|
|
234
|
+
value: Param[int]
|
|
235
|
+
|
|
236
|
+
def __convert__(self):
|
|
237
|
+
return NewConfig.C(values=[self.value])
|
|
238
|
+
|
|
239
|
+
# Create via old and new ways
|
|
240
|
+
via_old = OldConfig.C(value=42)
|
|
241
|
+
via_new = NewConfig.C(values=[42])
|
|
242
|
+
|
|
243
|
+
# Both should be NewConfig instances with same identifier
|
|
244
|
+
assert type(via_old).__name__ == "NewConfig.XPMConfig"
|
|
245
|
+
assert type(via_new).__name__ == "NewConfig.XPMConfig"
|
|
246
|
+
assert_equal(via_old, via_new)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_deprecate_replace_preserves_deprecated_id():
|
|
250
|
+
"""Test that deprecated identifier is preserved for fix_deprecated even with replace=True."""
|
|
251
|
+
|
|
252
|
+
class NewConfig(Config):
|
|
253
|
+
__xpmid__ = "test.deprecate.replace.preserve.new"
|
|
254
|
+
values: Param[list[int]]
|
|
255
|
+
|
|
256
|
+
@deprecate(NewConfig, replace=True)
|
|
257
|
+
class OldConfig(Config):
|
|
258
|
+
__xpmid__ = "test.deprecate.replace.preserve.old"
|
|
259
|
+
value: Param[int]
|
|
260
|
+
|
|
261
|
+
def __convert__(self):
|
|
262
|
+
return NewConfig.C(values=[self.value])
|
|
263
|
+
|
|
264
|
+
xpmtype = OldConfig.__getxpmtype__()
|
|
265
|
+
|
|
266
|
+
# The deprecated identifier should be preserved
|
|
267
|
+
assert xpmtype._deprecated_identifier is not None
|
|
268
|
+
assert xpmtype._deprecated_identifier.name == "test.deprecate.replace.preserve.old"
|
|
269
|
+
|
|
270
|
+
# The current identifier should point to new config
|
|
271
|
+
assert xpmtype.identifier.name == "test.deprecate.replace.preserve.new"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_deprecate_replace_deprecated_from_preserved():
|
|
275
|
+
"""Test that converted config preserves reference to original deprecated config."""
|
|
276
|
+
|
|
277
|
+
class NewConfig(Config):
|
|
278
|
+
__xpmid__ = "test.deprecate.replace.from.new"
|
|
279
|
+
values: Param[list[int]]
|
|
280
|
+
|
|
281
|
+
@deprecate(NewConfig, replace=True)
|
|
282
|
+
class OldConfig(Config):
|
|
283
|
+
__xpmid__ = "test.deprecate.replace.from.old"
|
|
284
|
+
value: Param[int]
|
|
285
|
+
|
|
286
|
+
def __convert__(self):
|
|
287
|
+
return NewConfig.C(values=[self.value])
|
|
288
|
+
|
|
289
|
+
result = OldConfig.C(value=42)
|
|
290
|
+
|
|
291
|
+
# The converted config should have reference to original
|
|
292
|
+
assert result._deprecated_from is not None
|
|
293
|
+
assert type(result._deprecated_from).__name__ == "OldConfig.XPMConfig"
|
|
294
|
+
assert result._deprecated_from.value == 42
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# --- fix_deprecated tests ---
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_deprecation_info_available_for_fix_deprecated():
|
|
301
|
+
"""Test that deprecation info is available for fix_deprecated tool.
|
|
302
|
+
|
|
303
|
+
The fix_deprecated tool needs to know:
|
|
304
|
+
1. The original (deprecated) identifier
|
|
305
|
+
2. The new (target) identifier
|
|
306
|
+
This test verifies this information is available via _deprecation.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
class NewConfig(Config):
|
|
310
|
+
__xpmid__ = "test.fixdeprecated.info.new"
|
|
311
|
+
values: Param[list[int]]
|
|
312
|
+
|
|
313
|
+
@deprecate(NewConfig)
|
|
314
|
+
class OldConfig(Config):
|
|
315
|
+
__xpmid__ = "test.fixdeprecated.info.old"
|
|
316
|
+
value: Param[int]
|
|
317
|
+
|
|
318
|
+
def __convert__(self):
|
|
319
|
+
return NewConfig.C(values=[self.value])
|
|
320
|
+
|
|
321
|
+
xpmtype = OldConfig.__getxpmtype__()
|
|
322
|
+
|
|
323
|
+
# Verify deprecation info is accessible
|
|
324
|
+
assert xpmtype._deprecation is not None
|
|
325
|
+
assert (
|
|
326
|
+
xpmtype._deprecation.original_identifier.name == "test.fixdeprecated.info.old"
|
|
327
|
+
)
|
|
328
|
+
assert xpmtype._deprecation.target == NewConfig
|
|
329
|
+
assert xpmtype._deprecation.replace is False
|
|
330
|
+
|
|
331
|
+
# The identifier should point to new config
|
|
332
|
+
assert xpmtype.identifier.name == "test.fixdeprecated.info.new"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_deprecation_info_with_replace():
|
|
336
|
+
"""Test that deprecation info includes replace flag."""
|
|
337
|
+
|
|
338
|
+
class NewConfig(Config):
|
|
339
|
+
__xpmid__ = "test.fixdeprecated.replace.new"
|
|
340
|
+
values: Param[list[int]]
|
|
341
|
+
|
|
342
|
+
@deprecate(NewConfig, replace=True)
|
|
343
|
+
class OldConfig(Config):
|
|
344
|
+
__xpmid__ = "test.fixdeprecated.replace.old"
|
|
345
|
+
value: Param[int]
|
|
346
|
+
|
|
347
|
+
def __convert__(self):
|
|
348
|
+
return NewConfig.C(values=[self.value])
|
|
349
|
+
|
|
350
|
+
xpmtype = OldConfig.__getxpmtype__()
|
|
351
|
+
|
|
352
|
+
# Verify replace flag is set
|
|
353
|
+
assert xpmtype._deprecation is not None
|
|
354
|
+
assert xpmtype._deprecation.replace is True
|
|
355
|
+
assert (
|
|
356
|
+
xpmtype._deprecation.original_identifier.name
|
|
357
|
+
== "test.fixdeprecated.replace.old"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# =============================================================================
|
|
362
|
+
# Task deprecation tests (with actual experiment context and fix_deprecated)
|
|
363
|
+
# =============================================================================
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class NewConfigForTask(Config):
|
|
367
|
+
"""New configuration used in task deprecation tests."""
|
|
368
|
+
|
|
369
|
+
__xpmid__ = "test.deprecated.task.config.new"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@deprecate
|
|
373
|
+
class DeprecatedConfigForTask(NewConfigForTask):
|
|
374
|
+
"""Deprecated configuration (legacy pattern)."""
|
|
375
|
+
|
|
376
|
+
__xpmid__ = "test.deprecated.task.config.deprecated"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class OldConfigForTask(NewConfigForTask):
|
|
380
|
+
"""Old configuration without deprecate flag (for comparison)."""
|
|
381
|
+
|
|
382
|
+
__xpmid__ = "test.deprecated.task.config.deprecated"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class TaskWithDeprecatedConfig(Task):
|
|
386
|
+
"""Task that uses a deprecated config as parameter."""
|
|
387
|
+
|
|
388
|
+
__xpmid__ = "test.deprecated.task.with.config"
|
|
389
|
+
p: Param[NewConfigForTask]
|
|
390
|
+
|
|
391
|
+
def execute(self):
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _check_symlink_paths(task_new, task_old_path):
|
|
396
|
+
"""Helper to verify symlink was created correctly."""
|
|
397
|
+
task_new_path = task_new.__xpm__.job.path # type: Path
|
|
398
|
+
|
|
399
|
+
assert task_new_path.exists(), f"New path {task_new_path} should exist"
|
|
400
|
+
assert task_new_path.is_symlink(), f"New path {task_new_path} should be a symlink"
|
|
401
|
+
assert task_new_path.resolve() == task_old_path
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_task_deprecated_config_identifier():
|
|
405
|
+
"""Test that tasks using deprecated configs have correct identifiers."""
|
|
406
|
+
with TemporaryExperiment("deprecated_config", maxwait=0):
|
|
407
|
+
# Create tasks with new, old, and deprecated configs
|
|
408
|
+
task_new = TaskWithDeprecatedConfig.C(p=NewConfigForTask.C()).submit(
|
|
409
|
+
run_mode=RunMode.DRY_RUN
|
|
410
|
+
)
|
|
411
|
+
task_old = TaskWithDeprecatedConfig.C(p=OldConfigForTask.C()).submit(
|
|
412
|
+
run_mode=RunMode.DRY_RUN
|
|
413
|
+
)
|
|
414
|
+
task_deprecated = TaskWithDeprecatedConfig.C(
|
|
415
|
+
p=DeprecatedConfigForTask.C()
|
|
416
|
+
).submit(run_mode=RunMode.DRY_RUN)
|
|
417
|
+
|
|
418
|
+
logging.debug("New task ID: %s", task_new.__xpm__.identifier.all.hex())
|
|
419
|
+
logging.debug("Old task ID: %s", task_old.__xpm__.identifier.all.hex())
|
|
420
|
+
logging.debug(
|
|
421
|
+
"Deprecated task ID: %s", task_deprecated.__xpm__.identifier.all.hex()
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Old (non-deprecated) and new should have different paths
|
|
425
|
+
assert (
|
|
426
|
+
task_new.stdout() != task_old.stdout()
|
|
427
|
+
), "Old and new path should be different"
|
|
428
|
+
|
|
429
|
+
# Deprecated should have same path as new (identifier matches)
|
|
430
|
+
assert (
|
|
431
|
+
task_new.stdout() == task_deprecated.stdout()
|
|
432
|
+
), "Deprecated path should be the same as non deprecated"
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def test_task_deprecated_config_fix_deprecated():
|
|
436
|
+
"""Test fix_deprecated creates symlinks for tasks with deprecated configs."""
|
|
437
|
+
with TemporaryExperiment("deprecated_config_fix", maxwait=0) as xp:
|
|
438
|
+
task_new = TaskWithDeprecatedConfig.C(p=NewConfigForTask.C()).submit(
|
|
439
|
+
run_mode=RunMode.DRY_RUN
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Run old task (before deprecation)
|
|
443
|
+
task_old = TaskWithDeprecatedConfig.C(p=OldConfigForTask.C()).submit()
|
|
444
|
+
task_old.wait()
|
|
445
|
+
task_old_path = task_old.stdout().parent
|
|
446
|
+
|
|
447
|
+
# Now deprecate the old config
|
|
448
|
+
OldConfigForTask.__xpmtype__.deprecate()
|
|
449
|
+
|
|
450
|
+
# Run fix_deprecated
|
|
451
|
+
fix_deprecated(xp.workspace.path, fix=True, cleanup=False)
|
|
452
|
+
|
|
453
|
+
# Verify symlink was created
|
|
454
|
+
_check_symlink_paths(task_new, task_old_path)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class NewTask(Task):
|
|
458
|
+
"""New task for deprecation tests."""
|
|
459
|
+
|
|
460
|
+
__xpmid__ = "test.deprecated.task.new"
|
|
461
|
+
x: Param[int]
|
|
462
|
+
|
|
463
|
+
def execute(self):
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class OldTask(NewTask):
|
|
468
|
+
"""Old task without deprecate flag (for comparison)."""
|
|
469
|
+
|
|
470
|
+
__xpmid__ = "test.deprecated.task.deprecated"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@deprecate
|
|
474
|
+
class DeprecatedTask(NewTask):
|
|
475
|
+
"""Deprecated task (legacy pattern)."""
|
|
476
|
+
|
|
477
|
+
__xpmid__ = "test.deprecated.task.deprecated"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_task_deprecated_identifier():
|
|
481
|
+
"""Test that deprecated tasks have correct identifiers."""
|
|
482
|
+
with TemporaryExperiment("deprecated_task", maxwait=20):
|
|
483
|
+
task_new = NewTask.C(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
484
|
+
task_old = OldTask.C(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
485
|
+
task_deprecated = DeprecatedTask.C(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
486
|
+
|
|
487
|
+
# Old and new should have different paths
|
|
488
|
+
assert (
|
|
489
|
+
task_new.stdout() != task_old.stdout()
|
|
490
|
+
), "Old and new path should be different"
|
|
491
|
+
|
|
492
|
+
# Deprecated should have same path as new
|
|
493
|
+
assert (
|
|
494
|
+
task_new.stdout() == task_deprecated.stdout()
|
|
495
|
+
), "Deprecated path should be the same as non deprecated"
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_task_deprecated_fix_deprecated():
|
|
499
|
+
"""Test fix_deprecated creates symlinks for deprecated tasks."""
|
|
500
|
+
with TemporaryExperiment("deprecated_task_fix", maxwait=20) as xp:
|
|
501
|
+
task_new = NewTask.C(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
502
|
+
|
|
503
|
+
# Run old task (before deprecation)
|
|
504
|
+
task_old = OldTask.C(x=1).submit()
|
|
505
|
+
task_old.wait()
|
|
506
|
+
task_old_path = task_old.stdout().parent
|
|
507
|
+
|
|
508
|
+
# Deprecate and fix
|
|
509
|
+
OldTask.__xpmtype__.deprecate()
|
|
510
|
+
fix_deprecated(xp.workspace.path, fix=True, cleanup=False)
|
|
511
|
+
|
|
512
|
+
# Verify symlink
|
|
513
|
+
_check_symlink_paths(task_new, task_old_path)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# =============================================================================
|
|
517
|
+
# Extended tests for new deprecation mechanism with tasks
|
|
518
|
+
# =============================================================================
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class NewTaskWithConvert(Task):
|
|
522
|
+
"""New task with different parameter structure."""
|
|
523
|
+
|
|
524
|
+
__xpmid__ = "test.deprecated.task.convert.new"
|
|
525
|
+
values: Param[List[int]]
|
|
526
|
+
|
|
527
|
+
def execute(self):
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@deprecate(NewTaskWithConvert)
|
|
532
|
+
class OldTaskWithConvert(Task):
|
|
533
|
+
"""Old task with single value, deprecated to new task with list."""
|
|
534
|
+
|
|
535
|
+
__xpmid__ = "test.deprecated.task.convert.old"
|
|
536
|
+
value: Param[int]
|
|
537
|
+
|
|
538
|
+
def __convert__(self):
|
|
539
|
+
return NewTaskWithConvert.C(values=[self.value])
|
|
540
|
+
|
|
541
|
+
def execute(self):
|
|
542
|
+
pass
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_task_deprecated_with_convert_identifier():
|
|
546
|
+
"""Test deprecated task with __convert__ has correct identifier."""
|
|
547
|
+
with TemporaryExperiment("deprecated_task_convert", maxwait=0):
|
|
548
|
+
# Old task should compute identifier via __convert__
|
|
549
|
+
task_old = OldTaskWithConvert.C(value=42).submit(run_mode=RunMode.DRY_RUN)
|
|
550
|
+
task_new = NewTaskWithConvert.C(values=[42]).submit(run_mode=RunMode.DRY_RUN)
|
|
551
|
+
|
|
552
|
+
# Identifiers should match (computed via __convert__)
|
|
553
|
+
assert (
|
|
554
|
+
task_old.stdout() == task_new.stdout()
|
|
555
|
+
), "Deprecated task should have same path as equivalent new task"
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@deprecate(NewTaskWithConvert, replace=True)
|
|
559
|
+
class ReplacedTaskWithConvert(Task):
|
|
560
|
+
"""Old task that gets immediately replaced with new task."""
|
|
561
|
+
|
|
562
|
+
__xpmid__ = "test.deprecated.task.replace.old"
|
|
563
|
+
value: Param[int]
|
|
564
|
+
|
|
565
|
+
def __convert__(self):
|
|
566
|
+
return NewTaskWithConvert.C(values=[self.value])
|
|
567
|
+
|
|
568
|
+
def execute(self):
|
|
569
|
+
pass
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_task_deprecated_replace_returns_new_type():
|
|
573
|
+
"""Test deprecated task with replace=True returns new task type."""
|
|
574
|
+
with TemporaryExperiment("deprecated_task_replace", maxwait=0):
|
|
575
|
+
# Creating old task should return new task
|
|
576
|
+
result = ReplacedTaskWithConvert.C(value=42)
|
|
577
|
+
|
|
578
|
+
# Should be NewTaskWithConvert type
|
|
579
|
+
assert type(result).__name__ == "NewTaskWithConvert.XPMConfig"
|
|
580
|
+
assert result.values == [42]
|
|
581
|
+
|
|
582
|
+
# Submit should work with the new task
|
|
583
|
+
task = result.submit(run_mode=RunMode.DRY_RUN)
|
|
584
|
+
|
|
585
|
+
# Create equivalent new task directly
|
|
586
|
+
task_new = NewTaskWithConvert.C(values=[42]).submit(run_mode=RunMode.DRY_RUN)
|
|
587
|
+
|
|
588
|
+
# Paths should match
|
|
589
|
+
assert task.stdout() == task_new.stdout()
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# =============================================================================
|
|
593
|
+
# Attribute warning tests for replaced configs
|
|
594
|
+
# =============================================================================
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def test_deprecate_replace_warns_on_old_attribute(caplog):
|
|
598
|
+
"""Test that setting attributes that existed on deprecated config warns."""
|
|
599
|
+
|
|
600
|
+
class NewConfigForWarn(Config):
|
|
601
|
+
__xpmid__ = "test.deprecate.replace.warn.new"
|
|
602
|
+
values: Param[list[int]]
|
|
603
|
+
|
|
604
|
+
@deprecate(NewConfigForWarn, replace=True)
|
|
605
|
+
class OldConfigForWarn(Config):
|
|
606
|
+
__xpmid__ = "test.deprecate.replace.warn.old"
|
|
607
|
+
value: Param[int]
|
|
608
|
+
extra: Param[str] = field(
|
|
609
|
+
ignore_default="default"
|
|
610
|
+
) # This doesn't exist in NewConfigForWarn
|
|
611
|
+
|
|
612
|
+
def __convert__(self):
|
|
613
|
+
return NewConfigForWarn.C(values=[self.value])
|
|
614
|
+
|
|
615
|
+
# Creating OldConfig returns NewConfig
|
|
616
|
+
result = OldConfigForWarn.C(value=42)
|
|
617
|
+
assert type(result).__name__ == "NewConfigForWarn.XPMConfig"
|
|
618
|
+
|
|
619
|
+
# Setting an attribute that only exists on OldConfigForWarn should warn
|
|
620
|
+
import logging
|
|
621
|
+
|
|
622
|
+
with caplog.at_level(logging.WARNING):
|
|
623
|
+
result.extra = "new_value"
|
|
624
|
+
|
|
625
|
+
# Check that warning was logged
|
|
626
|
+
assert any("extra" in record.message for record in caplog.records)
|
|
627
|
+
assert any("deprecated" in record.message.lower() for record in caplog.records)
|
|
628
|
+
|
|
629
|
+
# The attribute should NOT be set on the new config
|
|
630
|
+
assert not hasattr(result, "extra") or getattr(result, "extra", None) is None
|