experimaestro 2.0.0a1__py3-none-any.whl → 2.0.0a4__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.

@@ -16,7 +16,7 @@ from experimaestro.utils import logger
16
16
  from experimaestro.locking import Lock
17
17
  from experimaestro.tokens import Token
18
18
  from experimaestro.utils.asyncio import asyncThreadcheck
19
- import pkg_resources
19
+ from importlib.metadata import entry_points
20
20
 
21
21
 
22
22
  class RedirectType(enum.Enum):
@@ -101,7 +101,7 @@ class Process:
101
101
  """Get a handler"""
102
102
  if Process.HANDLERS is None:
103
103
  Process.HANDLERS = {}
104
- for ep in pkg_resources.iter_entry_points(group="experimaestro.process"):
104
+ for ep in entry_points(group="experimaestro.process"):
105
105
  logging.debug("Adding process handler for type %s", ep.name)
106
106
  handler = ep.load()
107
107
  Process.HANDLERS[ep.name] = handler
@@ -80,10 +80,13 @@ 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
89
+
87
90
  if default.default is not None:
88
91
  self.default = default.default
89
92
  elif default.default_factory is not None:
@@ -184,13 +187,29 @@ DataPath = Annotated[Path, dataHint]
184
187
  class field:
185
188
  """Extra information for a given experimaestro field (param or meta)"""
186
189
 
187
- def __init__(self, *, default: Any = None, default_factory: Callable = None):
190
+ def __init__(
191
+ self,
192
+ *,
193
+ default: Any = None,
194
+ default_factory: Callable = None,
195
+ ignore_generated=False,
196
+ ):
197
+ """Gives some extra per-field information
198
+
199
+ :param default: a default value, defaults to None
200
+ :param default_factory: a default factory for values, defaults to None
201
+ :param ignore_generated: True if the value is hidden – it won't be accessible in
202
+ tasks, defaults to False. The interest of hidden is to add a
203
+ configuration field that changes the identifier, but will not be
204
+ used.
205
+ """
188
206
  assert not (
189
207
  (default is not None) and (default_factory is not None)
190
208
  ), "default and default_factory are mutually exclusive options"
191
209
 
192
210
  self.default_factory = default_factory
193
211
  self.default = default
212
+ self.ignore_generated = ignore_generated
194
213
 
195
214
 
196
215
  class help(TypeAnnotation):
@@ -169,10 +169,33 @@ class ConfigInformation:
169
169
  self._sealed = False
170
170
  self._meta = None
171
171
 
172
- #: This flags is True when a value in this configuration,
173
- #: or any sub-configuration, is generated. This prevents problem
174
- #: when a configuration with generated values is re-used
175
- self._has_generated_value = False
172
+ # This contains the list of generated values (using context) in this
173
+ # configuration or any sub-configuration, is generated. This prevents
174
+ # problem when a configuration with generated values is re-used.
175
+ self._generated_values = []
176
+
177
+ def get_generated_paths(
178
+ self, path: list[str] = None, paths: list[str] = None
179
+ ) -> list[str]:
180
+ """Get the list of generated paths, useful to track down those
181
+
182
+ :param path: The current path
183
+ :param paths: The list of generated paths so far, defaults to None
184
+ :return: The full list of generated paths
185
+ """
186
+ paths = [] if paths is None else paths
187
+ path = [] if path is None else path
188
+
189
+ for key in self._generated_values:
190
+ value = self.values[key]
191
+ if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
192
+ path.append(key)
193
+ value.__xpm__.get_generated_paths(path, paths)
194
+ path.pop()
195
+ else:
196
+ paths.append(".".join(path + [key]))
197
+
198
+ return paths
176
199
 
177
200
  def set_meta(self, value: Optional[bool]):
178
201
  """Sets the meta flag"""
@@ -191,6 +214,31 @@ class ConfigInformation:
191
214
  # Not an argument, bypass
192
215
  return object.__getattribute__(self.pyobject, name)
193
216
 
217
+ @staticmethod
218
+ def is_generated_value(argument, value):
219
+ if argument.ignore_generated:
220
+ return False
221
+
222
+ if value is None:
223
+ return False
224
+
225
+ if isinstance(value, (int, str, float, bool, Path)):
226
+ return False
227
+
228
+ if isinstance(value, ConfigMixin):
229
+ return value.__xpm__._generated_values and value.__xpm__.task is None
230
+
231
+ if isinstance(value, list):
232
+ return any(ConfigInformation.is_generated_value(argument, x) for x in value)
233
+
234
+ if isinstance(value, dict):
235
+ return any(
236
+ ConfigInformation.is_generated_value(argument, x)
237
+ for x in value.values()
238
+ )
239
+
240
+ assert False, f"Cannot handle values of type {type(value)}"
241
+
194
242
  def set(self, k, v, bypass=False):
195
243
  from experimaestro.generators import Generator
196
244
 
@@ -207,18 +255,16 @@ class ConfigInformation:
207
255
  "Configuration (and not objects) should be used. Consider using .C(...)"
208
256
  )
209
257
 
210
- if (
211
- isinstance(v, ConfigMixin)
212
- and v.__xpm__._has_generated_value
213
- and v.__xpm__.task is None
214
- ):
215
- raise AttributeError(
216
- f"Cannot set {k} to a configuration with generated values"
217
- )
218
-
219
258
  try:
220
259
  argument = self.xpmtype.arguments.get(k, None)
221
260
  if argument:
261
+ if ConfigInformation.is_generated_value(argument, v):
262
+ raise AttributeError(
263
+ f"Cannot set {k} to a configuration with generated values. "
264
+ "Here is the list of paths to help you: "
265
+ f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
266
+ )
267
+
222
268
  if not bypass and (
223
269
  (isinstance(argument.generator, Generator)) or argument.constant
224
270
  ):
@@ -344,14 +390,15 @@ class ConfigInformation:
344
390
  Arguments:
345
391
  - context: the generation context
346
392
  """
347
- subconfigs = [
348
- v.__xpm__
349
- for v in self.values.values()
350
- if isinstance(v, Config) and v.__xpm__.task is None
351
- ]
352
-
353
- if any(v._has_generated_value for v in subconfigs):
354
- raise AttributeError("Cannot seal a configuration with generated values")
393
+ if generated_keys := [
394
+ k
395
+ for k, v in self.values.items()
396
+ if ConfigInformation.is_generated_value(self.xpmtype.arguments[k], v)
397
+ ]:
398
+ raise AttributeError(
399
+ "Cannot seal a configuration with generated values:"
400
+ f"""{",".join(generated_keys)} in {context.currentpath}"""
401
+ )
355
402
 
356
403
  class Sealer(ConfigWalk):
357
404
  def preprocess(self, config: ConfigMixin):
@@ -375,13 +422,18 @@ class ConfigInformation:
375
422
  if len(sig.parameters) == 0:
376
423
  value = argument.generator()
377
424
  elif len(sig.parameters) == 2:
425
+ # Only in that case do we need to flag this configuration
426
+ # as containing generated values
427
+ if not argument.ignore_generated:
428
+ config.__xpm__._generated_values.append(k)
429
+ else:
430
+ logging.warning("Ignoring %s", k)
378
431
  value = argument.generator(self.context, config)
379
432
  else:
380
433
  assert (
381
434
  False
382
435
  ), "generator has either two parameters (context and config), or none"
383
436
  config.__xpm__.set(k, value, bypass=True)
384
- config.__xpm__._has_generated_value = True
385
437
  else:
386
438
  value = config.__xpm__.values.get(k)
387
439
  except Exception:
@@ -392,11 +444,14 @@ class ConfigInformation:
392
444
 
393
445
  # Propagate the generated value flag
394
446
  if (
395
- (value is not None)
447
+ value is not None
396
448
  and isinstance(value, ConfigMixin)
397
- and value.__xpm__._has_generated_value
449
+ and value.__xpm__._generated_values
398
450
  ):
399
- self._has_generated_value = True
451
+ if not argument.ignore_generated:
452
+ config.__xpm__._generated_values.append(k)
453
+ else:
454
+ logging.warning("Ignoring %s", k)
400
455
 
401
456
  config.__xpm__._sealed = True
402
457
 
@@ -889,6 +944,7 @@ class ConfigInformation:
889
944
  "workspace": str(context.workspace.path.absolute()),
890
945
  "tags": {key: value for key, value in self.tags().items()},
891
946
  "version": 2,
947
+ "experimaestro": experimaestro.__version__,
892
948
  "objects": self.__get_objects__([], context),
893
949
  },
894
950
  out,
@@ -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
@@ -1 +1,18 @@
1
- from .base import *
1
+ from .base import Scheduler, Listener
2
+ from .workspace import Workspace, RunMode
3
+ from .experiment import experiment, FailedExperiment
4
+ from .jobs import Job, JobState, JobFailureStatus, JobDependency, JobContext
5
+
6
+ __all__ = [
7
+ "Scheduler",
8
+ "Listener",
9
+ "Workspace",
10
+ "RunMode",
11
+ "experiment",
12
+ "FailedExperiment",
13
+ "Job",
14
+ "JobState",
15
+ "JobFailureStatus",
16
+ "JobDependency",
17
+ "JobContext",
18
+ ]