experimaestro 1.12.0__py3-none-any.whl → 1.14.0__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.

@@ -80,10 +80,12 @@ class Argument:
80
80
 
81
81
  self.generator = generator
82
82
  self.default = None
83
+ self.ignore_generated = False
83
84
 
84
85
  if default is not None:
85
86
  assert self.generator is None, "generator and default are exclusive options"
86
87
  if isinstance(default, field):
88
+ self.ignore_generated = default.ignore_generated
87
89
  if default.default is not None:
88
90
  self.default = default.default
89
91
  elif default.default_factory is not None:
@@ -184,13 +186,20 @@ DataPath = Annotated[Path, dataHint]
184
186
  class field:
185
187
  """Extra information for a given experimaestro field (param or meta)"""
186
188
 
187
- def __init__(self, *, default: Any = None, default_factory: Callable = None):
189
+ def __init__(
190
+ self,
191
+ *,
192
+ default: Any = None,
193
+ default_factory: Callable = None,
194
+ ignore_generated=False,
195
+ ):
188
196
  assert not (
189
197
  (default is not None) and (default_factory is not None)
190
198
  ), "default and default_factory are mutually exclusive options"
191
199
 
192
200
  self.default_factory = default_factory
193
201
  self.default = default
202
+ self.ignore_generated = ignore_generated
194
203
 
195
204
 
196
205
  class help(TypeAnnotation):
@@ -122,11 +122,11 @@ class ConfigInformation:
122
122
  def __init__(self, pyobject: "ConfigMixin"):
123
123
  # The underlying pyobject and XPM type
124
124
  self.pyobject = pyobject
125
- self.xpmtype = pyobject.__xpmtype__ # type: ObjectType
125
+ self.xpmtype: "ObjectType" = pyobject.__xpmtype__
126
126
  self.values = {}
127
127
 
128
128
  # Meta-informations
129
- self._tags = {}
129
+ self._tags: dict[str, Any] = {}
130
130
  self._initinfo = ""
131
131
 
132
132
  self._taskoutput = None
@@ -142,7 +142,7 @@ class ConfigInformation:
142
142
  #: True when this configuration was loaded from disk
143
143
  self.loaded = False
144
144
 
145
- # Explicitely added dependencies
145
+ # Explicitly added dependencies
146
146
  self.dependencies = []
147
147
 
148
148
  # Concrete type variables resolutions
@@ -170,6 +170,34 @@ class ConfigInformation:
170
170
  self._sealed = False
171
171
  self._meta = None
172
172
 
173
+ # This contains the list of generated values (using context) in this
174
+ # configuration or any sub-configuration, is generated. This prevents
175
+ # problem when a configuration with generated values is re-used.
176
+ self._generated_values = []
177
+
178
+ def get_generated_paths(
179
+ self, path: list[str] = None, paths: list[str] = None
180
+ ) -> list[str]:
181
+ """Get the list of generated paths, useful to track down those
182
+
183
+ :param path: The current path
184
+ :param paths: The list of generated paths so far, defaults to None
185
+ :return: The full list of generated paths
186
+ """
187
+ paths = [] if paths is None else paths
188
+ path = [] if path is None else path
189
+
190
+ for key in self._generated_values:
191
+ value = self.values[key]
192
+ if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
193
+ path.append(key)
194
+ value.__xpm__.get_generated_paths(path, paths)
195
+ path.pop()
196
+ else:
197
+ paths.append(".".join(path + [key]))
198
+
199
+ return paths
200
+
173
201
  def set_meta(self, value: Optional[bool]):
174
202
  """Sets the meta flag"""
175
203
  assert not self._sealed, "Configuration is sealed"
@@ -187,6 +215,31 @@ class ConfigInformation:
187
215
  # Not an argument, bypass
188
216
  return object.__getattribute__(self.pyobject, name)
189
217
 
218
+ @staticmethod
219
+ def is_generated_value(argument, value):
220
+ if argument.ignore_generated:
221
+ return False
222
+
223
+ if value is None:
224
+ return False
225
+
226
+ if isinstance(value, (int, str, float, bool, Path)):
227
+ return False
228
+
229
+ if isinstance(value, ConfigMixin):
230
+ return value.__xpm__._generated_values and value.__xpm__.task is None
231
+
232
+ if isinstance(value, list):
233
+ return any(ConfigInformation.is_generated_value(argument, x) for x in value)
234
+
235
+ if isinstance(value, dict):
236
+ return any(
237
+ ConfigInformation.is_generated_value(argument, x)
238
+ for x in value.values()
239
+ )
240
+
241
+ return False
242
+
190
243
  def set(self, k, v, bypass=False):
191
244
  from experimaestro.generators import Generator
192
245
 
@@ -198,9 +251,21 @@ class ConfigInformation:
198
251
  if self._sealed and not bypass:
199
252
  raise AttributeError(f"Object is read-only (trying to set {k})")
200
253
 
254
+ if not isinstance(v, ConfigMixin) and isinstance(v, Config):
255
+ raise AttributeError(
256
+ "Configuration (and not objects) should be used. Consider using .C(...)"
257
+ )
258
+
201
259
  try:
202
260
  argument = self.xpmtype.arguments.get(k, None)
203
261
  if argument:
262
+ if ConfigInformation.is_generated_value(argument, v):
263
+ raise AttributeError(
264
+ f"Cannot set {k} to a configuration with generated values. "
265
+ "Here is the list of paths to help you: "
266
+ f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
267
+ )
268
+
204
269
  if not bypass and (
205
270
  (isinstance(argument.generator, Generator)) or argument.constant
206
271
  ):
@@ -326,12 +391,21 @@ class ConfigInformation:
326
391
  Arguments:
327
392
  - context: the generation context
328
393
  """
394
+ if generated_keys := [
395
+ k
396
+ for k, v in self.values.items()
397
+ if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
398
+ ]:
399
+ raise AttributeError(
400
+ "Cannot seal a configuration with generated values:"
401
+ f"""{",".join(generated_keys)} in {context.currentpath}"""
402
+ )
329
403
 
330
404
  class Sealer(ConfigWalk):
331
- def preprocess(self, config: Config):
405
+ def preprocess(self, config: ConfigMixin):
332
406
  return not config.__xpm__._sealed, config
333
407
 
334
- def postprocess(self, stub, config: Config, values):
408
+ def postprocess(self, stub, config: ConfigMixin, values):
335
409
  # Generate values
336
410
  from experimaestro.generators import Generator
337
411
 
@@ -344,22 +418,36 @@ class ConfigInformation:
344
418
  continue
345
419
  value = argument.generator()
346
420
  else:
421
+ # Generate a value
347
422
  sig = inspect.signature(argument.generator)
348
423
  if len(sig.parameters) == 0:
349
424
  value = argument.generator()
350
425
  elif len(sig.parameters) == 2:
426
+ # Only in that case do we need to flag this configuration
427
+ # as containing generated values
428
+ config.__xpm__._generated_values.append(k)
351
429
  value = argument.generator(self.context, config)
352
430
  else:
353
431
  assert (
354
432
  False
355
433
  ), "generator has either two parameters (context and config), or none"
356
434
  config.__xpm__.set(k, value, bypass=True)
435
+ else:
436
+ value = config.__xpm__.values.get(k)
357
437
  except Exception:
358
438
  logger.error(
359
439
  "While setting %s of %s", argument.name, config.__xpmtype__
360
440
  )
361
441
  raise
362
442
 
443
+ # Propagate the generated value flag
444
+ if (
445
+ (value is not None)
446
+ and isinstance(value, ConfigMixin)
447
+ and value.__xpm__._generated_values
448
+ ):
449
+ config.__xpm__._generated_values.append(k)
450
+
363
451
  config.__xpm__._sealed = True
364
452
 
365
453
  Sealer(context, recurse_task=True)(self.pyobject)
@@ -657,13 +745,6 @@ class ConfigInformation:
657
745
 
658
746
  print(file=sys.stderr) # noqa: T201
659
747
 
660
- # Handle an output configuration # FIXME: remove
661
- def mark_output(config: "Config"):
662
- """Sets a dependency on the job"""
663
- assert not isinstance(config, Task), "Cannot set a dependency on a task"
664
- config.__xpm__.task = self.pyobject
665
- return config
666
-
667
748
  # Mark this configuration also
668
749
  self.task = self.pyobject
669
750
 
@@ -1242,6 +1323,9 @@ def clone(v):
1242
1323
  if isinstance(v, Enum):
1243
1324
  return v
1244
1325
 
1326
+ if isinstance(v, tuple):
1327
+ return tuple(clone(x) for x in v)
1328
+
1245
1329
  if isinstance(v, Config):
1246
1330
  # Create a new instance
1247
1331
  kwargs = {
@@ -1260,6 +1344,11 @@ class ConfigMixin:
1260
1344
  """Class for configuration objects"""
1261
1345
 
1262
1346
  __xpmtype__: ObjectType
1347
+ """The associated XPM type"""
1348
+
1349
+ __xpm__: ConfigInformation
1350
+ """The __xpm__ object contains all instance specific information about a
1351
+ configuration/task"""
1263
1352
 
1264
1353
  def __init__(self, **kwargs):
1265
1354
  """Initialize the configuration with the given parameters"""
@@ -1441,10 +1530,6 @@ class Config:
1441
1530
  """The object type holds all the information about a specific subclass
1442
1531
  experimaestro metadata"""
1443
1532
 
1444
- __xpm__: ConfigInformation
1445
- """The __xpm__ object contains all instance specific information about a
1446
- configuration/task"""
1447
-
1448
1533
  @classproperty
1449
1534
  def XPMConfig(cls):
1450
1535
  if issubclass(cls, ConfigMixin):
@@ -71,6 +71,7 @@ class ConfigWalk:
71
71
  return self.context.push(str(i))
72
72
 
73
73
  def map(self, k: str):
74
+ """Provides a path context when processing a tree"""
74
75
  return self.context.push(k)
75
76
 
76
77
  def stub(self, config):
@@ -123,7 +124,8 @@ class ConfigWalk:
123
124
  and self.recurse_task
124
125
  and x.__xpm__.task is not x
125
126
  ):
126
- self(x.__xpm__.task)
127
+ with self.map("__task__"):
128
+ self(x.__xpm__.task)
127
129
 
128
130
  processed = self.postprocess(stub, x, result)
129
131
  self.visited[xid] = processed
@@ -0,0 +1,93 @@
1
+ from experimaestro import Config, Task, Param, Meta, Path, field, PathGenerator
2
+ from experimaestro.scheduler.workspace import Workspace
3
+ from experimaestro.settings import Settings, WorkspaceSettings
4
+ import pytest
5
+ from experimaestro.scheduler import RunMode
6
+
7
+
8
+ class Validation(Config):
9
+ best_checkpoint: Meta[Path] = field(default_factory=PathGenerator("index"))
10
+
11
+
12
+ class Learner(Task):
13
+ validation: Param[Validation]
14
+ x: Param[int]
15
+
16
+ @staticmethod
17
+ def create(x: int, validation: Param[Validation]):
18
+ return Learner.C(x=x, validation=validation)
19
+
20
+
21
+ class LearnerList(Task):
22
+ validation: Param[list[Validation]]
23
+ x: Param[int]
24
+
25
+ @staticmethod
26
+ def create(x: int, validation: Param[Validation]):
27
+ return LearnerList.C(x=x, validation=[validation])
28
+
29
+
30
+ class LearnerDict(Task):
31
+ validation: Param[dict[str, Validation]]
32
+ x: Param[int]
33
+
34
+ @staticmethod
35
+ def create(x: int, validation: Param[Validation]):
36
+ return LearnerDict.C(x=x, validation={"key": validation})
37
+
38
+
39
+ class ModuleLoader(Task):
40
+ validation: Param[Validation] = field(ignore_generated=True)
41
+
42
+
43
+ @pytest.mark.parametrize("cls", [Learner, LearnerDict, LearnerList])
44
+ def test_generators_reuse_on_submit(cls):
45
+ # We have one way to select the best model
46
+ validation = Validation.C()
47
+
48
+ workspace = Workspace(
49
+ Settings(),
50
+ WorkspaceSettings("test_generators_reuse", path=Path("/tmp")),
51
+ run_mode=RunMode.DRY_RUN,
52
+ )
53
+
54
+ # OK, the path is generated depending on Learner with x=1
55
+ cls.create(1, validation).submit(workspace=workspace)
56
+
57
+ with pytest.raises((AttributeError)):
58
+ # Here we have a problem...
59
+ # the path is still the previous one
60
+ cls.create(2, validation).submit(workspace=workspace)
61
+
62
+
63
+ @pytest.mark.parametrize("cls", [Learner, LearnerDict, LearnerList])
64
+ def test_generators_delayed_submit(cls):
65
+ workspace = Workspace(
66
+ Settings(),
67
+ WorkspaceSettings("test_generators_simple", path=Path("/tmp")),
68
+ run_mode=RunMode.DRY_RUN,
69
+ )
70
+ validation = Validation.C()
71
+ task1 = cls.create(1, validation)
72
+ task2 = cls.create(2, validation)
73
+ task1.submit(workspace=workspace)
74
+ with pytest.raises((AttributeError)):
75
+ task2.submit(workspace=workspace)
76
+
77
+
78
+ @pytest.mark.parametrize("cls", [Learner, LearnerDict, LearnerList])
79
+ def test_generators_reuse_on_set(cls):
80
+ workspace = Workspace(
81
+ Settings(),
82
+ WorkspaceSettings("test_generators_simple", path=Path("/tmp")),
83
+ run_mode=RunMode.DRY_RUN,
84
+ )
85
+ validation = Validation.C()
86
+ cls.create(1, validation).submit(workspace=workspace)
87
+ with pytest.raises((AttributeError)):
88
+ # We should not be able to *create* a second task with the same validation,
89
+ # even without submitting it
90
+ cls.create(2, validation)
91
+
92
+ # This should run OK
93
+ ModuleLoader.C(validation=validation)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: experimaestro
3
- Version: 1.12.0
3
+ Version: 1.14.0
4
4
  Summary: "Experimaestro is a computer science experiment manager"
5
5
  License: GPL-3
6
6
  License-File: LICENSE
@@ -12,14 +12,14 @@ experimaestro/connectors/__init__.py,sha256=UKhDU3uv9jFH37oUb0JiejrekA85xtEirn79
12
12
  experimaestro/connectors/local.py,sha256=lCGIubqmUJZ1glLtLRXOgakTMfEaEmFtNkEcw9qV5vw,6143
13
13
  experimaestro/connectors/ssh.py,sha256=5giqvv1y0QQKF-GI0IFUzI_Z5H8Bj9EuL_Szpvk899Q,8600
14
14
  experimaestro/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- experimaestro/core/arguments.py,sha256=7hpkU1f8LJ7JL8kQaD514h9CFSfMotYLsVfMsMmdpWk,6487
15
+ experimaestro/core/arguments.py,sha256=jlK2_EILKLblLSomUDjlGmR-_xq3rAZPgAwbUjU4boc,6710
16
16
  experimaestro/core/callbacks.py,sha256=59JfeUgWcCCdIQ3pvh-xNnoRp9BX8f4iOAkgm16wBzE,1660
17
17
  experimaestro/core/context.py,sha256=1tLmX7WcgEKSbGw77vfziTzS8KNsoZJ02JBWMBCqqOk,2606
18
18
  experimaestro/core/identifier.py,sha256=JadBAdW2fOIgoTyMRtKI_RY9SXQP2B1vq1rUcV465Hs,10214
19
19
  experimaestro/core/objects/__init__.py,sha256=ucJY5e17QQ1Kc-GYXeL7g8GFj8rP0XB4g2vrl32uhxY,721
20
- experimaestro/core/objects/config.py,sha256=qOUgCmX1NwhRhpTVxgr2sSAqBCMYloUXb5f-NiXgV8Q,56989
20
+ experimaestro/core/objects/config.py,sha256=ycZx6252pNmJadpSiu_1jd-LsiHw7kWWufkWH0vcOPc,60293
21
21
  experimaestro/core/objects/config_utils.py,sha256=ZLECGkeIWdzunm8vwWsQhvcSgV1e064BgXbLiZnxSEM,1288
22
- experimaestro/core/objects/config_walk.py,sha256=gyDMrVPbBMChn7r4em_gQXuqnxASO_JVauEbnJNO8II,4245
22
+ experimaestro/core/objects/config_walk.py,sha256=b8u6oohf1gXyva4Y_Cyyl_3BNivzI2y-I2B6MUPV2aU,4353
23
23
  experimaestro/core/objects.pyi,sha256=xvlsRj4u1xsJxbevJl5Ner_HwmxR8x1JlAeIVDJzuy0,6498
24
24
  experimaestro/core/serialization.py,sha256=CSPEwOzlDsgAz6V2og-TgyU0RXDtzt_nXaoXFZleDZE,5775
25
25
  experimaestro/core/serializers.py,sha256=R_CAMyjjfU1oi-eHU6VlEUixJpFayGqEPaYu7VsD9xA,1197
@@ -121,6 +121,7 @@ experimaestro/tests/test_dependencies.py,sha256=xfWrSkvjT45G4FSCL535m1huLT2ghmyW
121
121
  experimaestro/tests/test_experiment.py,sha256=QWF9aHewL9hepagrKKFyfikKn3iiZ_lRRXl1LLttta0,1687
122
122
  experimaestro/tests/test_findlauncher.py,sha256=KPy8ow--NXS1KFCIpxrmEJFRvjo-v-PwlVHVyoVKLPg,3134
123
123
  experimaestro/tests/test_forward.py,sha256=9y1zYm7hT_Lx5citxnK7n20cMZ2WJbsaEeY5irCZ9U4,735
124
+ experimaestro/tests/test_generators.py,sha256=R0UypTzxX0dPYvY4A_kozpLDHhGzQDNfLQyc0oRaSx8,2891
124
125
  experimaestro/tests/test_identifier.py,sha256=zgfpz5UumQftQTHhdw3nmr044Xd7Ntrh7e2hIAoS_PA,13862
125
126
  experimaestro/tests/test_instance.py,sha256=h-8UeoOlNsD-STWq5jbhmxw35CyiwJxtKAaUGmLPgB8,1521
126
127
  experimaestro/tests/test_objects.py,sha256=zycxjvWuJAbPR8-q2T3zuBY9xfmlhf1YvtOcrImHxnc,2431
@@ -151,8 +152,8 @@ experimaestro/utils/multiprocessing.py,sha256=am3DkHP_kmWbpynbck2c9QystCUtPBoSAC
151
152
  experimaestro/utils/resources.py,sha256=j-nvsTFwmgENMoVGOD2Ap-UD3WU85WkI0IgeSszMCX4,1328
152
153
  experimaestro/utils/settings.py,sha256=jpFMqF0DLL4_P1xGal0zVR5cOrdD8O0Y2IOYvnRgN3k,793
153
154
  experimaestro/xpmutils.py,sha256=S21eMbDYsHfvmZ1HmKpq5Pz5O-1HnCLYxKbyTBbASyQ,638
154
- experimaestro-1.12.0.dist-info/METADATA,sha256=BbLWHf4Fg1w4rkSNnAjjdRWf9S4aNx92uKlePpSacQc,5705
155
- experimaestro-1.12.0.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
156
- experimaestro-1.12.0.dist-info/entry_points.txt,sha256=TppTNiz5qm5xm1fhAcdLKdCLMrlL-eQggtCrCI00D9c,446
157
- experimaestro-1.12.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
158
- experimaestro-1.12.0.dist-info/RECORD,,
155
+ experimaestro-1.14.0.dist-info/METADATA,sha256=qf7GOrTHf73pHUHBc2APqLSWvhCKbVhuPBOZmX53dpw,5705
156
+ experimaestro-1.14.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
157
+ experimaestro-1.14.0.dist-info/entry_points.txt,sha256=TppTNiz5qm5xm1fhAcdLKdCLMrlL-eQggtCrCI00D9c,446
158
+ experimaestro-1.14.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
159
+ experimaestro-1.14.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.0
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any