experimaestro 1.13.0__py3-none-any.whl → 1.15.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):
@@ -6,7 +6,7 @@ import logging
6
6
  import os
7
7
  import struct
8
8
  from typing import Optional
9
- from experimaestro.core.objects import Config
9
+ from experimaestro.core.objects import Config, ConfigMixin
10
10
 
11
11
 
12
12
  class ConfigPath:
@@ -116,7 +116,7 @@ class IdentifierComputer:
116
116
  CYCLE_REFERENCE = b"\x0b"
117
117
  INIT_TASKS = b"\x0c"
118
118
 
119
- def __init__(self, config: "Config", config_path: ConfigPath, *, version=None):
119
+ def __init__(self, config: "ConfigMixin", config_path: ConfigPath, *, version=None):
120
120
  # Hasher for parameters
121
121
  self._hasher = hashlib.sha256()
122
122
  self.config = config
@@ -170,7 +170,7 @@ class IdentifierComputer:
170
170
  self._hashupdate(IdentifierComputer.ENUM_ID)
171
171
  k = value.__class__
172
172
  self._hashupdate(
173
- f"{k.__module__}.{k.__qualname__ }:{value.name}".encode("utf-8"),
173
+ f"{k.__module__}.{k.__qualname__}:{value.name}".encode("utf-8"),
174
174
  )
175
175
  elif isinstance(value, dict):
176
176
  self._hashupdate(IdentifierComputer.DICT_ID)
@@ -183,7 +183,7 @@ class IdentifierComputer:
183
183
  self.update(value)
184
184
 
185
185
  # Handles configurations
186
- elif isinstance(value, Config):
186
+ elif isinstance(value, ConfigMixin):
187
187
  # Encodes the identifier
188
188
  self._hashupdate(IdentifierComputer.OBJECT_ID)
189
189
 
@@ -264,12 +264,17 @@ class IdentifierComputer:
264
264
  self._hashupdate(IdentifierComputer.NAME_ID)
265
265
  self.update(argvalue)
266
266
 
267
+ # Add init tasks
268
+ if value.__xpm__.init_tasks:
269
+ self._hashupdate(IdentifierComputer.INIT_TASKS)
270
+ for init_task in value.__xpm__.init_tasks:
271
+ self.update(init_task)
267
272
  else:
268
273
  raise NotImplementedError("Cannot compute hash of type %s" % type(value))
269
274
 
270
275
  @staticmethod
271
276
  def compute(
272
- config: "Config", config_path: ConfigPath | None = None, version=None
277
+ config: "ConfigMixin", config_path: ConfigPath | None = None, version=None
273
278
  ) -> Identifier:
274
279
  """Compute the identifier for a configuration
275
280
 
@@ -281,7 +286,7 @@ class IdentifierComputer:
281
286
  # Try to use the cached value first
282
287
  # (if there are no loops)
283
288
  if config.__xpm__._sealed:
284
- identifier = config.__xpm__._raw_identifier
289
+ identifier = config.__xpm__._identifier
285
290
  if identifier is not None and not identifier.has_loops:
286
291
  return identifier
287
292
 
@@ -9,7 +9,6 @@ from experimaestro import taskglobals
9
9
 
10
10
  from termcolor import cprint
11
11
  from pathlib import Path
12
- import hashlib
13
12
  import logging
14
13
  import io
15
14
  from enum import Enum
@@ -20,7 +19,6 @@ from typing import (
20
19
  Callable,
21
20
  ClassVar,
22
21
  Dict,
23
- Iterator,
24
22
  List,
25
23
  Optional,
26
24
  Set,
@@ -49,7 +47,6 @@ from .config_walk import ConfigWalk, ConfigWalkContext
49
47
  from .config_utils import (
50
48
  getqualattr,
51
49
  add_to_path,
52
- SealedError,
53
50
  TaggedValue,
54
51
  ObjectStore,
55
52
  classproperty,
@@ -149,9 +146,6 @@ class ConfigInformation:
149
146
  # This is used to check typevars coherence
150
147
  self.concrete_typevars: Dict[TypeVar, type] = {}
151
148
 
152
- # Lightweight tasks
153
- self.pre_tasks: List["LightweightTask"] = []
154
-
155
149
  # Initialization tasks
156
150
  self.init_tasks: List["LightweightTask"] = []
157
151
 
@@ -160,20 +154,40 @@ class ConfigInformation:
160
154
 
161
155
  # Cached information
162
156
 
163
- self._full_identifier = None
164
- """The full identifier (with pre-tasks)"""
165
-
166
- self._raw_identifier = None
167
- """The identifier without taking into account pre-tasks"""
157
+ self._identifier = None
158
+ """The configuration identifier (cached when sealed)"""
168
159
 
169
160
  self._validated = False
170
161
  self._sealed = False
171
162
  self._meta = None
172
163
 
173
- #: This flags is True when a value in this configuration,
174
- #: or any sub-configuration, is generated. This prevents problem
175
- #: when a configuration with generated values is re-used
176
- self._has_generated_value = False
164
+ # This contains the list of generated values (using context) in this
165
+ # configuration or any sub-configuration, is generated. This prevents
166
+ # problem when a configuration with generated values is re-used.
167
+ self._generated_values = []
168
+
169
+ def get_generated_paths(
170
+ self, path: list[str] = None, paths: list[str] = None
171
+ ) -> list[str]:
172
+ """Get the list of generated paths, useful to track down those
173
+
174
+ :param path: The current path
175
+ :param paths: The list of generated paths so far, defaults to None
176
+ :return: The full list of generated paths
177
+ """
178
+ paths = [] if paths is None else paths
179
+ path = [] if path is None else path
180
+
181
+ for key in self._generated_values:
182
+ value = self.values[key]
183
+ if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
184
+ path.append(key)
185
+ value.__xpm__.get_generated_paths(path, paths)
186
+ path.pop()
187
+ else:
188
+ paths.append(".".join(path + [key]))
189
+
190
+ return paths
177
191
 
178
192
  def set_meta(self, value: Optional[bool]):
179
193
  """Sets the meta flag"""
@@ -192,6 +206,31 @@ class ConfigInformation:
192
206
  # Not an argument, bypass
193
207
  return object.__getattribute__(self.pyobject, name)
194
208
 
209
+ @staticmethod
210
+ def is_generated_value(argument, value):
211
+ if argument.ignore_generated:
212
+ return False
213
+
214
+ if value is None:
215
+ return False
216
+
217
+ if isinstance(value, (int, str, float, bool, Path)):
218
+ return False
219
+
220
+ if isinstance(value, ConfigMixin):
221
+ return value.__xpm__._generated_values and value.__xpm__.task is None
222
+
223
+ if isinstance(value, list):
224
+ return any(ConfigInformation.is_generated_value(argument, x) for x in value)
225
+
226
+ if isinstance(value, dict):
227
+ return any(
228
+ ConfigInformation.is_generated_value(argument, x)
229
+ for x in value.values()
230
+ )
231
+
232
+ return False
233
+
195
234
  def set(self, k, v, bypass=False):
196
235
  from experimaestro.generators import Generator
197
236
 
@@ -208,18 +247,16 @@ class ConfigInformation:
208
247
  "Configuration (and not objects) should be used. Consider using .C(...)"
209
248
  )
210
249
 
211
- if (
212
- isinstance(v, ConfigMixin)
213
- and v.__xpm__._has_generated_value
214
- and v.__xpm__.task is None
215
- ):
216
- raise AttributeError(
217
- f"Cannot set {k} to a configuration with generated values"
218
- )
219
-
220
250
  try:
221
251
  argument = self.xpmtype.arguments.get(k, None)
222
252
  if argument:
253
+ if ConfigInformation.is_generated_value(argument, v):
254
+ raise AttributeError(
255
+ f"Cannot set {k} to a configuration with generated values. "
256
+ "Here is the list of paths to help you: "
257
+ f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
258
+ )
259
+
223
260
  if not bypass and (
224
261
  (isinstance(argument.generator, Generator)) or argument.constant
225
262
  ):
@@ -321,10 +358,6 @@ class ConfigInformation:
321
358
  % (k, self.xpmtype, self._initinfo)
322
359
  )
323
360
 
324
- # Validate pre-tasks
325
- for pre_task in self.pre_tasks:
326
- pre_task.__xpm__.validate()
327
-
328
361
  # Validate init tasks
329
362
  for init_task in self.init_tasks:
330
363
  init_task.__xpm__.validate()
@@ -345,6 +378,15 @@ class ConfigInformation:
345
378
  Arguments:
346
379
  - context: the generation context
347
380
  """
381
+ if generated_keys := [
382
+ k
383
+ for k, v in self.values.items()
384
+ if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
385
+ ]:
386
+ raise AttributeError(
387
+ "Cannot seal a configuration with generated values:"
388
+ f"""{",".join(generated_keys)} in {context.currentpath}"""
389
+ )
348
390
 
349
391
  class Sealer(ConfigWalk):
350
392
  def preprocess(self, config: ConfigMixin):
@@ -368,13 +410,15 @@ class ConfigInformation:
368
410
  if len(sig.parameters) == 0:
369
411
  value = argument.generator()
370
412
  elif len(sig.parameters) == 2:
413
+ # Only in that case do we need to flag this configuration
414
+ # as containing generated values
415
+ config.__xpm__._generated_values.append(k)
371
416
  value = argument.generator(self.context, config)
372
417
  else:
373
418
  assert (
374
419
  False
375
420
  ), "generator has either two parameters (context and config), or none"
376
421
  config.__xpm__.set(k, value, bypass=True)
377
- config.__xpm__._has_generated_value = True
378
422
  else:
379
423
  value = config.__xpm__.values.get(k)
380
424
  except Exception:
@@ -387,9 +431,9 @@ class ConfigInformation:
387
431
  if (
388
432
  (value is not None)
389
433
  and isinstance(value, ConfigMixin)
390
- and value.__xpm__._has_generated_value
434
+ and value.__xpm__._generated_values
391
435
  ):
392
- self._has_generated_value = True
436
+ config.__xpm__._generated_values.append(k)
393
437
 
394
438
  config.__xpm__._sealed = True
395
439
 
@@ -403,90 +447,29 @@ class ConfigInformation:
403
447
  context = ConfigWalkContext()
404
448
 
405
449
  class Unsealer(ConfigWalk):
406
- def preprocess(self, config: Config):
450
+ def preprocess(self, config: ConfigMixin):
407
451
  return config.__xpm__._sealed, config
408
452
 
409
- def postprocess(self, stub, config: Config, values):
453
+ def postprocess(self, stub, config: ConfigMixin, values):
410
454
  config.__xpm__._sealed = False
411
455
  config.__xpm__._identifier = None
412
456
 
413
457
  Unsealer(context, recurse_task=True)(self.pyobject)
414
458
 
415
- def collect_pre_tasks(self) -> Iterator["Config"]:
416
- context = ConfigWalkContext()
417
- pre_tasks: Dict[int, "Config"] = {}
418
-
419
- class PreTaskCollect(ConfigWalk):
420
- def preprocess(self, config: Config):
421
- # Do not cross tasks
422
- return not isinstance(config.__xpm__, Task), config
423
-
424
- def postprocess(self, stub, config: Config, values):
425
- pre_tasks.update(
426
- {id(pre_task): pre_task for pre_task in config.__xpm__.pre_tasks}
427
- )
428
-
429
- PreTaskCollect(context, recurse_task=True)(self.pyobject)
430
- return pre_tasks.values()
431
-
432
- def identifiers(self, only_raw: bool):
459
+ @property
460
+ def identifier(self):
433
461
  """Computes the unique identifier"""
434
- from ..identifier import IdentifierComputer, Identifier
435
-
436
- raw_identifier = self._raw_identifier
437
- full_identifier = self._full_identifier
462
+ from ..identifier import IdentifierComputer
438
463
 
439
464
  # Computes raw identifier if needed
440
- if raw_identifier is None or not self._sealed:
441
- # Get the main identifier
442
- raw_identifier = IdentifierComputer.compute(self.pyobject)
443
- if self._sealed:
444
- self._raw_identifier = raw_identifier
445
-
446
- if only_raw:
447
- return raw_identifier, full_identifier
448
-
449
- # OK, let's compute the full identifier
450
- if full_identifier is None or not self._sealed:
451
- # Compute the full identifier by including the pre-tasks
452
- hasher = hashlib.sha256()
453
- hasher.update(raw_identifier.all)
454
- pre_tasks_ids = [
455
- pre_task.__xpm__.raw_identifier.all
456
- for pre_task in self.collect_pre_tasks()
457
- ]
458
- for task_id in sorted(pre_tasks_ids):
459
- hasher.update(task_id)
460
-
461
- # Adds init tasks
462
- if self.init_tasks:
463
- hasher.update(IdentifierComputer.INIT_TASKS)
464
- for init_task in self.init_tasks:
465
- hasher.update(init_task.__xpm__.raw_identifier.all)
466
-
467
- full_identifier = Identifier(hasher.digest())
468
- full_identifier.has_loops = raw_identifier.has_loops
469
-
470
- # Only cache the identifier if sealed
471
- if self._sealed:
472
- self._full_identifier = full_identifier
465
+ if self._identifier is not None:
466
+ return self._identifier
473
467
 
474
- return raw_identifier, full_identifier
475
-
476
- @property
477
- def raw_identifier(self) -> "Identifier":
478
- """Computes the unique identifier (without task modifiers)"""
479
- raw_identifier, _ = self.identifiers(True)
480
- return raw_identifier
481
-
482
- @property
483
- def full_identifier(self) -> "Identifier":
484
- """Computes the unique identifier (with task modifiers)"""
485
- _, full_identifier = self.identifiers(False)
486
- return full_identifier
487
-
488
- identifier = full_identifier
489
- """Deprecated: use full_identifier"""
468
+ # Get the main identifier
469
+ identifier = IdentifierComputer.compute(self.pyobject)
470
+ if self._sealed:
471
+ self._identifier = identifier
472
+ return identifier
490
473
 
491
474
  def dependency(self):
492
475
  """Returns a dependency"""
@@ -501,12 +484,6 @@ class ConfigInformation:
501
484
  path: List[str],
502
485
  taskids: Set[int],
503
486
  ):
504
- # Add pre-tasks
505
- for pre_task in self.pre_tasks:
506
- pre_task.__xpm__.updatedependencies(
507
- dependencies, path + ["__pre_tasks__"], taskids
508
- )
509
-
510
487
  # Add initialization tasks
511
488
  for init_task in self.init_tasks:
512
489
  init_task.__xpm__.updatedependencies(
@@ -776,9 +753,6 @@ class ConfigInformation:
776
753
  if self.task is not None and self.task is not self:
777
754
  ConfigInformation.__collect_objects__(self.task, objects, context)
778
755
 
779
- # Serialize pre-tasks
780
- ConfigInformation.__collect_objects__(self.pre_tasks, objects, context)
781
-
782
756
  # Serialize initialization tasks
783
757
  ConfigInformation.__collect_objects__(self.init_tasks, objects, context)
784
758
 
@@ -792,8 +766,6 @@ class ConfigInformation:
792
766
  }
793
767
 
794
768
  # Add pre/init tasks
795
- if self.pre_tasks:
796
- state_dict["pre-tasks"] = [id(pre_task) for pre_task in self.pre_tasks]
797
769
  if self.init_tasks:
798
770
  state_dict["init-tasks"] = [id(init_task) for init_task in self.init_tasks]
799
771
 
@@ -1125,12 +1097,6 @@ class ConfigInformation:
1125
1097
  o.__post_init__()
1126
1098
 
1127
1099
  else:
1128
- # Sets pre-tasks
1129
- o.__xpm__.pre_tasks = [
1130
- objects[pre_task_id]
1131
- for pre_task_id in definition.get("pre-tasks", [])
1132
- ]
1133
-
1134
1100
  if task_id := definition.get("task", None):
1135
1101
  o.__xpm__.task = objects[task_id]
1136
1102
 
@@ -1164,15 +1130,6 @@ class ConfigInformation:
1164
1130
 
1165
1131
  # Run pre-task (or returns them)
1166
1132
  if as_instance or return_tasks:
1167
- # Collect pre-tasks (just once)
1168
- completed_pretasks = set()
1169
- pre_tasks = []
1170
- for definition in definitions:
1171
- for pre_task_id in definition.get("pre-tasks", []):
1172
- if pre_task_id not in completed_pretasks:
1173
- completed_pretasks.add(pre_task_id)
1174
- pre_tasks.append(objects[pre_task_id])
1175
-
1176
1133
  # Collect init tasks
1177
1134
  init_tasks = []
1178
1135
  for init_task_id in definitions[-1].get("init-tasks", []):
@@ -1180,14 +1137,11 @@ class ConfigInformation:
1180
1137
  init_tasks.append(init_task)
1181
1138
 
1182
1139
  if as_instance:
1183
- for pre_task in pre_tasks:
1184
- logger.info("Executing pre-task %s", type(pre_task))
1185
- pre_task.execute()
1186
1140
  for init_task in init_tasks:
1187
1141
  logger.info("Executing init task %s", type(init_task))
1188
1142
  init_task.execute()
1189
1143
  else:
1190
- return o, pre_tasks, pre_task + init_tasks
1144
+ return o, init_tasks
1191
1145
 
1192
1146
  return o
1193
1147
 
@@ -1195,7 +1149,6 @@ class ConfigInformation:
1195
1149
  def __init__(self, context: ConfigWalkContext, *, objects: ObjectStore = None):
1196
1150
  super().__init__(context)
1197
1151
  self.objects = ObjectStore() if objects is None else objects
1198
- self.pre_tasks = {}
1199
1152
 
1200
1153
  def preprocess(self, config: "Config"):
1201
1154
  if self.objects.is_constructed(id(config)):
@@ -1222,10 +1175,6 @@ class ConfigInformation:
1222
1175
  # Call __post_init__
1223
1176
  stub.__post_init__()
1224
1177
 
1225
- # Gather pre-tasks
1226
- for pre_task in config.__xpm__.pre_tasks:
1227
- self.pre_tasks[id(pre_task)] = self.stub(pre_task)
1228
-
1229
1178
  self.objects.set_constructed(id(config))
1230
1179
  return stub
1231
1180
 
@@ -1239,10 +1188,6 @@ class ConfigInformation:
1239
1188
  processor = ConfigInformation.FromPython(context, objects=objects)
1240
1189
  last_object = processor(self.pyobject)
1241
1190
 
1242
- # Execute pre-tasks
1243
- for pre_task in processor.pre_tasks.values():
1244
- pre_task.execute()
1245
-
1246
1191
  return last_object
1247
1192
 
1248
1193
  def add_dependencies(self, *dependencies):
@@ -1428,29 +1373,7 @@ class ConfigMixin:
1428
1373
  attributes)"""
1429
1374
  return clone(self)
1430
1375
 
1431
- def add_pretasks(self, *tasks: "LightweightTask"):
1432
- assert all(
1433
- [isinstance(task, LightweightTask) for task in tasks]
1434
- ), "One of the pre-tasks are not lightweight tasks"
1435
- if self.__xpm__._sealed:
1436
- raise SealedError("Cannot add pre-tasks to a sealed configuration")
1437
- self.__xpm__.pre_tasks.extend(tasks)
1438
- return self
1439
-
1440
- def add_pretasks_from(self, *configs: "Config"):
1441
- assert all(
1442
- [isinstance(config, ConfigMixin) for config in configs]
1443
- ), "One of the parameters is not a configuration object"
1444
- for config in configs:
1445
- self.add_pretasks(*config.__xpm__.pre_tasks)
1446
- return self
1447
-
1448
- @property
1449
- def pre_tasks(self) -> List["LightweightTask"]:
1450
- """Access pre-tasks"""
1451
- return self.__xpm__.pre_tasks
1452
-
1453
- def copy_dependencies(self, other: "Config"):
1376
+ def copy_dependencies(self, other: "ConfigMixin"):
1454
1377
  """Add all the dependencies from other configuration"""
1455
1378
 
1456
1379
  # Add task dependency
@@ -1587,27 +1510,12 @@ class Config:
1587
1510
  def __identifier__(self) -> "Identifier":
1588
1511
  return self.__xpm__.full_identifier
1589
1512
 
1590
- def add_pretasks(self, *tasks: "LightweightTask"):
1591
- """Add pre-tasks"""
1592
- raise AssertionError("This method can only be used during configuration")
1593
-
1594
- def add_pretasks_from(self, *configs: "Config"):
1595
- """Add pre-tasks from the listed configurations"""
1596
- raise AssertionError(
1597
- "The 'add_pretasks_from' can only be used during configuration"
1598
- )
1599
-
1600
1513
  def copy_dependencies(self, other: "Config"):
1601
1514
  """Add pre-tasks from the listed configurations"""
1602
1515
  raise AssertionError(
1603
1516
  "The 'copy_dependencies' method can only be used during configuration"
1604
1517
  )
1605
1518
 
1606
- @property
1607
- def pre_tasks(self) -> List["LightweightTask"]:
1608
- """Access pre-tasks"""
1609
- raise AssertionError("Pre-tasks can be accessed only during configuration")
1610
-
1611
1519
  def register_task_output(self, method, *args, **kwargs):
1612
1520
  # Determine the path for this...
1613
1521
  path = taskglobals.Env.instance().xpm_path / "task-outputs.jsonl"
@@ -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):
@@ -108,11 +109,7 @@ class ConfigWalk:
108
109
  else:
109
110
  result[arg.name] = None
110
111
 
111
- # Deals with pre-tasks
112
- if info.pre_tasks:
113
- with self.map("__pre_tasks__"):
114
- self(info.pre_tasks)
115
-
112
+ # Deals with init tasks
116
113
  if info.init_tasks:
117
114
  with self.map("__init_tasks__"):
118
115
  self(info.init_tasks)
@@ -123,7 +120,8 @@ class ConfigWalk:
123
120
  and self.recurse_task
124
121
  and x.__xpm__.task is not x
125
122
  ):
126
- self(x.__xpm__.task)
123
+ with self.map("__task__"):
124
+ self(x.__xpm__.task)
127
125
 
128
126
  processed = self.postprocess(stub, x, result)
129
127
  self.visited[xid] = processed
@@ -168,7 +168,7 @@ class ConfigMixin:
168
168
  *,
169
169
  workspace: Incomplete | None = ...,
170
170
  launcher: Incomplete | None = ...,
171
- run_mode: RunMode = ...
171
+ run_mode: RunMode = ...,
172
172
  ): ...
173
173
  def stdout(self): ...
174
174
  def stderr(self): ...
@@ -195,11 +195,7 @@ class Config:
195
195
  def __post_init__(self) -> None: ...
196
196
  def __json__(self): ...
197
197
  def __identifier__(self) -> Identifier: ...
198
- def add_pretasks(self, *tasks: "LightweightTask"): ...
199
- def add_pretasks_from(self, configs: "Config"): ...
200
198
  def copy_dependencies(self, other: "Config"): ...
201
- @property
202
- def pre_tasks(self) -> List["LightweightTask"]: ...
203
199
 
204
200
  class LightweightTask(Config):
205
201
  def execute(self) -> None: ...
@@ -213,7 +209,7 @@ class Task(LightweightTask):
213
209
  workspace: Incomplete | None = ...,
214
210
  launcher: Incomplete | None = ...,
215
211
  run_mode: RunMode = ...,
216
- init_tasks: List["LightweightTask"] = []
212
+ init_tasks: List["LightweightTask"] = [],
217
213
  ): ...
218
214
  def task_outputs(self, dep: Callable[[Config], None]) -> Any: ...
219
215
 
@@ -1,10 +1,8 @@
1
- from typing import List, TypeVar, Callable, Any
2
- from pathlib import Path
1
+ from typing import List, TypeVar
3
2
  from experimaestro import Param
4
3
 
5
4
  from .objects import Config, LightweightTask
6
5
  from .arguments import DataPath
7
- from experimaestro import copyconfig
8
6
 
9
7
 
10
8
  class SerializationLWTask(LightweightTask):
@@ -39,8 +37,3 @@ class PathSerializationLWTask(SerializationLWTask):
39
37
 
40
38
  path: DataPath
41
39
  """Path containing the data"""
42
-
43
- @classmethod
44
- def construct(cls, value: T, path: Path, dep: Callable[[Config], Any]) -> T:
45
- value = copyconfig(value)
46
- return value.add_pretasks(dep(cls(value=value, path=path)))
@@ -85,9 +85,3 @@ def test_dependencies_inner_task_output(xp):
85
85
  a = task_a.submit()
86
86
  b = Inner_TaskB(param_a=a).submit()
87
87
  check_dependencies(b, task_a)
88
-
89
-
90
- def test_dependencies_pre_task(xp):
91
- a = TaskA().submit()
92
- a2 = TaskA().add_pretasks(a).submit()
93
- check_dependencies(a2, a)
@@ -13,8 +13,35 @@ class Learner(Task):
13
13
  validation: Param[Validation]
14
14
  x: Param[int]
15
15
 
16
+ @staticmethod
17
+ def create(x: int, validation: Param[Validation]):
18
+ return Learner.C(x=x, validation=validation)
16
19
 
17
- def test_generators_reuse():
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):
18
45
  # We have one way to select the best model
19
46
  validation = Validation.C()
20
47
 
@@ -25,9 +52,42 @@ def test_generators_reuse():
25
52
  )
26
53
 
27
54
  # OK, the path is generated depending on Learner with x=1
28
- Learner.C(x=1, validation=validation).submit(workspace=workspace)
55
+ cls.create(1, validation).submit(workspace=workspace)
29
56
 
30
57
  with pytest.raises((AttributeError)):
31
58
  # Here we have a problem...
32
59
  # the path is still the previous one
33
- Learner.C(x=2, validation=validation).submit(workspace=workspace)
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)
@@ -68,42 +68,42 @@ def assert_notequal(a, b, message=""):
68
68
  assert getidentifier(a) != getidentifier(b), message
69
69
 
70
70
 
71
- def test_param_int():
72
- assert_equal(A(a=1), A(a=1))
71
+ def test_identifier_int():
72
+ assert_equal(A.C(a=1), A.C(a=1))
73
73
 
74
74
 
75
- def test_param_different_type():
76
- assert_notequal(A(a=1), B(a=1))
75
+ def test_identifier_different_type():
76
+ assert_notequal(A.C(a=1), B.C(a=1))
77
77
 
78
78
 
79
- def test_param_order():
80
- assert_equal(Values(value1=1, value2=2), Values(value2=2, value1=1))
79
+ def test_identifier_order():
80
+ assert_equal(Values.C(value1=1, value2=2), Values.C(value2=2, value1=1))
81
81
 
82
82
 
83
- def test_param_default():
84
- assert_equal(C(a=1, b=2), C(b=2))
83
+ def test_identifier_default():
84
+ assert_equal(C.C(a=1, b=2), C.C(b=2))
85
85
 
86
86
 
87
87
  def test_identifier_default_field():
88
88
  assert_equal(CField(a=1, b=2), CField(b=2))
89
89
 
90
90
 
91
- def test_param_inner_eq():
92
- assert_equal(D(a=A(a=1)), D(a=A(a=1)))
91
+ def test_identifier_inner_eq():
92
+ assert_equal(D.C(a=A.C(a=1)), D.C(a=A.C(a=1)))
93
93
 
94
94
 
95
- def test_param_float():
96
- assert_equal(Float(value=1), Float(value=1))
95
+ def test_identifier_float():
96
+ assert_equal(Float.C(value=1), Float.C(value=1))
97
97
 
98
98
 
99
- def test_param_float2():
100
- assert_equal(Float(value=1.0), Float(value=1))
99
+ def test_identifier_float2():
100
+ assert_equal(Float.C(value=1.0), Float.C(value=1))
101
101
 
102
102
 
103
103
  # --- Argument name
104
104
 
105
105
 
106
- def test_param_name():
106
+ def test_identifier_name():
107
107
  """The identifier fully determines the hash code"""
108
108
 
109
109
  class Config0(Config):
@@ -125,7 +125,7 @@ def test_param_name():
125
125
  # --- Test option
126
126
 
127
127
 
128
- def test_param_option():
128
+ def test_identifier_option():
129
129
  class OptionConfig(Config):
130
130
  __xpmid__ = "test.identifier.option"
131
131
  a: Param[int]
@@ -139,7 +139,7 @@ def test_param_option():
139
139
  # --- Dictionnary
140
140
 
141
141
 
142
- def test_param_identifier_dict():
142
+ def test_identifier_dict():
143
143
  """Test identifiers of dictionary structures"""
144
144
 
145
145
  class B(Config):
@@ -163,7 +163,7 @@ class TypeWithPath(Config):
163
163
  path: Param[Path]
164
164
 
165
165
 
166
- def test_param_identifier_path():
166
+ def test_identifier_path():
167
167
  """Path should be ignored"""
168
168
  assert_equal(TypeWithPath(a=1, path="/a/b"), TypeWithPath(a=1, path="/c/d"))
169
169
  assert_notequal(TypeWithPath(a=2, path="/a/b"), TypeWithPath(a=1, path="/c/d"))
@@ -172,7 +172,7 @@ def test_param_identifier_path():
172
172
  # --- Test with added arguments
173
173
 
174
174
 
175
- def test_param_identifier_pathoption():
175
+ def test_identifier_pathoption():
176
176
  """Path arguments should be ignored"""
177
177
 
178
178
  class A_with_path(Config):
@@ -187,7 +187,7 @@ def test_param_identifier_pathoption():
187
187
  assert_equal(A_with_path(a=1), A_without_path(a=1))
188
188
 
189
189
 
190
- def test_param_identifier_enum():
190
+ def test_identifier_enum():
191
191
  """test enum parameters"""
192
192
  from enum import Enum
193
193
 
@@ -202,7 +202,7 @@ def test_param_identifier_enum():
202
202
  assert_equal(EnumConfig(a=EnumParam.FIRST), EnumConfig(a=EnumParam.FIRST))
203
203
 
204
204
 
205
- def test_param_identifier_addnone():
205
+ def test_identifier_addnone():
206
206
  """Test the case of new parameter (with None default)"""
207
207
 
208
208
  class B(Config):
@@ -219,7 +219,7 @@ def test_param_identifier_addnone():
219
219
  assert_notequal(A_with_b(b=B(x=1)), A())
220
220
 
221
221
 
222
- def test_param_defaultnew():
222
+ def test_identifier_defaultnew():
223
223
  """Path arguments should be ignored"""
224
224
 
225
225
  class A_with_b(Config):
@@ -236,7 +236,7 @@ def test_param_defaultnew():
236
236
  assert_equal(A_with_b(a=1), A(a=1))
237
237
 
238
238
 
239
- def test_param_taskconfigidentifier():
239
+ def test_identifier_taskconfigidentifier():
240
240
  """Test whether the embedded task arguments make the configuration different"""
241
241
 
242
242
  class MyConfig(Config):
@@ -258,7 +258,7 @@ def test_param_taskconfigidentifier():
258
258
  )
259
259
 
260
260
 
261
- def test_param_constant():
261
+ def test_identifier_constant():
262
262
  """Test if constants are taken into account for signature computation"""
263
263
 
264
264
  class A1(Config):
@@ -278,7 +278,7 @@ def test_param_constant():
278
278
  assert_notequal(A1(), A2())
279
279
 
280
280
 
281
- def test_param_identifier_deprecated_class():
281
+ def test_identifier_deprecated_class():
282
282
  """Test that when submitting the task, the computed identifier is the one of
283
283
  the new class"""
284
284
 
@@ -300,7 +300,7 @@ def test_param_identifier_deprecated_class():
300
300
  )
301
301
 
302
302
 
303
- def test_param_identifier_deprecated_attribute():
303
+ def test_identifier_deprecated_attribute():
304
304
  class Values(Config):
305
305
  values: Param[List[int]] = []
306
306
 
@@ -315,7 +315,7 @@ class MetaA(Config):
315
315
  x: Param[int]
316
316
 
317
317
 
318
- def test_param_identifier_meta():
318
+ def test_identifier_meta():
319
319
  """Test forced meta-parameter"""
320
320
 
321
321
  class B(Config):
@@ -354,7 +354,7 @@ def test_param_identifier_meta():
354
354
  )
355
355
 
356
356
 
357
- def test_param_identifier_meta_default_dict():
357
+ def test_identifier_meta_default_dict():
358
358
  class DictConfig(Config):
359
359
  params: Param[Dict[str, MetaA]] = {}
360
360
 
@@ -370,7 +370,7 @@ def test_param_identifier_meta_default_dict():
370
370
  )
371
371
 
372
372
 
373
- def test_param_identifier_meta_default_array():
373
+ def test_identifier_meta_default_array():
374
374
  class ArrayConfigWithDefault(Config):
375
375
  array: Param[List[MetaA]] = []
376
376
 
@@ -386,37 +386,7 @@ def test_param_identifier_meta_default_array():
386
386
  )
387
387
 
388
388
 
389
- def test_param_identifier_pre_task():
390
- class MyConfig(Config):
391
- pass
392
-
393
- class IdentifierPreLightTask(LightweightTask):
394
- pass
395
-
396
- class IdentifierPreTask(Task):
397
- x: Param[MyConfig]
398
-
399
- task = IdentifierPreTask(x=MyConfig()).submit(run_mode=RunMode.DRY_RUN)
400
- task_with_pre = (
401
- IdentifierPreTask(x=MyConfig())
402
- .add_pretasks(IdentifierPreLightTask())
403
- .submit(run_mode=RunMode.DRY_RUN)
404
- )
405
- task_with_pre_2 = (
406
- IdentifierPreTask(x=MyConfig())
407
- .add_pretasks(IdentifierPreLightTask())
408
- .submit(run_mode=RunMode.DRY_RUN)
409
- )
410
- task_with_pre_3 = IdentifierPreTask(
411
- x=MyConfig().add_pretasks(IdentifierPreLightTask())
412
- ).submit(run_mode=RunMode.DRY_RUN)
413
-
414
- assert_notequal(task, task_with_pre, "No pre-task")
415
- assert_equal(task_with_pre, task_with_pre_2, "Same parameters")
416
- assert_equal(task_with_pre, task_with_pre_3, "Pre-tasks are order-less")
417
-
418
-
419
- def test_param_identifier_init_task():
389
+ def test_identifier_init_task():
420
390
  class MyConfig(Config):
421
391
  pass
422
392
 
@@ -426,26 +396,67 @@ def test_param_identifier_init_task():
426
396
  class IdentifierInitTask2(Task):
427
397
  pass
428
398
 
429
- class IdentierTask(Task):
399
+ class IdentifierTask(Task):
430
400
  x: Param[MyConfig]
431
401
 
432
- task = IdentierTask(x=MyConfig()).submit(run_mode=RunMode.DRY_RUN)
433
- task_with_pre = IdentierTask(x=MyConfig()).submit(
402
+ task = IdentifierTask.C(x=MyConfig.C()).submit(run_mode=RunMode.DRY_RUN)
403
+ task_with_pre = IdentifierTask.C(x=MyConfig.C()).submit(
434
404
  run_mode=RunMode.DRY_RUN,
435
405
  init_tasks=[IdentifierInitTask(), IdentifierInitTask2()],
436
406
  )
437
- task_with_pre_2 = IdentierTask(x=MyConfig()).submit(
407
+ task_with_pre_2 = IdentifierTask.C(x=MyConfig.C()).submit(
438
408
  run_mode=RunMode.DRY_RUN,
439
409
  init_tasks=[IdentifierInitTask(), IdentifierInitTask2()],
440
410
  )
441
- task_with_pre_3 = IdentierTask(x=MyConfig()).submit(
411
+ task_with_pre_3 = IdentifierTask.C(x=MyConfig.C()).submit(
442
412
  run_mode=RunMode.DRY_RUN,
443
413
  init_tasks=[IdentifierInitTask2(), IdentifierInitTask()],
444
414
  )
445
415
 
446
- assert_notequal(task, task_with_pre, "No pre-task")
416
+ assert_notequal(task, task_with_pre, "Should be different with init-task")
447
417
  assert_equal(task_with_pre, task_with_pre_2, "Same parameters")
448
- assert_notequal(task_with_pre, task_with_pre_3, "Same parameters")
418
+ assert_notequal(task_with_pre, task_with_pre_3, "Other parameters")
419
+
420
+
421
+ def test_identifier_init_task_dep():
422
+ class Loader(LightweightTask):
423
+ param1: Param[float]
424
+
425
+ def execute(self):
426
+ pass
427
+
428
+ class FirstTask(Task):
429
+ def task_outputs(self, dep):
430
+ return dep(Loader.C(param1=1))
431
+
432
+ def execute(self):
433
+ pass
434
+
435
+ class SecondTask(Task):
436
+ param3: Param[int]
437
+
438
+ def execute(self):
439
+ pass
440
+
441
+ # Two identical tasks
442
+ task_a_1 = FirstTask.C()
443
+ task_a_2 = FirstTask.C()
444
+ assert_equal(task_a_1, task_a_2)
445
+
446
+ # We process them with two different init tasks
447
+ loader_1 = task_a_1.submit(
448
+ init_tasks=[Loader.C(param1=0.5)], run_mode=RunMode.DRY_RUN
449
+ )
450
+ loader_2 = task_a_2.submit(
451
+ init_tasks=[Loader.C(param1=5)], run_mode=RunMode.DRY_RUN
452
+ )
453
+ assert_notequal(loader_1, loader_2)
454
+
455
+ # Now, we process
456
+ c_1 = SecondTask.C(param3=2).submit(init_tasks=[loader_1], run_mode=RunMode.DRY_RUN)
457
+
458
+ c_2 = SecondTask.C(param3=2).submit(init_tasks=[loader_2], run_mode=RunMode.DRY_RUN)
459
+ assert_notequal(c_1, c_2)
449
460
 
450
461
 
451
462
  # --- Check configuration reloads
@@ -463,7 +474,7 @@ def check_reload(config):
463
474
  new_config = ConfigInformation.fromParameters(
464
475
  data, as_instance=False, discard_id=True
465
476
  )
466
- assert new_config.__xpm__._full_identifier is None
477
+ assert new_config.__xpm__._identifier is None
467
478
  new_identifier = new_config.__xpm__.identifier.all
468
479
 
469
480
  assert new_identifier == old_identifier
@@ -473,7 +484,7 @@ class IdentifierReloadConfig(Config):
473
484
  id: Param[str]
474
485
 
475
486
 
476
- def test_param_identifier_reload_config():
487
+ def test_identifier_reload_config():
477
488
  # Creates the configuration
478
489
  check_reload(IdentifierReloadConfig(id="123"))
479
490
 
@@ -489,7 +500,7 @@ class IdentifierReloadDerived(Config):
489
500
  task: Param[IdentifierReloadConfig]
490
501
 
491
502
 
492
- def test_param_identifier_reload_taskoutput():
503
+ def test_identifier_reload_taskoutput():
493
504
  """When using a task output, the identifier should not be different"""
494
505
 
495
506
  # Creates the configuration
@@ -511,7 +522,7 @@ class IdentifierReloadTaskDerived(Config):
511
522
  other: Param[IdentifierReloadTaskConfig]
512
523
 
513
524
 
514
- def test_param_identifier_reload_task_direct():
525
+ def test_identifier_reload_task_direct():
515
526
  """When using a direct task output, the identifier should not be different"""
516
527
 
517
528
  # Creates the configuration
@@ -522,7 +533,7 @@ def test_param_identifier_reload_task_direct():
522
533
  check_reload(config)
523
534
 
524
535
 
525
- def test_param_identifier_reload_meta():
536
+ def test_identifier_reload_meta():
526
537
  """Test identifier don't change when using meta"""
527
538
  # Creates the configuration
528
539
  task = IdentifierReloadTask(id="123").submit(run_mode=RunMode.DRY_RUN)
@@ -545,10 +556,10 @@ class LoopC(Config):
545
556
  param_b: Param["LoopB"]
546
557
 
547
558
 
548
- def test_param_identifier_loop():
549
- c = LoopC()
550
- b = LoopB(param_c=c)
551
- a = LoopA(param_b=b)
559
+ def test_identifier_loop():
560
+ c = LoopC.C()
561
+ b = LoopB.C(param_c=c)
562
+ a = LoopA.C(param_b=b)
552
563
  c.param_a = a
553
564
  c.param_b = b
554
565
 
@@ -46,18 +46,6 @@ class LoadModel(SerializationLWTask):
46
46
  self.value.initialized = True
47
47
 
48
48
 
49
- def test_instance_serialized():
50
- model = Model()
51
- model.add_pretasks(LoadModel(value=model))
52
- trainer = Evaluator(model=model)
53
- instance = trainer.instance()
54
-
55
- assert isinstance(
56
- instance.model, Model
57
- ), f"The model is not a Model but a {type(instance.model).__qualname__}"
58
- assert instance.model.initialized, "The model was not initialized"
59
-
60
-
61
49
  class ConfigWithOptional(Config):
62
50
  x: Param[int] = 1
63
51
  y: Param[Optional[int]]
@@ -1,71 +1,12 @@
1
1
  from typing import Optional
2
2
  from experimaestro import (
3
3
  Config,
4
- Task,
5
4
  Param,
6
- SerializationLWTask,
7
- copyconfig,
8
5
  state_dict,
9
6
  from_state_dict,
10
7
  )
11
8
  from experimaestro.core.context import SerializationContext
12
9
  from experimaestro.core.objects import ConfigMixin
13
- from experimaestro.tests.utils import TemporaryExperiment
14
-
15
-
16
- class SubModel(Config):
17
- pass
18
-
19
-
20
- class Model(Config):
21
- submodel: Param[SubModel]
22
-
23
- def __post_init__(self):
24
- self.initialized = False
25
- self.submodel.initialized = False
26
-
27
-
28
- class LoadModel(SerializationLWTask):
29
- def execute(self):
30
- self.value.initialized = True
31
- self.value.submodel.initialized = True
32
-
33
-
34
- class Trainer(Task):
35
- model: Param[Config]
36
-
37
- def task_outputs(self, dep):
38
- model = copyconfig(self.model)
39
- return model.add_pretasks(dep(LoadModel(value=model)))
40
-
41
- def execute(self):
42
- assert not self.model.initialized, "Model not initialized"
43
-
44
-
45
- class Evaluate(Task):
46
- model: Param[Config]
47
- is_submodel: Param[bool] = False
48
-
49
- def execute(self):
50
- assert self.model.initialized, "Model not initialized"
51
- if self.is_submodel:
52
- assert isinstance(self.model, SubModel)
53
- else:
54
- assert isinstance(self.model, Model)
55
-
56
-
57
- def test_serializers_xp():
58
- with TemporaryExperiment("serializers", maxwait=20, port=0):
59
- model = Model(submodel=SubModel())
60
- trained_model: Model = Trainer(model=model).submit()
61
-
62
- # Use the model itself
63
- Evaluate(model=trained_model).submit()
64
-
65
- # Use a submodel
66
- Evaluate(model=trained_model.submodel, is_submodel=True).add_pretasks_from(
67
- trained_model
68
- ).submit()
69
10
 
70
11
 
71
12
  class Object1(Config):
@@ -299,26 +299,6 @@ class MyLightweightTask(Task):
299
299
  assert self.x.data == 1
300
300
 
301
301
 
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
302
  def test_task_lightweight_init():
323
303
  with TemporaryExperiment("lightweight_init", maxwait=20):
324
304
  x = LightweightConfig()
@@ -26,7 +26,7 @@ def test_multiple_inheritance():
26
26
 
27
27
  for C in (C1, C2):
28
28
  logging.info("Testing %s", C)
29
- ctype = C.__xpmtype__
29
+ ctype = C.C.__xpmtype__
30
30
  assert issubclass(C, A)
31
31
  assert issubclass(C, B)
32
32
  assert issubclass(C, B1)
@@ -49,7 +49,7 @@ def test_missing_hierarchy():
49
49
  class B(A1):
50
50
  pass
51
51
 
52
- B.__xpmtype__
52
+ B.C.__xpmtype__
53
53
 
54
54
  assert issubclass(B, A)
55
55
  assert issubclass(B, A1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: experimaestro
3
- Version: 1.13.0
3
+ Version: 1.15.0
4
4
  Summary: "Experimaestro is a computer science experiment manager"
5
5
  License: GPL-3
6
6
  License-File: LICENSE
@@ -12,17 +12,17 @@ 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
- experimaestro/core/identifier.py,sha256=JadBAdW2fOIgoTyMRtKI_RY9SXQP2B1vq1rUcV465Hs,10214
18
+ experimaestro/core/identifier.py,sha256=5r2F425yX7Sfeub3EJZCs2Q4Qggifgi4uxMGcpI-ex0,10473
19
19
  experimaestro/core/objects/__init__.py,sha256=ucJY5e17QQ1Kc-GYXeL7g8GFj8rP0XB4g2vrl32uhxY,721
20
- experimaestro/core/objects/config.py,sha256=QDmD82NyWN9OQ_UVSV6WaPd1srKHVXeAmfcEW1Fw9Ps,58080
20
+ experimaestro/core/objects/config.py,sha256=dYDEqB_6WtTK51ECCkBmBQTBkKZbuAOFA-giVpyc1lU,54695
21
21
  experimaestro/core/objects/config_utils.py,sha256=ZLECGkeIWdzunm8vwWsQhvcSgV1e064BgXbLiZnxSEM,1288
22
- experimaestro/core/objects/config_walk.py,sha256=gyDMrVPbBMChn7r4em_gQXuqnxASO_JVauEbnJNO8II,4245
23
- experimaestro/core/objects.pyi,sha256=xvlsRj4u1xsJxbevJl5Ner_HwmxR8x1JlAeIVDJzuy0,6498
22
+ experimaestro/core/objects/config_walk.py,sha256=SYBrGuT7HV12no9mQd1HjnebiyygHyO-zq_TO1brUtc,4233
23
+ experimaestro/core/objects.pyi,sha256=Q1tq4F8LrExPm00E-P5aaygxMvViYZqhHvByo_TTZRE,6315
24
24
  experimaestro/core/serialization.py,sha256=CSPEwOzlDsgAz6V2og-TgyU0RXDtzt_nXaoXFZleDZE,5775
25
- experimaestro/core/serializers.py,sha256=R_CAMyjjfU1oi-eHU6VlEUixJpFayGqEPaYu7VsD9xA,1197
25
+ experimaestro/core/serializers.py,sha256=iOBuASEgD8dRKPnL16iOLBsM0GHChCJgjBd7LixFui4,919
26
26
  experimaestro/core/types.py,sha256=AH3ni1nIKGTeRmc7ECL6GH_dYqiuZp_mAN4E-JqdqnY,21741
27
27
  experimaestro/core/utils.py,sha256=JfC3qGUS9b6FUHc2VxIYUI9ysNpXSQ1LjOBkjfZ8n7o,495
28
28
  experimaestro/exceptions.py,sha256=cUy83WHM3GeynxmMk6QRr5xsnpqUAdAoc-m3KQVrE2o,44
@@ -117,24 +117,24 @@ experimaestro/tests/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
117
117
  experimaestro/tests/tasks/all.py,sha256=OMkHsWZkErCmTajiNO7hNhvnk9eKzJC-VatWgabWlsI,1955
118
118
  experimaestro/tests/tasks/foreign.py,sha256=uhXDOcWozkcm26ybbeEU9RElJpbhjC-zdzmlSKfPcdY,122
119
119
  experimaestro/tests/test_checkers.py,sha256=Kg5frDNRE3pvWVmmYzyk0tJFNO885KOrK48lSu-NlYA,403
120
- experimaestro/tests/test_dependencies.py,sha256=xfWrSkvjT45G4FSCL535m1huLT2ghmyW7kvP_XvvCJQ,2005
120
+ experimaestro/tests/test_dependencies.py,sha256=fEmBNQaEkP8xb_P_5EfKIJflL-NCeSOOZk_FumlgBC0,1870
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=Cx0jDI8JA_tFsEfg9EkV353iKxBd6j_1560p_66_oNU,1040
125
- experimaestro/tests/test_identifier.py,sha256=zgfpz5UumQftQTHhdw3nmr044Xd7Ntrh7e2hIAoS_PA,13862
126
- experimaestro/tests/test_instance.py,sha256=h-8UeoOlNsD-STWq5jbhmxw35CyiwJxtKAaUGmLPgB8,1521
124
+ experimaestro/tests/test_generators.py,sha256=R0UypTzxX0dPYvY4A_kozpLDHhGzQDNfLQyc0oRaSx8,2891
125
+ experimaestro/tests/test_identifier.py,sha256=REFB9RGyVlSdAPMpmvJ9a0WbAoe6ZTR1xl73lqtRFU0,14022
126
+ experimaestro/tests/test_instance.py,sha256=xLdSNVo_vAEaOWmjik1tAOYgyOkJPbmnwopyuDEQm1A,1147
127
127
  experimaestro/tests/test_objects.py,sha256=zycxjvWuJAbPR8-q2T3zuBY9xfmlhf1YvtOcrImHxnc,2431
128
128
  experimaestro/tests/test_outputs.py,sha256=DYzPk5TT_yLumy8SsQbl-S66ivVxJ-ERFrZ68KQZ4KU,781
129
129
  experimaestro/tests/test_param.py,sha256=7BltCUm9uhtEupJZ-QSaj7MxuXoUl73NjE8NfzJD5BA,7290
130
130
  experimaestro/tests/test_progress.py,sha256=wtIGQzlV3ldd_wMng11LinVESchW-1J954mCJNlG28E,7580
131
- experimaestro/tests/test_serializers.py,sha256=u3A5JbusN8aW6x6reUIwpU1Fs-avnzTRU7WuTear9Qs,2722
131
+ experimaestro/tests/test_serializers.py,sha256=dQkiuvHAQ1g-SCRCfOy977nQMWR7CFuBUud65N_vfiI,1248
132
132
  experimaestro/tests/test_snippets.py,sha256=rojnyDjtmAMnSuDUj6Bv9XEgdP8oQf2nVc132JF8vsM,3081
133
133
  experimaestro/tests/test_ssh.py,sha256=KS1NWltiXrJBSStY9d4mwrexeqgNGWmhxuAU_WLQDAU,1449
134
134
  experimaestro/tests/test_tags.py,sha256=v_8_7LuEHfY_gfa0JRCUkmgJh8h6RMya_nd5NcPAJPw,2852
135
- experimaestro/tests/test_tasks.py,sha256=bUSB_UT1MTN2P_RPHd4AT5NK-DFsgCVeFKSiXu3bEz8,9429
135
+ experimaestro/tests/test_tasks.py,sha256=eka9LiOMTp0LhRxSM4Khw1jAner6wgjF8sNDOHSqwnQ,8781
136
136
  experimaestro/tests/test_tokens.py,sha256=U3nKBL1KftWhmk3dQZZLQd-MLJL_SscMjI3zMieMHfc,7823
137
- experimaestro/tests/test_types.py,sha256=HVOu05ivOQB0Y7iBLVBg1J_FKg9E49pbU3yk0jvWA6U,1301
137
+ experimaestro/tests/test_types.py,sha256=CVyAyiWgEeV1FpWc-oC8TUqGN4o-9eVBrv75PHvWapA,1305
138
138
  experimaestro/tests/test_validation.py,sha256=t0fu-5wkCzpUKLu8K0rbU5zDE_LtsgehWm84HtIF5JY,4076
139
139
  experimaestro/tests/token_reschedule.py,sha256=V8lAbjTWTatBrBjxde_KN-fDEI4sQ3HNr4scCXBU6fI,1701
140
140
  experimaestro/tests/utils.py,sha256=41krZFgUaCxCYBQPmo5dNFDd9W6RU8ZzzyzY3FyutUI,3805
@@ -152,8 +152,8 @@ experimaestro/utils/multiprocessing.py,sha256=am3DkHP_kmWbpynbck2c9QystCUtPBoSAC
152
152
  experimaestro/utils/resources.py,sha256=j-nvsTFwmgENMoVGOD2Ap-UD3WU85WkI0IgeSszMCX4,1328
153
153
  experimaestro/utils/settings.py,sha256=jpFMqF0DLL4_P1xGal0zVR5cOrdD8O0Y2IOYvnRgN3k,793
154
154
  experimaestro/xpmutils.py,sha256=S21eMbDYsHfvmZ1HmKpq5Pz5O-1HnCLYxKbyTBbASyQ,638
155
- experimaestro-1.13.0.dist-info/METADATA,sha256=UbZQxb19lKM9jI8HI8aZli0ZmOck1UflmQmN6VHvDEI,5705
156
- experimaestro-1.13.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
157
- experimaestro-1.13.0.dist-info/entry_points.txt,sha256=TppTNiz5qm5xm1fhAcdLKdCLMrlL-eQggtCrCI00D9c,446
158
- experimaestro-1.13.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
159
- experimaestro-1.13.0.dist-info/RECORD,,
155
+ experimaestro-1.15.0.dist-info/METADATA,sha256=c6VFYeeH9cN7JxYxGve-CJN-w45gvmxQnruB4q88WY4,5705
156
+ experimaestro-1.15.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
157
+ experimaestro-1.15.0.dist-info/entry_points.txt,sha256=TppTNiz5qm5xm1fhAcdLKdCLMrlL-eQggtCrCI00D9c,446
158
+ experimaestro-1.15.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
159
+ experimaestro-1.15.0.dist-info/RECORD,,