experimaestro 1.11.1__py3-none-any.whl → 2.0.0rc0__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 (37) hide show
  1. experimaestro/annotations.py +1 -1
  2. experimaestro/cli/__init__.py +10 -11
  3. experimaestro/cli/progress.py +269 -0
  4. experimaestro/core/identifier.py +11 -2
  5. experimaestro/core/objects/config.py +64 -94
  6. experimaestro/core/types.py +35 -57
  7. experimaestro/launcherfinder/registry.py +3 -3
  8. experimaestro/mkdocs/base.py +6 -8
  9. experimaestro/notifications.py +12 -3
  10. experimaestro/progress.py +406 -0
  11. experimaestro/settings.py +4 -2
  12. experimaestro/tests/launchers/common.py +2 -2
  13. experimaestro/tests/restart.py +1 -1
  14. experimaestro/tests/test_checkers.py +2 -2
  15. experimaestro/tests/test_dependencies.py +12 -12
  16. experimaestro/tests/test_experiment.py +3 -3
  17. experimaestro/tests/test_file_progress.py +425 -0
  18. experimaestro/tests/test_file_progress_integration.py +477 -0
  19. experimaestro/tests/test_generators.py +61 -0
  20. experimaestro/tests/test_identifier.py +90 -81
  21. experimaestro/tests/test_instance.py +9 -9
  22. experimaestro/tests/test_objects.py +9 -32
  23. experimaestro/tests/test_outputs.py +6 -6
  24. experimaestro/tests/test_param.py +14 -14
  25. experimaestro/tests/test_progress.py +4 -4
  26. experimaestro/tests/test_serializers.py +5 -5
  27. experimaestro/tests/test_tags.py +15 -15
  28. experimaestro/tests/test_tasks.py +40 -36
  29. experimaestro/tests/test_tokens.py +8 -6
  30. experimaestro/tests/test_types.py +10 -10
  31. experimaestro/tests/test_validation.py +19 -19
  32. experimaestro/tests/token_reschedule.py +1 -1
  33. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/METADATA +1 -1
  34. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/RECORD +37 -32
  35. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/LICENSE +0 -0
  36. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/WHEEL +0 -0
  37. {experimaestro-1.11.1.dist-info → experimaestro-2.0.0rc0.dist-info}/entry_points.txt +0 -0
@@ -203,18 +203,14 @@ class ObjectType(Type):
203
203
  """ObjectType contains class-level information about
204
204
  experimaestro configurations and tasks
205
205
 
206
- :param objecttype: The python Type of the associated object
207
- :param configtype: The python Type of the configuration object that uses
208
- property for arguments
206
+ :param value_type: The Python type of the associated object
207
+ :param config_type: The Python type of the configuration object
209
208
  """
210
209
 
211
- # Those entries should not be copied in the __dict__
212
- FORBIDDEN_KEYS = set(("__dict__", "__weakref__"))
213
-
214
210
  def __init__(
215
211
  self,
216
212
  tp: type,
217
- identifier: Union[str, Identifier] = None,
213
+ identifier: Union[str, Identifier, None] = None,
218
214
  ):
219
215
  """Creates a type"""
220
216
  from .objects import Config, ConfigMixin
@@ -225,7 +221,7 @@ class ObjectType(Type):
225
221
  self._title = None
226
222
  self.submit_hooks = set()
227
223
 
228
- # Get the identifier
224
+ # --- Get the identifier
229
225
  if identifier is None and hasattr(tp, "__xpmid__"):
230
226
  __xpmid__ = getattr(tp, "__xpmid__")
231
227
  if isinstance(__xpmid__, Identifier):
@@ -250,59 +246,41 @@ class ObjectType(Type):
250
246
  # --- Creates the config type and not config type
251
247
 
252
248
  self.originaltype = tp
253
- if not issubclass(tp, Config):
254
- # Adds Config as a base class if not present
255
- __bases__ = () if tp.__bases__ == (object,) else tp.__bases__
256
- __dict__ = dict(tp.__dict__)
257
-
258
- __dict__ = {
259
- key: value
260
- for key, value in tp.__dict__.items()
261
- if key not in ObjectType.FORBIDDEN_KEYS
262
- }
263
- self.basetype = type(tp.__name__, (Config,) + __bases__, __dict__)
264
- self.basetype.__module__ = tp.__module__
265
- self.basetype.__qualname__ = tp.__qualname__
266
- else:
267
- self.basetype = tp
249
+ assert issubclass(tp, Config)
250
+ self.value_type = tp
268
251
 
269
252
  # --- Create the type-specific configuration class (XPMConfig)
270
253
  __configbases__ = tuple(
271
- s.__getxpmtype__().configtype
254
+ s.__getxpmtype__().config_type
272
255
  for s in tp.__bases__
273
256
  if issubclass(s, Config) and (s is not Config)
274
257
  ) or (ConfigMixin,)
275
258
 
276
- *tp_qual, tp_name = self.basetype.__qualname__.split(".")
277
- self.configtype = type(
278
- f"{tp_name}.XPMConfig", __configbases__ + (self.basetype,), {}
259
+ *tp_qual, tp_name = self.value_type.__qualname__.split(".")
260
+ self.config_type = type(
261
+ f"{tp_name}.XPMConfig", __configbases__ + (self.value_type,), {}
279
262
  )
280
- self.configtype.__qualname__ = ".".join(tp_qual + [self.configtype.__name__])
281
- self.configtype.__module__ = tp.__module__
263
+ self.config_type.__qualname__ = ".".join(tp_qual + [self.config_type.__name__])
264
+ self.config_type.__module__ = tp.__module__
282
265
 
283
- # Return type is used by tasks to change the output
284
- if hasattr(self.basetype, "task_outputs") or False:
266
+ # --- Get the return type
267
+ if hasattr(self.value_type, "task_outputs") or False:
285
268
  self.returntype = get_type_hints(
286
- getattr(self.basetype, "task_outputs")
269
+ getattr(self.value_type, "task_outputs")
287
270
  ).get("return", typing.Any)
288
271
  else:
289
- self.returntype = self.basetype
272
+ self.returntype = self.value_type
290
273
 
291
- # Registers ourselves
292
- self.basetype.__xpmtype__ = self
293
- self.configtype.__xpmtype__ = self
274
+ # --- Registers ourselves
275
+ self.value_type.__xpmtype__ = self
276
+ self.config_type.__xpmtype__ = self
294
277
 
295
- # Other initializations
278
+ # --- Other initializations
296
279
  self.__initialized__ = False
297
280
  self._runtype = None
298
281
  self.annotations = []
299
282
  self._deprecated = False
300
283
 
301
- @property
302
- def objecttype(self):
303
- """Returns the object type"""
304
- return self.basetype.XPMValue
305
-
306
284
  def addAnnotation(self, annotation):
307
285
  assert not self.__initialized__
308
286
  self.annotations.append(annotation)
@@ -357,15 +335,15 @@ class ObjectType(Type):
357
335
  # Add task
358
336
  if self.taskcommandfactory is not None:
359
337
  self.task = self.taskcommandfactory(self)
360
- elif issubclass(self.basetype, Task):
338
+ elif issubclass(self.value_type, Task):
361
339
  self.task = self.getpythontaskcommand()
362
340
 
363
341
  # Add arguments from type hints
364
342
  from .arguments import TypeAnnotation
365
343
 
366
- if hasattr(self.basetype, "__annotations__"):
367
- typekeys = set(self.basetype.__dict__.get("__annotations__", {}).keys())
368
- hints = get_type_hints(self.basetype, include_extras=True)
344
+ if hasattr(self.value_type, "__annotations__"):
345
+ typekeys = set(self.value_type.__dict__.get("__annotations__", {}).keys())
346
+ hints = get_type_hints(self.value_type, include_extras=True)
369
347
  for key, typehint in hints.items():
370
348
  # Filter out hints from parent classes
371
349
  if key in typekeys:
@@ -378,19 +356,19 @@ class ObjectType(Type):
378
356
  try:
379
357
  self.addArgument(
380
358
  options.create(
381
- key, self.objecttype, typehint.__args__[0]
359
+ key, self.value_type, typehint.__args__[0]
382
360
  )
383
361
  )
384
362
  except Exception:
385
363
  logger.error(
386
364
  "while adding argument %s of %s",
387
365
  key,
388
- self.objecttype,
366
+ self.value_type,
389
367
  )
390
368
  raise
391
369
 
392
370
  def name(self):
393
- return f"{self.basetype.__module__}.{self.basetype.__qualname__}"
371
+ return f"{self.value_type.__module__}.{self.value_type.__qualname__}"
394
372
 
395
373
  def __parsedoc__(self):
396
374
  """Parse the documentation"""
@@ -400,7 +378,7 @@ class ObjectType(Type):
400
378
  self.__initialize__()
401
379
 
402
380
  # Get description from documentation
403
- __doc__ = self.basetype.__dict__.get("__doc__", None)
381
+ __doc__ = self.value_type.__dict__.get("__doc__", None)
404
382
  if __doc__:
405
383
  parseddoc = parse(__doc__)
406
384
  self._title = parseddoc.short_description
@@ -430,7 +408,7 @@ class ObjectType(Type):
430
408
  argname = None
431
409
 
432
410
  def deprecate(self):
433
- if len(self.basetype.__bases__) != 1:
411
+ if len(self.value_type.__bases__) != 1:
434
412
  raise RuntimeError(
435
413
  "Deprecated configurations must have "
436
414
  "only one parent (the new configuration)"
@@ -439,7 +417,7 @@ class ObjectType(Type):
439
417
 
440
418
  # Uses the parent identifier (and saves the deprecated one for path updates)
441
419
  self._deprecated_identifier = self.identifier
442
- parent = self.basetype.__bases__[0].__getxpmtype__()
420
+ parent = self.value_type.__bases__[0].__getxpmtype__()
443
421
  self.identifier = parent.identifier
444
422
  self._deprecated = True
445
423
 
@@ -454,7 +432,7 @@ class ObjectType(Type):
454
432
  return self._description
455
433
 
456
434
  @property
457
- def title(self) -> Dict[str, Argument]:
435
+ def title(self) -> str:
458
436
  self.__parsedoc__()
459
437
  return self._title or str(self.identifier)
460
438
 
@@ -469,7 +447,7 @@ class ObjectType(Type):
469
447
 
470
448
  # The the attribute for the config type
471
449
  setattr(
472
- self.configtype,
450
+ self.config_type,
473
451
  argument.name,
474
452
  property(
475
453
  lambda _self: _self.__xpm__.get(argument.name),
@@ -488,7 +466,7 @@ class ObjectType(Type):
488
466
  def parents(self) -> Iterator["ObjectType"]:
489
467
  from .objects import Config, Task
490
468
 
491
- for tp in self.basetype.__bases__:
469
+ for tp in self.value_type.__bases__:
492
470
  if issubclass(tp, Config) and tp not in [Config, Task]:
493
471
  yield tp.__xpmtype__
494
472
 
@@ -504,7 +482,7 @@ class ObjectType(Type):
504
482
  if not isinstance(value, Config):
505
483
  raise ValueError(f"{value} is not an experimaestro type or task")
506
484
 
507
- types = self.basetype
485
+ types = self.value_type
508
486
 
509
487
  if not isinstance(value, types):
510
488
  raise ValueError(
@@ -519,7 +497,7 @@ class ObjectType(Type):
519
497
 
520
498
  def fullyqualifiedname(self) -> str:
521
499
  """Returns the fully qualified (Python) name"""
522
- return f"{self.basetype.__module__}.{self.basetype.__qualname__}"
500
+ return f"{self.value_type.__module__}.{self.value_type.__qualname__}"
523
501
 
524
502
 
525
503
  class TypeProxy:
@@ -6,7 +6,7 @@ from typing import ClassVar, Dict, Optional, Set, Type, Union
6
6
  from pathlib import Path
7
7
  import typing
8
8
  from omegaconf import DictConfig, OmegaConf, SCMode
9
- import pkg_resources
9
+ from importlib.metadata import entry_points
10
10
  from experimaestro.utils import logger
11
11
  from .base import ConnectorConfiguration, TokenConfiguration
12
12
  from .specs import HostRequirement, RequirementUnion
@@ -75,10 +75,10 @@ class LauncherRegistry:
75
75
  self.find_launcher_fn = None
76
76
 
77
77
  # Use entry points for connectors and launchers
78
- for entry_point in pkg_resources.iter_entry_points("experimaestro.connectors"):
78
+ for entry_point in entry_points(group="experimaestro.connectors"):
79
79
  entry_point.load().init_registry(self)
80
80
 
81
- for entry_point in pkg_resources.iter_entry_points("experimaestro.tokens"):
81
+ for entry_point in entry_points(group="experimaestro.tokens"):
82
82
  entry_point.load().init_registry(self)
83
83
 
84
84
  # Register the find launcher function if it exists
@@ -4,12 +4,11 @@ See https://www.mkdocs.org/user-guide/plugins/ for plugin API documentation
4
4
  """
5
5
 
6
6
  from collections import defaultdict
7
- import functools
8
7
  import re
9
8
  from experimaestro.mkdocs.annotations import shoulddocument
10
9
  import requests
11
10
  from urllib.parse import urljoin
12
- from experimaestro.core.types import ObjectType, Type
11
+ from experimaestro.core.types import ObjectType
13
12
  import mkdocs
14
13
  from pathlib import Path
15
14
  from typing import Dict, Iterator, List, Optional, Set, Tuple, Type as TypingType
@@ -76,7 +75,7 @@ class ObjectLatticeNode:
76
75
  return f"node({self.objecttype.identifier})"
77
76
 
78
77
  def isAncestor(self, other):
79
- return issubclass(self.objecttype.configtype, other.objecttype.configtype)
78
+ return issubclass(self.objecttype.config_type, other.objecttype.config_type)
80
79
 
81
80
  def _addChild(self, child: "ObjectLatticeNode"):
82
81
  child.parents.add(self)
@@ -321,7 +320,7 @@ class Documentation(mkdocs.plugins.BasePlugin):
321
320
 
322
321
  for node in self.lattice.iter_all():
323
322
  if node.objecttype is not None:
324
- member = node.objecttype.basetype
323
+ member = node.objecttype.value_type
325
324
  qname = f"{member.__module__}.{member.__qualname__}"
326
325
  path = self.type2path[qname]
327
326
 
@@ -354,7 +353,7 @@ class Documentation(mkdocs.plugins.BasePlugin):
354
353
  # Now, sort according to descendant/ascendant relationship or name
355
354
  nodes = set()
356
355
  for _node in cfgs:
357
- if issubclass(_node.objecttype.configtype, xpmtype.configtype):
356
+ if issubclass(_node.objecttype.config_type, xpmtype.config_type):
358
357
  nodes.add(_node)
359
358
 
360
359
  # Removes so they are not generated twice
@@ -443,11 +442,10 @@ class Documentation(mkdocs.plugins.BasePlugin):
443
442
  lines.append("\n\n")
444
443
 
445
444
  for name, argument in xpminfo.arguments.items():
446
-
447
445
  if isinstance(argument.type, ObjectType):
448
- basetype = argument.type.basetype
446
+ value_type = argument.type.value_type
449
447
  typestr = self.getlink(
450
- page.url, f"{basetype.__module__}.{basetype.__qualname__}"
448
+ page.url, f"{value_type.__module__}.{value_type.__qualname__}"
451
449
  )
452
450
  else:
453
451
  typestr = argument.type.name()
@@ -12,6 +12,7 @@ from tqdm.auto import tqdm as std_tqdm
12
12
 
13
13
  from .utils import logger
14
14
  from experimaestro.taskglobals import Env as TaskEnv
15
+ from .progress import FileBasedProgressReporter
15
16
 
16
17
  # --- Progress and other notifications
17
18
 
@@ -41,7 +42,7 @@ class LevelInformation:
41
42
  return result
42
43
 
43
44
  def __repr__(self) -> str:
44
- return f"[{self.level}] {self.desc} {int(self.progress*1000)/10}%"
45
+ return f"[{self.level}] {self.desc} {int(self.progress * 1000) / 10}%"
45
46
 
46
47
 
47
48
  class ListenerInformation:
@@ -79,10 +80,14 @@ class Reporter(threading.Thread):
79
80
  self.progress_threshold = 0.01
80
81
  self.cv = threading.Condition()
81
82
 
83
+ # File-based progress reporter
84
+ self.file_reporter = FileBasedProgressReporter(task_path=path)
85
+
82
86
  def stop(self):
83
87
  self.stopping = True
84
88
  with self.cv:
85
- self.cv.notifyAll()
89
+ # self.cv.notifyAll()
90
+ self.cv.notify_all()
86
91
 
87
92
  @staticmethod
88
93
  def isfatal_httperror(e: Exception, info: ListenerInformation) -> bool:
@@ -186,7 +191,7 @@ class Reporter(threading.Thread):
186
191
  try:
187
192
  with urlopen(url) as _:
188
193
  logger.debug(
189
- "EOJ botification sent for %s",
194
+ "EOJ notification sent for %s",
190
195
  baseurl,
191
196
  )
192
197
  except Exception:
@@ -194,6 +199,8 @@ class Reporter(threading.Thread):
194
199
  "Could not report EOJ",
195
200
  )
196
201
 
202
+ self.file_reporter.eoj()
203
+
197
204
  def set_progress(
198
205
  self, progress: float, level: int, desc: Optional[str], console=False
199
206
  ):
@@ -212,6 +219,8 @@ class Reporter(threading.Thread):
212
219
  self.levels[level].desc = desc
213
220
  self.levels[level].progress = progress
214
221
 
222
+ self.file_reporter.set_progress(progress, level, desc)
223
+
215
224
  self.cv.notify_all()
216
225
 
217
226
  INSTANCE: ClassVar[Optional["Reporter"]] = None