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.

Files changed (133) hide show
  1. experimaestro/__init__.py +10 -11
  2. experimaestro/annotations.py +167 -206
  3. experimaestro/cli/__init__.py +140 -16
  4. experimaestro/cli/filter.py +42 -74
  5. experimaestro/cli/jobs.py +157 -106
  6. experimaestro/cli/progress.py +269 -0
  7. experimaestro/cli/refactor.py +249 -0
  8. experimaestro/click.py +0 -1
  9. experimaestro/commandline.py +19 -3
  10. experimaestro/connectors/__init__.py +22 -3
  11. experimaestro/connectors/local.py +12 -0
  12. experimaestro/core/arguments.py +192 -37
  13. experimaestro/core/identifier.py +127 -12
  14. experimaestro/core/objects/__init__.py +6 -0
  15. experimaestro/core/objects/config.py +702 -285
  16. experimaestro/core/objects/config_walk.py +24 -6
  17. experimaestro/core/serialization.py +91 -34
  18. experimaestro/core/serializers.py +1 -8
  19. experimaestro/core/subparameters.py +164 -0
  20. experimaestro/core/types.py +198 -83
  21. experimaestro/exceptions.py +26 -0
  22. experimaestro/experiments/cli.py +107 -25
  23. experimaestro/generators.py +50 -9
  24. experimaestro/huggingface.py +3 -1
  25. experimaestro/launcherfinder/parser.py +29 -0
  26. experimaestro/launcherfinder/registry.py +3 -3
  27. experimaestro/launchers/__init__.py +26 -1
  28. experimaestro/launchers/direct.py +12 -0
  29. experimaestro/launchers/slurm/base.py +154 -2
  30. experimaestro/mkdocs/base.py +6 -8
  31. experimaestro/mkdocs/metaloader.py +0 -1
  32. experimaestro/mypy.py +452 -7
  33. experimaestro/notifications.py +75 -16
  34. experimaestro/progress.py +404 -0
  35. experimaestro/rpyc.py +0 -1
  36. experimaestro/run.py +19 -6
  37. experimaestro/scheduler/__init__.py +18 -1
  38. experimaestro/scheduler/base.py +504 -959
  39. experimaestro/scheduler/dependencies.py +43 -28
  40. experimaestro/scheduler/dynamic_outputs.py +259 -130
  41. experimaestro/scheduler/experiment.py +582 -0
  42. experimaestro/scheduler/interfaces.py +474 -0
  43. experimaestro/scheduler/jobs.py +485 -0
  44. experimaestro/scheduler/services.py +186 -12
  45. experimaestro/scheduler/signal_handler.py +32 -0
  46. experimaestro/scheduler/state.py +1 -1
  47. experimaestro/scheduler/state_db.py +388 -0
  48. experimaestro/scheduler/state_provider.py +2345 -0
  49. experimaestro/scheduler/state_sync.py +834 -0
  50. experimaestro/scheduler/workspace.py +52 -10
  51. experimaestro/scriptbuilder.py +7 -0
  52. experimaestro/server/__init__.py +153 -32
  53. experimaestro/server/data/index.css +0 -125
  54. experimaestro/server/data/index.css.map +1 -1
  55. experimaestro/server/data/index.js +194 -58
  56. experimaestro/server/data/index.js.map +1 -1
  57. experimaestro/settings.py +47 -6
  58. experimaestro/sphinx/__init__.py +3 -3
  59. experimaestro/taskglobals.py +20 -0
  60. experimaestro/tests/conftest.py +80 -0
  61. experimaestro/tests/core/test_generics.py +2 -2
  62. experimaestro/tests/identifier_stability.json +45 -0
  63. experimaestro/tests/launchers/bin/sacct +6 -2
  64. experimaestro/tests/launchers/bin/sbatch +4 -2
  65. experimaestro/tests/launchers/common.py +2 -2
  66. experimaestro/tests/launchers/test_slurm.py +80 -0
  67. experimaestro/tests/restart.py +1 -1
  68. experimaestro/tests/tasks/all.py +7 -0
  69. experimaestro/tests/tasks/test_dynamic.py +231 -0
  70. experimaestro/tests/test_checkers.py +2 -2
  71. experimaestro/tests/test_cli_jobs.py +615 -0
  72. experimaestro/tests/test_dependencies.py +11 -17
  73. experimaestro/tests/test_deprecated.py +630 -0
  74. experimaestro/tests/test_environment.py +200 -0
  75. experimaestro/tests/test_experiment.py +3 -3
  76. experimaestro/tests/test_file_progress.py +425 -0
  77. experimaestro/tests/test_file_progress_integration.py +477 -0
  78. experimaestro/tests/test_forward.py +3 -3
  79. experimaestro/tests/test_generators.py +93 -0
  80. experimaestro/tests/test_identifier.py +520 -169
  81. experimaestro/tests/test_identifier_stability.py +458 -0
  82. experimaestro/tests/test_instance.py +16 -21
  83. experimaestro/tests/test_multitoken.py +442 -0
  84. experimaestro/tests/test_mypy.py +433 -0
  85. experimaestro/tests/test_objects.py +314 -30
  86. experimaestro/tests/test_outputs.py +8 -8
  87. experimaestro/tests/test_param.py +22 -26
  88. experimaestro/tests/test_partial_paths.py +231 -0
  89. experimaestro/tests/test_progress.py +2 -50
  90. experimaestro/tests/test_resumable_task.py +480 -0
  91. experimaestro/tests/test_serializers.py +141 -60
  92. experimaestro/tests/test_state_db.py +434 -0
  93. experimaestro/tests/test_subparameters.py +160 -0
  94. experimaestro/tests/test_tags.py +151 -15
  95. experimaestro/tests/test_tasks.py +137 -160
  96. experimaestro/tests/test_token_locking.py +252 -0
  97. experimaestro/tests/test_tokens.py +25 -19
  98. experimaestro/tests/test_types.py +133 -11
  99. experimaestro/tests/test_validation.py +19 -19
  100. experimaestro/tests/test_workspace_triggers.py +158 -0
  101. experimaestro/tests/token_reschedule.py +5 -3
  102. experimaestro/tests/utils.py +2 -2
  103. experimaestro/tokens.py +154 -57
  104. experimaestro/tools/diff.py +8 -1
  105. experimaestro/tui/__init__.py +8 -0
  106. experimaestro/tui/app.py +2303 -0
  107. experimaestro/tui/app.tcss +353 -0
  108. experimaestro/tui/log_viewer.py +228 -0
  109. experimaestro/typingutils.py +11 -2
  110. experimaestro/utils/__init__.py +23 -0
  111. experimaestro/utils/environment.py +148 -0
  112. experimaestro/utils/git.py +129 -0
  113. experimaestro/utils/resources.py +1 -1
  114. experimaestro/version.py +34 -0
  115. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/METADATA +70 -39
  116. experimaestro-2.0.0b4.dist-info/RECORD +181 -0
  117. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info}/WHEEL +1 -1
  118. experimaestro-2.0.0b4.dist-info/entry_points.txt +16 -0
  119. experimaestro/compat.py +0 -6
  120. experimaestro/core/objects.pyi +0 -225
  121. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  122. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  123. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  124. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  125. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  126. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  127. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  128. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  129. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  130. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  131. experimaestro-1.11.1.dist-info/RECORD +0 -158
  132. experimaestro-1.11.1.dist-info/entry_points.txt +0 -17
  133. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0b4.dist-info/licenses}/LICENSE +0 -0
@@ -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 Config, deprecate, Task, Param
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
- task1 = SimpleTask(x=1).submit()
127
- task2 = SimpleTask(x=1).submit()
128
- assert task1 is task2, f"{id(task1)} != {id(task2)}"
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