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
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import pytest
|
|
5
|
-
from experimaestro import Config, Task, Annotated, copyconfig,
|
|
4
|
+
from experimaestro import Config, Task, Annotated, copyconfig, field
|
|
6
5
|
from experimaestro.core.arguments import Param
|
|
7
6
|
from experimaestro.core.objects import ConfigMixin
|
|
8
|
-
from experimaestro.core.types import XPMValue
|
|
9
7
|
from experimaestro.generators import pathgenerator
|
|
10
8
|
from experimaestro.scheduler.workspace import RunMode
|
|
11
9
|
from experimaestro.tests.utils import TemporaryExperiment
|
|
@@ -18,17 +16,17 @@ def xp():
|
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
class A(Config):
|
|
21
|
-
x: Param[int] = 3
|
|
19
|
+
x: Param[int] = field(ignore_default=3)
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
def test_object_default():
|
|
25
23
|
"""Test plain default value"""
|
|
26
|
-
a = A()
|
|
24
|
+
a = A.C()
|
|
27
25
|
assert a.x == 3
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
class B(Config):
|
|
31
|
-
a: Param[A] = A(x=3)
|
|
29
|
+
a: Param[A] = field(ignore_default=A.C(x=3))
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
class C(B):
|
|
@@ -40,27 +38,26 @@ class D(B, A):
|
|
|
40
38
|
|
|
41
39
|
|
|
42
40
|
class DefaultAnnotationConfig(Config):
|
|
43
|
-
a:
|
|
41
|
+
a: Param[A] = field(default=A.C(x=3))
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
def test_object_config_default():
|
|
47
45
|
"""Test default configurations as default values"""
|
|
48
|
-
b = B()
|
|
46
|
+
b = B.C()
|
|
49
47
|
assert b.a.x == 3
|
|
50
48
|
|
|
51
|
-
c = C()
|
|
49
|
+
c = C.C()
|
|
52
50
|
assert c.a.x == 3
|
|
53
51
|
|
|
54
|
-
annotationConfig = DefaultAnnotationConfig()
|
|
52
|
+
annotationConfig = DefaultAnnotationConfig.C()
|
|
55
53
|
assert annotationConfig.a.x == 3
|
|
56
54
|
|
|
57
55
|
|
|
58
56
|
def test_hierarchy():
|
|
59
57
|
"""Test if the object hierarchy is OK"""
|
|
60
|
-
OA = A.__getxpmtype__().
|
|
61
|
-
OB = B.__getxpmtype__().
|
|
62
|
-
OC = C.__getxpmtype__().
|
|
63
|
-
OD = D.__getxpmtype__().objecttype
|
|
58
|
+
OA = A.__getxpmtype__().value_type
|
|
59
|
+
OB = B.__getxpmtype__().value_type
|
|
60
|
+
OC = C.__getxpmtype__().value_type
|
|
64
61
|
|
|
65
62
|
assert issubclass(A, Config)
|
|
66
63
|
assert issubclass(B, Config)
|
|
@@ -72,11 +69,6 @@ def test_hierarchy():
|
|
|
72
69
|
|
|
73
70
|
assert issubclass(C, B)
|
|
74
71
|
|
|
75
|
-
assert OA.__bases__ == (A, XPMValue)
|
|
76
|
-
assert OB.__bases__ == (B, XPMValue)
|
|
77
|
-
assert OC.__bases__ == (C, B.XPMValue)
|
|
78
|
-
assert OD.__bases__ == (D, B.XPMValue, A.XPMValue)
|
|
79
|
-
|
|
80
72
|
|
|
81
73
|
class CopyConfig(Task):
|
|
82
74
|
path: Annotated[Path, pathgenerator("hello.txt")]
|
|
@@ -84,7 +76,7 @@ class CopyConfig(Task):
|
|
|
84
76
|
|
|
85
77
|
|
|
86
78
|
def test_copyconfig(xp):
|
|
87
|
-
b = CopyConfig(x=2)
|
|
79
|
+
b = CopyConfig.C(x=2)
|
|
88
80
|
|
|
89
81
|
b.submit()
|
|
90
82
|
|
|
@@ -94,16 +86,308 @@ def test_copyconfig(xp):
|
|
|
94
86
|
assert "path" not in copy_b.__xpm__.values
|
|
95
87
|
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
# --- Composition operator tests (GH #33) ---
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CompositionA(Config):
|
|
93
|
+
x: Param[int]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CompositionSubA(CompositionA):
|
|
97
|
+
"""Subclass of CompositionA"""
|
|
98
|
+
|
|
99
|
+
y: Param[int] = field(ignore_default=0)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CompositionB(Config):
|
|
103
|
+
a: Param[CompositionA]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CompositionC(Config):
|
|
107
|
+
"""Config with two parameters of same type - should be ambiguous"""
|
|
108
|
+
|
|
109
|
+
a1: Param[CompositionA]
|
|
110
|
+
a2: Param[CompositionA]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CompositionD(Config):
|
|
114
|
+
"""Config with no matching parameter"""
|
|
115
|
+
|
|
116
|
+
x: Param[int]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CompositionE(Config):
|
|
120
|
+
"""Config with two parameters, one subclass of the other"""
|
|
121
|
+
|
|
122
|
+
base: Param[CompositionA]
|
|
123
|
+
sub: Param[CompositionSubA]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_composition_operator():
|
|
127
|
+
"""Test that B() @ A(x=1) is equivalent to B(a=A(x=1))"""
|
|
128
|
+
a = CompositionA.C(x=42)
|
|
129
|
+
b = CompositionB.C() @ a
|
|
130
|
+
|
|
131
|
+
assert b.a is a
|
|
132
|
+
assert b.a.x == 42
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_composition_operator_chained():
|
|
136
|
+
"""Test chaining composition operators
|
|
137
|
+
|
|
138
|
+
Chaining A @ B @ C adds both B and C to A (same outer config).
|
|
139
|
+
For nested structures, use parentheses: A @ (B @ C)
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
class MultiParam(Config):
|
|
143
|
+
a: Param[CompositionA]
|
|
144
|
+
b: Param[CompositionB]
|
|
145
|
+
|
|
146
|
+
# Chaining adds multiple configs to same outer config
|
|
147
|
+
result = MultiParam.C() @ CompositionA.C(x=10) @ CompositionB.C()
|
|
148
|
+
|
|
149
|
+
assert result.a.x == 10
|
|
150
|
+
assert result.b is not None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_composition_operator_nested():
|
|
154
|
+
"""Test nested composition with parentheses"""
|
|
155
|
+
|
|
156
|
+
class Outer(Config):
|
|
157
|
+
b: Param[CompositionB]
|
|
158
|
+
|
|
159
|
+
# For nested structures, use parentheses
|
|
160
|
+
result = Outer.C() @ (CompositionB.C() @ CompositionA.C(x=10))
|
|
161
|
+
|
|
162
|
+
assert result.b.a.x == 10
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_composition_operator_ambiguous():
|
|
166
|
+
"""Test that ambiguous composition raises ValueError"""
|
|
167
|
+
a = CompositionA.C(x=1)
|
|
168
|
+
|
|
169
|
+
with pytest.raises(ValueError, match="Ambiguous"):
|
|
170
|
+
CompositionC.C() @ a
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_composition_operator_no_match():
|
|
174
|
+
"""Test that composition with no matching param raises ValueError"""
|
|
175
|
+
a = CompositionA.C(x=1)
|
|
176
|
+
|
|
177
|
+
with pytest.raises(ValueError, match="No parameter"):
|
|
178
|
+
CompositionD.C() @ a
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_composition_operator_subclass():
|
|
182
|
+
"""Test composition works with subclasses"""
|
|
183
|
+
sub_a = CompositionSubA.C(x=5, y=10)
|
|
184
|
+
b = CompositionB.C() @ sub_a
|
|
185
|
+
|
|
186
|
+
assert b.a is sub_a
|
|
187
|
+
assert b.a.x == 5
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_composition_operator_subclass_hierarchy():
|
|
191
|
+
"""Test composition when two params have subclass relationship
|
|
192
|
+
|
|
193
|
+
When CompositionSubA is passed, both 'base' (CompositionA) and 'sub'
|
|
194
|
+
(CompositionSubA) match. This should be ambiguous since both accept it.
|
|
195
|
+
"""
|
|
196
|
+
sub_a = CompositionSubA.C(x=1, y=2)
|
|
197
|
+
|
|
198
|
+
# SubA matches both base (CompositionA) and sub (CompositionSubA)
|
|
199
|
+
with pytest.raises(ValueError, match="Ambiguous"):
|
|
200
|
+
CompositionE.C() @ sub_a
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_composition_operator_exact_match():
|
|
204
|
+
"""Test composition when base class instance matches only base param"""
|
|
205
|
+
# CompositionA matches only 'base', not 'sub' (which requires SubA)
|
|
206
|
+
a = CompositionA.C(x=1)
|
|
207
|
+
e = CompositionE.C() @ a
|
|
208
|
+
|
|
209
|
+
assert e.base is a
|
|
210
|
+
assert e.base.x == 1
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --- Value class decorator tests (GH #99) ---
|
|
214
|
+
|
|
215
|
+
# Test 1: Basic value class registration
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class ValueBasicModel(Config):
|
|
219
|
+
x: Param[int] = field(ignore_default=1)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@ValueBasicModel.value_class()
|
|
223
|
+
class ValueBasicModelImpl(ValueBasicModel):
|
|
224
|
+
def compute(self):
|
|
225
|
+
return self.x * 2
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Test 2: Subclass without explicit value class
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ValueInheritBase(Config):
|
|
232
|
+
x: Param[int] = field(ignore_default=1)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@ValueInheritBase.value_class()
|
|
236
|
+
class ValueInheritBaseImpl(ValueInheritBase):
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ValueInheritSubNoExplicit(ValueInheritBase):
|
|
241
|
+
"""Subclass without explicit value class"""
|
|
242
|
+
|
|
243
|
+
y: Param[int] = field(ignore_default=2)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Test 3: Value class with proper inheritance
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class ValueInheritParent(Config):
|
|
250
|
+
x: Param[int] = field(ignore_default=1)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@ValueInheritParent.value_class()
|
|
254
|
+
class ValueInheritParentImpl(ValueInheritParent):
|
|
255
|
+
def compute(self):
|
|
256
|
+
return self.x * 2
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class ValueInheritChild(ValueInheritParent):
|
|
260
|
+
y: Param[int] = field(ignore_default=2)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@ValueInheritChild.value_class()
|
|
264
|
+
class ValueInheritChildImpl(ValueInheritChild, ValueInheritParentImpl):
|
|
265
|
+
def compute_both(self):
|
|
266
|
+
return self.x + self.y
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# Test 4: Skip intermediate class (A -> B -> C, only A and C have value classes)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class ValueSkipBase(Config):
|
|
273
|
+
x: Param[int] = field(ignore_default=1)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@ValueSkipBase.value_class()
|
|
277
|
+
class ValueSkipBaseImpl(ValueSkipBase):
|
|
278
|
+
def compute(self):
|
|
279
|
+
return self.x * 2
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ValueSkipIntermediate(ValueSkipBase):
|
|
283
|
+
"""Intermediate class without explicit value class"""
|
|
284
|
+
|
|
285
|
+
y: Param[int] = field(ignore_default=2)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ValueSkipDeep(ValueSkipIntermediate):
|
|
289
|
+
"""Deep subclass with value class"""
|
|
290
|
+
|
|
291
|
+
z: Param[int] = field(ignore_default=3)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@ValueSkipDeep.value_class()
|
|
295
|
+
class ValueSkipDeepImpl(ValueSkipDeep, ValueSkipBaseImpl):
|
|
296
|
+
def compute_all(self):
|
|
297
|
+
return self.x + self.y + self.z
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --- Value class tests ---
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_value_decorator_basic():
|
|
304
|
+
"""Test basic value class registration"""
|
|
305
|
+
# XPMValue should return the registered value class
|
|
306
|
+
assert ValueBasicModel.XPMValue is ValueBasicModelImpl
|
|
307
|
+
|
|
308
|
+
# Creating an instance should use the value class
|
|
309
|
+
config = ValueBasicModel.C(x=5)
|
|
310
|
+
instance = config.instance()
|
|
311
|
+
|
|
312
|
+
assert isinstance(instance, ValueBasicModelImpl)
|
|
313
|
+
assert instance.x == 5
|
|
314
|
+
assert instance.compute() == 10
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_value_decorator_inheritance_no_explicit():
|
|
318
|
+
"""Test that subclass without value class uses config class as value"""
|
|
319
|
+
# SubModel has no explicit value class, XPMValue returns the config class
|
|
320
|
+
assert ValueInheritSubNoExplicit.XPMValue is ValueInheritSubNoExplicit
|
|
321
|
+
|
|
322
|
+
config = ValueInheritSubNoExplicit.C(x=3, y=4)
|
|
323
|
+
instance = config.instance()
|
|
324
|
+
|
|
325
|
+
# Instance is created from the config class (no explicit value type)
|
|
326
|
+
assert isinstance(instance, ValueInheritSubNoExplicit)
|
|
327
|
+
assert instance.x == 3
|
|
328
|
+
assert instance.y == 4
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_value_decorator_inheritance_with_explicit():
|
|
332
|
+
"""Test value class with proper inheritance from parent value class"""
|
|
333
|
+
assert ValueInheritChild.XPMValue is ValueInheritChildImpl
|
|
334
|
+
|
|
335
|
+
config = ValueInheritChild.C(x=3, y=4)
|
|
336
|
+
instance = config.instance()
|
|
337
|
+
|
|
338
|
+
assert isinstance(instance, ValueInheritChildImpl)
|
|
339
|
+
assert isinstance(instance, ValueInheritParentImpl)
|
|
340
|
+
assert instance.x == 3
|
|
341
|
+
assert instance.y == 4
|
|
342
|
+
assert instance.compute() == 6 # From parent value class
|
|
343
|
+
assert instance.compute_both() == 7 # From this value class
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_value_decorator_must_be_subclass():
|
|
347
|
+
"""Test that value class must be subclass of config"""
|
|
348
|
+
|
|
349
|
+
class LocalModel(Config):
|
|
350
|
+
x: Param[int]
|
|
351
|
+
|
|
352
|
+
class OtherConfig(Config):
|
|
353
|
+
z: Param[int]
|
|
354
|
+
|
|
355
|
+
with pytest.raises(TypeError, match="must be a subclass of"):
|
|
356
|
+
|
|
357
|
+
@LocalModel.value_class()
|
|
358
|
+
class InvalidValue(OtherConfig): # Not a subclass of LocalModel
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_value_decorator_must_inherit_parent_value():
|
|
363
|
+
"""Test that value class must inherit from parent value class"""
|
|
364
|
+
|
|
365
|
+
class LocalBase(Config):
|
|
366
|
+
x: Param[int] = field(ignore_default=1)
|
|
367
|
+
|
|
368
|
+
@LocalBase.value_class()
|
|
369
|
+
class LocalBaseImpl(LocalBase):
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
class LocalChild(LocalBase):
|
|
373
|
+
y: Param[int] = field(ignore_default=2)
|
|
374
|
+
|
|
375
|
+
with pytest.raises(TypeError, match="must be a subclass of.*parent value class"):
|
|
376
|
+
|
|
377
|
+
@LocalChild.value_class()
|
|
378
|
+
class InvalidChildValue(LocalChild): # Missing LocalBaseImpl inheritance
|
|
379
|
+
pass
|
|
380
|
+
|
|
100
381
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
382
|
+
def test_value_decorator_skip_intermediate():
|
|
383
|
+
"""Test value class when intermediate class has no value class"""
|
|
384
|
+
# ValueSkipBase has impl, ValueSkipIntermediate has none, ValueSkipDeep has impl
|
|
385
|
+
assert ValueSkipDeep.XPMValue is ValueSkipDeepImpl
|
|
104
386
|
|
|
105
|
-
|
|
387
|
+
config = ValueSkipDeep.C(x=1, y=2, z=3)
|
|
388
|
+
instance = config.instance()
|
|
106
389
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
390
|
+
assert isinstance(instance, ValueSkipDeepImpl)
|
|
391
|
+
assert isinstance(instance, ValueSkipBaseImpl)
|
|
392
|
+
assert instance.compute() == 2 # From ValueSkipBaseImpl
|
|
393
|
+
assert instance.compute_all() == 6 # From ValueSkipDeepImpl
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Test for task outputs"""
|
|
2
2
|
|
|
3
|
-
from experimaestro import Config, Task, Param
|
|
3
|
+
from experimaestro import field, Config, Task, Param
|
|
4
4
|
from experimaestro.scheduler.workspace import RunMode
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class B(Config):
|
|
8
|
-
x: Param[int] = 1
|
|
8
|
+
x: Param[int] = field(ignore_default=1)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class A(Config):
|
|
@@ -32,17 +32,17 @@ class MainB(Task):
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def test_output_taskoutput():
|
|
35
|
-
a = A(b=B())
|
|
36
|
-
output, ioutput = Main(a=a).submit(run_mode=RunMode.DRY_RUN)
|
|
35
|
+
a = A.C(b=B.C())
|
|
36
|
+
output, ioutput = Main.C(a=a).submit(run_mode=RunMode.DRY_RUN)
|
|
37
37
|
|
|
38
38
|
# Direct
|
|
39
|
-
Main(a=output)
|
|
39
|
+
Main.C(a=output)
|
|
40
40
|
|
|
41
41
|
# Via getattr
|
|
42
|
-
Main(a=A(b=output.b))
|
|
42
|
+
Main.C(a=A.C(b=output.b))
|
|
43
43
|
|
|
44
44
|
# Via getitem
|
|
45
|
-
Main(a=ioutput["a"])
|
|
45
|
+
Main.C(a=ioutput["a"])
|
|
46
46
|
|
|
47
47
|
# Now, submits
|
|
48
|
-
Main(a=output).submit(run_mode=RunMode.DRY_RUN)
|
|
48
|
+
Main.C(a=output).submit(run_mode=RunMode.DRY_RUN)
|
|
@@ -5,7 +5,6 @@ Test annotation handling for configurations and tasks
|
|
|
5
5
|
|
|
6
6
|
# Annotation specific tests
|
|
7
7
|
|
|
8
|
-
import sys
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Dict, Optional, List
|
|
11
10
|
from experimaestro.core.context import SerializationContext
|
|
@@ -17,7 +16,6 @@ from experimaestro import (
|
|
|
17
16
|
Constant,
|
|
18
17
|
Param,
|
|
19
18
|
Task,
|
|
20
|
-
default,
|
|
21
19
|
Meta,
|
|
22
20
|
Config,
|
|
23
21
|
pathgenerator,
|
|
@@ -80,8 +78,8 @@ def test_type_hinting():
|
|
|
80
78
|
__xpmid__ = "annotations.class_variable.config"
|
|
81
79
|
|
|
82
80
|
x: Param[int]
|
|
83
|
-
y: Param[float] = 2.3
|
|
84
|
-
y2:
|
|
81
|
+
y: Param[float] = field(ignore_default=2.3)
|
|
82
|
+
y2: Param[float] = field(ignore_default=2.3)
|
|
85
83
|
z: Param[Optional[float]]
|
|
86
84
|
t: Param[List[float]]
|
|
87
85
|
w: Param[int]
|
|
@@ -148,7 +146,7 @@ def test_generatedpath():
|
|
|
148
146
|
b: Param[B]
|
|
149
147
|
|
|
150
148
|
basepath = Path("/tmp/testconflict")
|
|
151
|
-
c = C(b=B(a=A())).instance(DirectoryContext(basepath))
|
|
149
|
+
c = C.C(b=B.C(a=A.C())).instance(DirectoryContext(basepath))
|
|
152
150
|
assert c.b.a.path.relative_to(basepath) == Path("out/b/a/test.txt")
|
|
153
151
|
|
|
154
152
|
|
|
@@ -158,13 +156,13 @@ def test_config_class():
|
|
|
158
156
|
class A(Config):
|
|
159
157
|
x: Param[int]
|
|
160
158
|
|
|
161
|
-
a = A(x=1)
|
|
159
|
+
a = A.C(x=1)
|
|
162
160
|
assert a.x == 1
|
|
163
161
|
|
|
164
162
|
class B(A):
|
|
165
163
|
y: Param[int]
|
|
166
164
|
|
|
167
|
-
b = B(x=1, y=2)
|
|
165
|
+
b = B.C(x=1, y=2)
|
|
168
166
|
assert b.x == 1
|
|
169
167
|
assert b.y == 2
|
|
170
168
|
|
|
@@ -174,15 +172,15 @@ def test_config_class():
|
|
|
174
172
|
class C(Config):
|
|
175
173
|
d: Param[D]
|
|
176
174
|
|
|
177
|
-
c = C(d=D(x=1))
|
|
175
|
+
c = C.C(d=D.C(x=1))
|
|
178
176
|
assert c.d.x == 1
|
|
179
177
|
|
|
180
178
|
|
|
181
179
|
def test_constant():
|
|
182
180
|
class A(Config):
|
|
183
|
-
x: Constant[int] = 2
|
|
181
|
+
x: Constant[int] = field(ignore_default=2)
|
|
184
182
|
|
|
185
|
-
a = A()
|
|
183
|
+
a = A.C()
|
|
186
184
|
assert a.x == 2, "Constant value not set"
|
|
187
185
|
|
|
188
186
|
# We should not be able to change the value
|
|
@@ -202,7 +200,7 @@ class EnumConfig(Config):
|
|
|
202
200
|
def test_param_enum():
|
|
203
201
|
"""Test for enum values"""
|
|
204
202
|
|
|
205
|
-
a = EnumConfig(x=EnumParam.OTHER)
|
|
203
|
+
a = EnumConfig.C(x=EnumParam.OTHER)
|
|
206
204
|
_a = serializeCycle(a)
|
|
207
205
|
|
|
208
206
|
assert isinstance(_a, EnumConfig)
|
|
@@ -214,9 +212,9 @@ def test_inheritance():
|
|
|
214
212
|
x: Param[int]
|
|
215
213
|
|
|
216
214
|
class B(A):
|
|
217
|
-
y: Param[int] = 3
|
|
215
|
+
y: Param[int] = field(ignore_default=3)
|
|
218
216
|
|
|
219
|
-
b = B()
|
|
217
|
+
b = B.C()
|
|
220
218
|
b.x = 2
|
|
221
219
|
assert b.__xpm__.values["y"] == 3
|
|
222
220
|
assert b.__xpm__.values["x"] == 2
|
|
@@ -227,7 +225,7 @@ def test_redefined_param():
|
|
|
227
225
|
x: Param[int]
|
|
228
226
|
|
|
229
227
|
class B(Config):
|
|
230
|
-
x: Param[int] = 3
|
|
228
|
+
x: Param[int] = field(ignore_default=3)
|
|
231
229
|
|
|
232
230
|
atx = A.C.__getxpmtype__().getArgument("x")
|
|
233
231
|
btx = B.C.__getxpmtype__().getArgument("x")
|
|
@@ -247,7 +245,7 @@ def test_param_dict():
|
|
|
247
245
|
assert isinstance(xarg.keytype, StrType)
|
|
248
246
|
assert isinstance(xarg.valuetype, IntType)
|
|
249
247
|
|
|
250
|
-
A(x={"OK": 1})
|
|
248
|
+
A.C(x={"OK": 1})
|
|
251
249
|
|
|
252
250
|
with pytest.raises(TypeError):
|
|
253
251
|
A(x={"wrong": "string"})
|
|
@@ -263,7 +261,7 @@ class ConfigWithDefault(Config):
|
|
|
263
261
|
|
|
264
262
|
|
|
265
263
|
def test_param_default():
|
|
266
|
-
assert ConfigWithDefault().x == 1
|
|
264
|
+
assert ConfigWithDefault.C().x == 1
|
|
267
265
|
|
|
268
266
|
|
|
269
267
|
class ConfigWithDefaultFactory(Config):
|
|
@@ -271,7 +269,7 @@ class ConfigWithDefaultFactory(Config):
|
|
|
271
269
|
|
|
272
270
|
|
|
273
271
|
def test_param_default_factory():
|
|
274
|
-
value = ConfigWithDefaultFactory()
|
|
272
|
+
value = ConfigWithDefaultFactory.C()
|
|
275
273
|
context = DirectoryContext(Path("/__fakepath__"))
|
|
276
274
|
value.__xpm__.seal(context)
|
|
277
275
|
assert value.x == 1
|
|
@@ -284,7 +282,7 @@ def test_default_mismatch():
|
|
|
284
282
|
"""Test mismatch between default and type"""
|
|
285
283
|
|
|
286
284
|
class A(Config):
|
|
287
|
-
x: Param[int] = 0.2
|
|
285
|
+
x: Param[int] = field(ignore_default=0.2)
|
|
288
286
|
|
|
289
287
|
with pytest.raises(TypeError):
|
|
290
288
|
A.__getxpmtype__().getArgument("x")
|
|
@@ -297,17 +295,17 @@ def test_param_default_set():
|
|
|
297
295
|
"""Test that the default setting is well set"""
|
|
298
296
|
|
|
299
297
|
class A0(Config):
|
|
300
|
-
x: Param[int] = 2
|
|
298
|
+
x: Param[int] = field(ignore_default=2)
|
|
301
299
|
|
|
302
|
-
assert A0().instance().x == 2
|
|
303
|
-
assert A0(x=3).instance().x == 3
|
|
300
|
+
assert A0.C().instance().x == 2
|
|
301
|
+
assert A0.C(x=3).instance().x == 3
|
|
304
302
|
|
|
305
303
|
class A(Config):
|
|
306
304
|
x: Param[int] = field(default_factory=lambda: 2)
|
|
307
305
|
|
|
308
|
-
assert A().instance().x == 2
|
|
306
|
+
assert A.C().instance().x == 2
|
|
309
307
|
|
|
310
|
-
assert A(x=3).instance().x == 3
|
|
308
|
+
assert A.C(x=3).instance().x == 3
|
|
311
309
|
|
|
312
310
|
|
|
313
311
|
# --- Handling help annotations
|
|
@@ -336,6 +334,4 @@ def test_help():
|
|
|
336
334
|
assert xpmtype.description.strip() == "Long description of A."
|
|
337
335
|
assert xpmtype.arguments["y"].help == "Parameter y"
|
|
338
336
|
|
|
339
|
-
|
|
340
|
-
if sys.version_info.major == 3 and sys.version_info.minor > 8:
|
|
341
|
-
assert xpmtype.arguments["x"].help == "Parameter x"
|
|
337
|
+
assert xpmtype.arguments["x"].help == "Parameter x"
|