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
experimaestro/tests/test_tags.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Dict
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
import logging
|
|
3
4
|
from experimaestro import (
|
|
4
5
|
tag,
|
|
5
6
|
LightweightTask,
|
|
@@ -21,20 +22,20 @@ class Config2(Config):
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def test_tag():
|
|
24
|
-
c = Config1(x=5)
|
|
25
|
+
c = Config1.C(x=5)
|
|
25
26
|
c.tag("x", 5)
|
|
26
27
|
assert c.tags() == {"x": 5}
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def test_taggedvalue():
|
|
30
|
-
c = Config1(x=tag(5))
|
|
31
|
+
c = Config1.C(x=tag(5))
|
|
31
32
|
assert c.tags() == {"x": 5}
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def test_tagcontain():
|
|
35
36
|
"""Test that tags are not propagated to the upper configurations"""
|
|
36
|
-
c1 = Config1(x=5)
|
|
37
|
-
c2 = Config2(c=c1, x=tag(3)).tag("out", 1)
|
|
37
|
+
c1 = Config1.C(x=5)
|
|
38
|
+
c2 = Config2.C(c=c1, x=tag(3)).tag("out", 1)
|
|
38
39
|
assert c1.tags() == {}
|
|
39
40
|
assert c2.tags() == {"x": 3, "out": 1}
|
|
40
41
|
|
|
@@ -50,17 +51,17 @@ def test_inneroutput():
|
|
|
50
51
|
class Evaluate(Task):
|
|
51
52
|
task: Param[MyTask]
|
|
52
53
|
|
|
53
|
-
output = Output().tag("hello", "world")
|
|
54
|
-
task = MyTask(outputs={}, mainoutput=output)
|
|
54
|
+
output = Output.C().tag("hello", "world")
|
|
55
|
+
task = MyTask.C(outputs={}, mainoutput=output)
|
|
55
56
|
task.submit(run_mode=RunMode.DRY_RUN)
|
|
56
57
|
assert output.tags() == {"hello": "world"}
|
|
57
58
|
|
|
58
|
-
output = Output().tag("hello", "world")
|
|
59
|
-
task = MyTask(outputs={"a": output}, mainoutput=Output())
|
|
59
|
+
output = Output.C().tag("hello", "world")
|
|
60
|
+
task = MyTask.C(outputs={"a": output}, mainoutput=Output.C())
|
|
60
61
|
task.submit(run_mode=RunMode.DRY_RUN)
|
|
61
62
|
assert output.tags() == {"hello": "world"}
|
|
62
63
|
|
|
63
|
-
evaluate = Evaluate(task=task).submit(run_mode=RunMode.DRY_RUN)
|
|
64
|
+
evaluate = Evaluate.C(task=task).submit(run_mode=RunMode.DRY_RUN)
|
|
64
65
|
assert evaluate.__xpm__.tags() == {"hello": "world"}
|
|
65
66
|
|
|
66
67
|
|
|
@@ -80,21 +81,21 @@ def test_tags_init_tasks():
|
|
|
80
81
|
x: Param[MyConfig]
|
|
81
82
|
|
|
82
83
|
def task_outputs(self, dep) -> MyConfig:
|
|
83
|
-
return dep(MyConfig())
|
|
84
|
+
return dep(MyConfig.C())
|
|
84
85
|
|
|
85
|
-
init_task = InitTask().tag("hello", "world")
|
|
86
|
-
task = MyTask()
|
|
86
|
+
init_task = InitTask.C().tag("hello", "world")
|
|
87
|
+
task = MyTask.C()
|
|
87
88
|
result = task.submit(run_mode=RunMode.DRY_RUN, init_tasks=[init_task])
|
|
88
89
|
assert result.tags() == {"hello": "world"}
|
|
89
90
|
|
|
90
|
-
other_task = TaskWithOutput(x=MyConfig().tag("hello", "world"))
|
|
91
|
+
other_task = TaskWithOutput.C(x=MyConfig.C().tag("hello", "world"))
|
|
91
92
|
assert other_task.tags() == {"hello": "world"}
|
|
92
93
|
|
|
93
94
|
result = other_task.submit(run_mode=RunMode.DRY_RUN)
|
|
94
95
|
assert isinstance(result, MyConfig)
|
|
95
96
|
assert result.tags() == {"hello": "world"}
|
|
96
97
|
|
|
97
|
-
result = MyTask().submit(run_mode=RunMode.DRY_RUN, init_tasks=[result])
|
|
98
|
+
result = MyTask.C().submit(run_mode=RunMode.DRY_RUN, init_tasks=[result])
|
|
98
99
|
assert result.tags() == {"hello": "world"}
|
|
99
100
|
|
|
100
101
|
|
|
@@ -115,6 +116,141 @@ def test_objects_tags():
|
|
|
115
116
|
x: Param[int]
|
|
116
117
|
|
|
117
118
|
context = DirectoryContext(Path("/__fakepath__"))
|
|
118
|
-
a = A(x=tag(1))
|
|
119
|
+
a = A.C(x=tag(1))
|
|
119
120
|
a.__xpm__.seal(context)
|
|
120
121
|
assert a.__xpm__.tags() == {"x": 1}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_conflicting_tags_warning(caplog):
|
|
125
|
+
"""Test that conflicting tag values produce a warning"""
|
|
126
|
+
|
|
127
|
+
class Inner(Config):
|
|
128
|
+
value: Param[int]
|
|
129
|
+
|
|
130
|
+
class Outer(Config):
|
|
131
|
+
inner: Param[Inner]
|
|
132
|
+
x: Param[int]
|
|
133
|
+
|
|
134
|
+
# Create inner config with tag "mytag" = 1
|
|
135
|
+
inner = Inner.C(value=10).tag("mytag", 1)
|
|
136
|
+
|
|
137
|
+
# Create outer config with same tag "mytag" = 2 (conflicting)
|
|
138
|
+
outer = Outer.C(inner=inner, x=5).tag("mytag", 2)
|
|
139
|
+
|
|
140
|
+
# Getting tags should warn about conflict
|
|
141
|
+
with caplog.at_level(logging.WARNING):
|
|
142
|
+
tags = outer.tags()
|
|
143
|
+
|
|
144
|
+
# The warning should mention the conflicting tag
|
|
145
|
+
assert any("mytag" in record.message for record in caplog.records)
|
|
146
|
+
assert any("conflicting" in record.message.lower() for record in caplog.records)
|
|
147
|
+
|
|
148
|
+
# The last value should win
|
|
149
|
+
assert tags["mytag"] == 2
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_same_tag_same_value_no_warning(caplog):
|
|
153
|
+
"""Test that same tag with same value does not produce a warning"""
|
|
154
|
+
|
|
155
|
+
class Inner(Config):
|
|
156
|
+
value: Param[int]
|
|
157
|
+
|
|
158
|
+
class Outer(Config):
|
|
159
|
+
inner: Param[Inner]
|
|
160
|
+
|
|
161
|
+
# Create inner config with tag "mytag" = 1
|
|
162
|
+
inner = Inner.C(value=10).tag("mytag", 1)
|
|
163
|
+
|
|
164
|
+
# Create outer config with same tag "mytag" = 1 (same value)
|
|
165
|
+
outer = Outer.C(inner=inner).tag("mytag", 1)
|
|
166
|
+
|
|
167
|
+
# Getting tags should NOT warn (same value)
|
|
168
|
+
with caplog.at_level(logging.WARNING):
|
|
169
|
+
tags = outer.tags()
|
|
170
|
+
|
|
171
|
+
# No warning for same values
|
|
172
|
+
assert not any("mytag" in record.message for record in caplog.records)
|
|
173
|
+
assert tags["mytag"] == 1
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_tag_source_tracking():
|
|
177
|
+
"""Test that tag source locations are tracked"""
|
|
178
|
+
|
|
179
|
+
class MyConfig(Config):
|
|
180
|
+
x: Param[int]
|
|
181
|
+
|
|
182
|
+
config = MyConfig.C(x=tag(5))
|
|
183
|
+
|
|
184
|
+
# Check that tags have source info stored internally
|
|
185
|
+
assert "x" in config.__xpm__._tags
|
|
186
|
+
value, source = config.__xpm__._tags["x"]
|
|
187
|
+
assert value == 5
|
|
188
|
+
# Source should contain file path and line number
|
|
189
|
+
assert ":" in source
|
|
190
|
+
assert "test_tags.py" in source
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_tag_method_source_tracking():
|
|
194
|
+
"""Test that tag() method also tracks source location"""
|
|
195
|
+
|
|
196
|
+
class MyConfig(Config):
|
|
197
|
+
x: Param[int]
|
|
198
|
+
|
|
199
|
+
config = MyConfig.C(x=5)
|
|
200
|
+
config.tag("mytag", "myvalue")
|
|
201
|
+
|
|
202
|
+
# Check that tag has source info
|
|
203
|
+
assert "mytag" in config.__xpm__._tags
|
|
204
|
+
value, source = config.__xpm__._tags["mytag"]
|
|
205
|
+
assert value == "myvalue"
|
|
206
|
+
assert ":" in source
|
|
207
|
+
assert "test_tags.py" in source
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_tag_via_setattr():
|
|
211
|
+
"""Test that config.key = tag(value) works and tracks source"""
|
|
212
|
+
|
|
213
|
+
class MyConfig(Config):
|
|
214
|
+
x: Param[int]
|
|
215
|
+
|
|
216
|
+
config = MyConfig.C(x=5)
|
|
217
|
+
config.x = tag(10)
|
|
218
|
+
|
|
219
|
+
# Check that tag was set correctly
|
|
220
|
+
assert config.tags() == {"x": 10}
|
|
221
|
+
assert config.x == 10
|
|
222
|
+
|
|
223
|
+
# Check that source is tracked
|
|
224
|
+
value, source = config.__xpm__._tags["x"]
|
|
225
|
+
assert value == 10
|
|
226
|
+
assert "test_tags.py" in source
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_tag_setattr_conflict_warning(caplog):
|
|
230
|
+
"""Test that setting conflicting tag via setattr produces warning"""
|
|
231
|
+
|
|
232
|
+
class Inner(Config):
|
|
233
|
+
value: Param[int]
|
|
234
|
+
|
|
235
|
+
class Outer(Config):
|
|
236
|
+
inner: Param[Inner]
|
|
237
|
+
x: Param[int]
|
|
238
|
+
|
|
239
|
+
# Create with tag via constructor
|
|
240
|
+
inner = Inner.C(value=tag(1))
|
|
241
|
+
|
|
242
|
+
# Create outer with same tag name
|
|
243
|
+
outer = Outer.C(inner=inner, x=5)
|
|
244
|
+
outer.x = tag(2) # Set tag on x
|
|
245
|
+
|
|
246
|
+
# Add a conflicting value tag
|
|
247
|
+
outer.tag("value", 99)
|
|
248
|
+
|
|
249
|
+
# Getting tags should warn about conflict
|
|
250
|
+
with caplog.at_level(logging.WARNING):
|
|
251
|
+
tags = outer.tags()
|
|
252
|
+
|
|
253
|
+
# The warning should mention the conflicting tag
|
|
254
|
+
assert any("value" in record.message for record in caplog.records)
|
|
255
|
+
assert tags["value"] == 99 # Last value wins
|
|
256
|
+
assert tags["x"] == 2
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# --- Task and types definitions
|
|
2
2
|
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
import pytest
|
|
5
7
|
import logging
|
|
6
|
-
from experimaestro import
|
|
8
|
+
from experimaestro import (
|
|
9
|
+
Config,
|
|
10
|
+
Task,
|
|
11
|
+
Param,
|
|
12
|
+
ResumableTask,
|
|
13
|
+
Meta,
|
|
14
|
+
field,
|
|
15
|
+
PathGenerator,
|
|
16
|
+
)
|
|
7
17
|
from experimaestro.scheduler.workspace import RunMode
|
|
8
|
-
from experimaestro.tools.jobs import fix_deprecated
|
|
9
18
|
from experimaestro.scheduler import FailedExperiment, JobState
|
|
10
19
|
from experimaestro import SubmitHook, Job, Launcher, LightweightTask
|
|
11
20
|
|
|
@@ -29,8 +38,8 @@ from .definitions_types import IntegerTask, FloatTask
|
|
|
29
38
|
|
|
30
39
|
def test_task_types():
|
|
31
40
|
with TemporaryExperiment("simple"):
|
|
32
|
-
assert IntegerTask(value=5).submit().__xpm__.job.wait() == JobState.DONE
|
|
33
|
-
assert FloatTask(value=5.1).submit().__xpm__.job.wait() == JobState.DONE
|
|
41
|
+
assert IntegerTask.C(value=5).submit().__xpm__.job.wait() == JobState.DONE
|
|
42
|
+
assert FloatTask.C(value=5.1).submit().__xpm__.job.wait() == JobState.DONE
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
def test_simple_task():
|
|
@@ -38,11 +47,11 @@ def test_simple_task():
|
|
|
38
47
|
assert isinstance(workdir, Path)
|
|
39
48
|
with TemporaryExperiment("helloworld", workdir=workdir, maxwait=20):
|
|
40
49
|
# Submit the tasks
|
|
41
|
-
hello = Say(word="hello").submit()
|
|
42
|
-
world = Say(word="world").submit()
|
|
50
|
+
hello = Say.C(word="hello").submit()
|
|
51
|
+
world = Say.C(word="world").submit()
|
|
43
52
|
|
|
44
53
|
# Concat will depend on the two first tasks
|
|
45
|
-
concat = Concat(strings=[hello, world]).submit()
|
|
54
|
+
concat = Concat.C(strings=[hello, world]).submit()
|
|
46
55
|
|
|
47
56
|
assert concat.__xpm__.job.state == JobState.DONE
|
|
48
57
|
assert Path(concat.stdout()).read_text() == "HELLO WORLD\n"
|
|
@@ -51,16 +60,16 @@ def test_simple_task():
|
|
|
51
60
|
def test_not_submitted():
|
|
52
61
|
"""A not submitted task should not be accepted as an argument"""
|
|
53
62
|
with TemporaryExperiment("helloworld", maxwait=2):
|
|
54
|
-
hello = Say(word="hello")
|
|
63
|
+
hello = Say.C(word="hello")
|
|
55
64
|
with pytest.raises(ValueError):
|
|
56
|
-
Concat(strings=[hello])
|
|
65
|
+
Concat.C(strings=[hello])
|
|
57
66
|
|
|
58
67
|
|
|
59
68
|
def test_fail_simple():
|
|
60
69
|
"""Failing task... should fail"""
|
|
61
70
|
with pytest.raises(FailedExperiment):
|
|
62
71
|
with TemporaryExperiment("failing", maxwait=20):
|
|
63
|
-
fail = Fail().submit()
|
|
72
|
+
fail = Fail.C().submit()
|
|
64
73
|
fail.touch()
|
|
65
74
|
|
|
66
75
|
|
|
@@ -70,8 +79,8 @@ def test_foreign_type():
|
|
|
70
79
|
# Submit the tasks
|
|
71
80
|
from .tasks.foreign import ForeignClassB2
|
|
72
81
|
|
|
73
|
-
b = ForeignClassB2(x=1, y=2)
|
|
74
|
-
a = ForeignTaskA(b=b).submit()
|
|
82
|
+
b = ForeignClassB2.C(x=1, y=2)
|
|
83
|
+
a = ForeignTaskA.C(b=b).submit()
|
|
75
84
|
|
|
76
85
|
assert a.__xpm__.job.wait() == JobState.DONE
|
|
77
86
|
assert a.stdout().read_text().strip() == "1"
|
|
@@ -81,8 +90,8 @@ def test_fail_dep():
|
|
|
81
90
|
"""Failing task... should cancel dependent"""
|
|
82
91
|
with pytest.raises(FailedExperiment):
|
|
83
92
|
with TemporaryExperiment("failingdep"):
|
|
84
|
-
fail = Fail().submit()
|
|
85
|
-
dep = FailConsumer(fail=fail).submit()
|
|
93
|
+
fail = Fail.C().submit()
|
|
94
|
+
dep = FailConsumer.C(fail=fail).submit()
|
|
86
95
|
fail.touch()
|
|
87
96
|
|
|
88
97
|
assert fail.__xpm__.job.state == JobState.ERROR
|
|
@@ -92,14 +101,14 @@ def test_fail_dep():
|
|
|
92
101
|
def test_unknown_attribute():
|
|
93
102
|
"""No check when setting attributes while executing"""
|
|
94
103
|
with TemporaryExperiment("unknown"):
|
|
95
|
-
method = SetUnknown().submit()
|
|
104
|
+
method = SetUnknown.C().submit()
|
|
96
105
|
|
|
97
106
|
assert method.__xpm__.job.wait() == JobState.DONE
|
|
98
107
|
|
|
99
108
|
|
|
100
109
|
def test_function():
|
|
101
110
|
with TemporaryExperiment("function"):
|
|
102
|
-
method = Method(a=1).submit()
|
|
111
|
+
method = Method.C(a=1).submit()
|
|
103
112
|
|
|
104
113
|
assert method.__xpm__.job.wait() == JobState.DONE
|
|
105
114
|
|
|
@@ -111,7 +120,7 @@ def test_done():
|
|
|
111
120
|
|
|
112
121
|
|
|
113
122
|
def restart_function(xp):
|
|
114
|
-
restart.Restart().submit()
|
|
123
|
+
restart.Restart.C().submit()
|
|
115
124
|
|
|
116
125
|
|
|
117
126
|
@pytest.mark.parametrize("terminate", restart.TERMINATES_FUNC)
|
|
@@ -123,135 +132,27 @@ def test_restart(terminate):
|
|
|
123
132
|
def test_submitted_twice():
|
|
124
133
|
"""Check that a job cannot be submitted twice within the same experiment"""
|
|
125
134
|
with TemporaryExperiment("duplicate", maxwait=20):
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
|
|
136
|
+
task1 = SimpleTask.C(x=1)
|
|
137
|
+
o1 = task1.submit()
|
|
138
|
+
|
|
139
|
+
task2 = SimpleTask.C(x=1)
|
|
140
|
+
o2 = task2.submit()
|
|
141
|
+
|
|
142
|
+
print(o1)
|
|
143
|
+
assert o1.task is not o2.task
|
|
144
|
+
assert task1.__xpm__.job is task2.__xpm__.job, f"{id(task1)} != {id(task2)}"
|
|
129
145
|
|
|
130
146
|
|
|
131
147
|
def test_configcache():
|
|
132
148
|
"""Test a configuration cache"""
|
|
133
149
|
|
|
134
150
|
with TemporaryExperiment("configcache", maxwait=20):
|
|
135
|
-
task = CacheConfigTask(data=CacheConfig()).submit()
|
|
151
|
+
task = CacheConfigTask.C(data=CacheConfig.C()).submit()
|
|
136
152
|
|
|
137
153
|
assert task.__xpm__.job.wait() == JobState.DONE
|
|
138
154
|
|
|
139
155
|
|
|
140
|
-
# ---- Deprecation
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
class NewConfig(Config):
|
|
144
|
-
__xpmid__ = "new"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@deprecate
|
|
148
|
-
class DeprecatedConfig(NewConfig):
|
|
149
|
-
__xpmid__ = "deprecated"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class OldConfig(NewConfig):
|
|
153
|
-
__xpmid__ = "deprecated"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class TaskWithDeprecated(Task):
|
|
157
|
-
p: Param[NewConfig]
|
|
158
|
-
|
|
159
|
-
def execute(self):
|
|
160
|
-
pass
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def checknewpaths(task_new, task_old_path):
|
|
164
|
-
task_new_path = task_new.__xpm__.job.path # type: Path
|
|
165
|
-
|
|
166
|
-
assert task_new_path.exists(), f"New path {task_new_path} should exist"
|
|
167
|
-
assert task_new_path.is_symlink(), f"New path {task_new_path} should be a symlink"
|
|
168
|
-
|
|
169
|
-
assert task_new_path.resolve() == task_old_path
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def test_tasks_deprecated_inner():
|
|
173
|
-
"""Test that when submitting the task, the computed identifier is the one of
|
|
174
|
-
the new class"""
|
|
175
|
-
with TemporaryExperiment("deprecated", maxwait=0) as xp:
|
|
176
|
-
# --- Check that paths are really different first
|
|
177
|
-
task_new = TaskWithDeprecated(p=NewConfig()).submit(run_mode=RunMode.DRY_RUN)
|
|
178
|
-
task_old = TaskWithDeprecated(p=OldConfig()).submit(run_mode=RunMode.DRY_RUN)
|
|
179
|
-
task_deprecated = TaskWithDeprecated(p=DeprecatedConfig()).submit(
|
|
180
|
-
run_mode=RunMode.DRY_RUN
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
logging.debug("New task ID: %s", task_new.__xpm__.identifier.all.hex())
|
|
184
|
-
logging.debug("Old task ID: %s", task_old.__xpm__.identifier.all.hex())
|
|
185
|
-
logging.debug(
|
|
186
|
-
"Old task (with deprecated flag): %s",
|
|
187
|
-
task_deprecated.__xpm__.identifier.all.hex(),
|
|
188
|
-
)
|
|
189
|
-
assert (
|
|
190
|
-
task_new.stdout() != task_old.stdout()
|
|
191
|
-
), "Old and new path should be different"
|
|
192
|
-
|
|
193
|
-
assert (
|
|
194
|
-
task_new.stdout() == task_deprecated.stdout()
|
|
195
|
-
), "Deprecated path should be the same as non deprecated"
|
|
196
|
-
|
|
197
|
-
# --- Now check that automatic linking is performed
|
|
198
|
-
|
|
199
|
-
# Run old task with deprecated configuration
|
|
200
|
-
task_old = TaskWithDeprecated(p=OldConfig()).submit()
|
|
201
|
-
task_old.wait()
|
|
202
|
-
task_old_path = task_old.stdout().parent
|
|
203
|
-
|
|
204
|
-
# Fix deprecated
|
|
205
|
-
OldConfig.__xpmtype__.deprecate()
|
|
206
|
-
fix_deprecated(xp.workspace.path, True, False)
|
|
207
|
-
|
|
208
|
-
checknewpaths(task_new, task_old_path)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class NewTask(Task):
|
|
212
|
-
x: Param[int]
|
|
213
|
-
|
|
214
|
-
def execute(self):
|
|
215
|
-
pass
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
class OldTask(NewTask):
|
|
219
|
-
__xpmid__ = "deprecated"
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@deprecate
|
|
223
|
-
class DeprecatedTask(NewTask):
|
|
224
|
-
__xpmid__ = "deprecated"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def test_tasks_deprecated():
|
|
228
|
-
"""Test that when submitting the task, the computed identifier is the one of
|
|
229
|
-
the new class"""
|
|
230
|
-
with TemporaryExperiment("deprecated", maxwait=20) as xp:
|
|
231
|
-
# Check that paths are really different first
|
|
232
|
-
task_new = NewTask(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
233
|
-
task_old = OldTask(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
234
|
-
task_deprecated = DeprecatedTask(x=1).submit(run_mode=RunMode.DRY_RUN)
|
|
235
|
-
|
|
236
|
-
assert (
|
|
237
|
-
task_new.stdout() != task_old.stdout()
|
|
238
|
-
), "Old and new path should be different"
|
|
239
|
-
assert (
|
|
240
|
-
task_new.stdout() == task_deprecated.stdout()
|
|
241
|
-
), "Deprecated path should be the same as non deprecated"
|
|
242
|
-
|
|
243
|
-
# OK, now check that automatic linking is performed
|
|
244
|
-
task_old = OldTask(x=1).submit()
|
|
245
|
-
task_old.wait()
|
|
246
|
-
task_old_path = task_old.stdout().parent
|
|
247
|
-
|
|
248
|
-
# Fix deprecated
|
|
249
|
-
OldTask.__xpmtype__.deprecate()
|
|
250
|
-
fix_deprecated(xp.workspace.path, True, False)
|
|
251
|
-
|
|
252
|
-
checknewpaths(task_new, task_old_path)
|
|
253
|
-
|
|
254
|
-
|
|
255
156
|
class needs_java(SubmitHook):
|
|
256
157
|
def __init__(self, version: int):
|
|
257
158
|
self.version = version
|
|
@@ -270,7 +171,7 @@ class HookedTask(Task):
|
|
|
270
171
|
|
|
271
172
|
|
|
272
173
|
def test_task_submit_hook():
|
|
273
|
-
result = HookedTask().submit(run_mode=RunMode.DRY_RUN)
|
|
174
|
+
result = HookedTask.C().submit(run_mode=RunMode.DRY_RUN)
|
|
274
175
|
assert (
|
|
275
176
|
result.__xpm__.task.__xpm__.job.environ.get("JAVA_HOME", None)
|
|
276
177
|
== "THE_JAVA_HOME"
|
|
@@ -299,31 +200,107 @@ class MyLightweightTask(Task):
|
|
|
299
200
|
assert self.x.data == 1
|
|
300
201
|
|
|
301
202
|
|
|
302
|
-
def test_task_lightweight():
|
|
303
|
-
with TemporaryExperiment("lightweight", maxwait=20):
|
|
304
|
-
x = LightweightConfig()
|
|
305
|
-
lwtask = LightweightTask(x=x)
|
|
306
|
-
assert (
|
|
307
|
-
MyLightweightTask(x=x).add_pretasks(lwtask).submit().__xpm__.job.wait()
|
|
308
|
-
== JobState.DONE
|
|
309
|
-
), "Pre-tasks should be executed"
|
|
310
|
-
|
|
311
|
-
x_2 = LightweightConfig()
|
|
312
|
-
lwtask_2 = LightweightTask(x=x)
|
|
313
|
-
assert (
|
|
314
|
-
MyLightweightTask(x=x_2.add_pretasks(lwtask_2))
|
|
315
|
-
.add_pretasks(lwtask_2)
|
|
316
|
-
.submit()
|
|
317
|
-
.__xpm__.job.wait()
|
|
318
|
-
== JobState.DONE
|
|
319
|
-
), "Pre-tasks should be run just once"
|
|
320
|
-
|
|
321
|
-
|
|
322
203
|
def test_task_lightweight_init():
|
|
323
204
|
with TemporaryExperiment("lightweight_init", maxwait=20):
|
|
324
|
-
x = LightweightConfig()
|
|
325
|
-
lwtask = LightweightTask(x=x)
|
|
205
|
+
x = LightweightConfig.C()
|
|
206
|
+
lwtask = LightweightTask.C(x=x)
|
|
326
207
|
assert (
|
|
327
|
-
MyLightweightTask(x=x).submit(init_tasks=[lwtask]).__xpm__.job.wait()
|
|
208
|
+
MyLightweightTask.C(x=x).submit(init_tasks=[lwtask]).__xpm__.job.wait()
|
|
328
209
|
== JobState.DONE
|
|
329
210
|
), "Init tasks should be executed"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --- Test for resumable task resubmission
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ControllableResumableTask(ResumableTask):
|
|
217
|
+
"""A resumable task that can be controlled via files"""
|
|
218
|
+
|
|
219
|
+
control_file: Meta[Path] = field(default_factory=PathGenerator("control"))
|
|
220
|
+
|
|
221
|
+
def execute(self):
|
|
222
|
+
# Wait for control file
|
|
223
|
+
while not self.control_file.is_file():
|
|
224
|
+
time.sleep(0.1)
|
|
225
|
+
|
|
226
|
+
# Read control: "fail" to exit with error, "complete" to succeed
|
|
227
|
+
action = self.control_file.read_text().strip()
|
|
228
|
+
self.control_file.unlink()
|
|
229
|
+
|
|
230
|
+
if action == "fail":
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_resumable_task_resubmit():
|
|
235
|
+
"""Test resubmitting a failed ResumableTask within the same experiment"""
|
|
236
|
+
with TemporaryExperiment("resumable_resubmit", maxwait=30):
|
|
237
|
+
task1 = ControllableResumableTask.C()
|
|
238
|
+
task1.submit()
|
|
239
|
+
|
|
240
|
+
# Tell task to fail
|
|
241
|
+
task1.control_file.parent.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
task1.control_file.write_text("fail")
|
|
243
|
+
|
|
244
|
+
# Wait for the job to fail
|
|
245
|
+
job = task1.__xpm__.job
|
|
246
|
+
assert job.wait() == JobState.ERROR, "Job should have failed"
|
|
247
|
+
|
|
248
|
+
# Resubmit by creating a new instance with same parameters
|
|
249
|
+
task2 = ControllableResumableTask.C()
|
|
250
|
+
task2.submit()
|
|
251
|
+
|
|
252
|
+
# Tell task to complete
|
|
253
|
+
task2.control_file.write_text("complete")
|
|
254
|
+
|
|
255
|
+
# Wait for the resubmitted job to complete
|
|
256
|
+
assert task2.__xpm__.job.wait() == JobState.DONE
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_resumable_task_resubmit_across_experiments():
|
|
260
|
+
"""Test resubmitting a failed ResumableTask across two experiment instances"""
|
|
261
|
+
with TemporaryDirectory(prefix="xpm", suffix="resubmit_across") as workdir:
|
|
262
|
+
# First experiment: task fails
|
|
263
|
+
try:
|
|
264
|
+
with TemporaryExperiment("resubmit_across", maxwait=10, workdir=workdir):
|
|
265
|
+
task1 = ControllableResumableTask.C()
|
|
266
|
+
task1.submit()
|
|
267
|
+
|
|
268
|
+
# Tell task to fail
|
|
269
|
+
task1.control_file.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
task1.control_file.write_text("fail")
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logging.info("First experiment ended (expected): %s", e)
|
|
273
|
+
|
|
274
|
+
# Second experiment: task completes
|
|
275
|
+
with TemporaryExperiment("resubmit_across", maxwait=30, workdir=workdir):
|
|
276
|
+
task2 = ControllableResumableTask.C()
|
|
277
|
+
task2.submit()
|
|
278
|
+
|
|
279
|
+
# Tell task to complete
|
|
280
|
+
task2.control_file.write_text("complete")
|
|
281
|
+
|
|
282
|
+
# Wait for the resubmitted job to complete
|
|
283
|
+
assert task2.__xpm__.job.wait() == JobState.DONE
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_task_resubmit_across_experiments():
|
|
287
|
+
"""Test resubmitting a completed task across two experiment instances"""
|
|
288
|
+
with TemporaryDirectory(prefix="xpm", suffix="resubmit_across") as workdir:
|
|
289
|
+
# First experiment: task completes
|
|
290
|
+
with TemporaryExperiment("resubmit_across", maxwait=30, workdir=workdir):
|
|
291
|
+
task1 = ControllableResumableTask.C()
|
|
292
|
+
task1.submit()
|
|
293
|
+
|
|
294
|
+
# Tell task to complete
|
|
295
|
+
task1.control_file.parent.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
task1.control_file.write_text("complete")
|
|
297
|
+
|
|
298
|
+
assert task1.__xpm__.job.wait() == JobState.DONE
|
|
299
|
+
|
|
300
|
+
# Second experiment: resubmit completed task (uses same workdir)
|
|
301
|
+
with TemporaryExperiment("resubmit_across", maxwait=30, workdir=workdir):
|
|
302
|
+
task2 = ControllableResumableTask.C()
|
|
303
|
+
task2.submit()
|
|
304
|
+
|
|
305
|
+
# Task should recognize it's already done
|
|
306
|
+
assert task2.__xpm__.job.wait() == JobState.DONE
|