experimaestro 1.5.1__py3-none-any.whl → 2.0.0a8__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 (118) hide show
  1. experimaestro/__init__.py +14 -4
  2. experimaestro/__main__.py +3 -423
  3. experimaestro/annotations.py +14 -4
  4. experimaestro/cli/__init__.py +311 -0
  5. experimaestro/{filter.py → cli/filter.py} +23 -9
  6. experimaestro/cli/jobs.py +268 -0
  7. experimaestro/cli/progress.py +269 -0
  8. experimaestro/click.py +0 -35
  9. experimaestro/commandline.py +3 -7
  10. experimaestro/connectors/__init__.py +29 -14
  11. experimaestro/connectors/local.py +19 -10
  12. experimaestro/connectors/ssh.py +27 -8
  13. experimaestro/core/arguments.py +45 -3
  14. experimaestro/core/callbacks.py +52 -0
  15. experimaestro/core/context.py +8 -9
  16. experimaestro/core/identifier.py +310 -0
  17. experimaestro/core/objects/__init__.py +44 -0
  18. experimaestro/core/{objects.py → objects/config.py} +399 -772
  19. experimaestro/core/objects/config_utils.py +58 -0
  20. experimaestro/core/objects/config_walk.py +151 -0
  21. experimaestro/core/objects.pyi +15 -45
  22. experimaestro/core/serialization.py +63 -9
  23. experimaestro/core/serializers.py +1 -8
  24. experimaestro/core/types.py +104 -66
  25. experimaestro/experiments/cli.py +154 -72
  26. experimaestro/experiments/configuration.py +10 -1
  27. experimaestro/generators.py +6 -1
  28. experimaestro/ipc.py +4 -1
  29. experimaestro/launcherfinder/__init__.py +1 -1
  30. experimaestro/launcherfinder/base.py +2 -18
  31. experimaestro/launcherfinder/parser.py +8 -3
  32. experimaestro/launcherfinder/registry.py +52 -140
  33. experimaestro/launcherfinder/specs.py +49 -10
  34. experimaestro/launchers/direct.py +0 -47
  35. experimaestro/launchers/slurm/base.py +54 -14
  36. experimaestro/mkdocs/__init__.py +1 -1
  37. experimaestro/mkdocs/base.py +6 -8
  38. experimaestro/notifications.py +38 -12
  39. experimaestro/progress.py +406 -0
  40. experimaestro/run.py +24 -3
  41. experimaestro/scheduler/__init__.py +18 -1
  42. experimaestro/scheduler/base.py +108 -808
  43. experimaestro/scheduler/dynamic_outputs.py +184 -0
  44. experimaestro/scheduler/experiment.py +387 -0
  45. experimaestro/scheduler/jobs.py +475 -0
  46. experimaestro/scheduler/signal_handler.py +32 -0
  47. experimaestro/scheduler/state.py +75 -0
  48. experimaestro/scheduler/workspace.py +27 -8
  49. experimaestro/scriptbuilder.py +18 -3
  50. experimaestro/server/__init__.py +36 -5
  51. experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  52. experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  53. experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  54. experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  55. experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  56. experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  57. experimaestro/server/data/index.css +5187 -5068
  58. experimaestro/server/data/index.css.map +1 -1
  59. experimaestro/server/data/index.js +68887 -68064
  60. experimaestro/server/data/index.js.map +1 -1
  61. experimaestro/settings.py +45 -5
  62. experimaestro/sphinx/__init__.py +7 -17
  63. experimaestro/taskglobals.py +7 -2
  64. experimaestro/tests/core/__init__.py +0 -0
  65. experimaestro/tests/core/test_generics.py +206 -0
  66. experimaestro/tests/definitions_types.py +5 -3
  67. experimaestro/tests/launchers/bin/sbatch +34 -7
  68. experimaestro/tests/launchers/bin/srun +5 -0
  69. experimaestro/tests/launchers/common.py +17 -5
  70. experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
  71. experimaestro/tests/restart.py +10 -5
  72. experimaestro/tests/tasks/all.py +23 -10
  73. experimaestro/tests/tasks/foreign.py +2 -4
  74. experimaestro/tests/test_checkers.py +2 -2
  75. experimaestro/tests/test_dependencies.py +11 -17
  76. experimaestro/tests/test_experiment.py +73 -0
  77. experimaestro/tests/test_file_progress.py +425 -0
  78. experimaestro/tests/test_file_progress_integration.py +477 -0
  79. experimaestro/tests/test_findlauncher.py +12 -5
  80. experimaestro/tests/test_forward.py +5 -5
  81. experimaestro/tests/test_generators.py +93 -0
  82. experimaestro/tests/test_identifier.py +182 -158
  83. experimaestro/tests/test_instance.py +19 -27
  84. experimaestro/tests/test_objects.py +13 -20
  85. experimaestro/tests/test_outputs.py +6 -6
  86. experimaestro/tests/test_param.py +68 -30
  87. experimaestro/tests/test_progress.py +4 -4
  88. experimaestro/tests/test_serializers.py +24 -64
  89. experimaestro/tests/test_ssh.py +7 -0
  90. experimaestro/tests/test_tags.py +50 -21
  91. experimaestro/tests/test_tasks.py +42 -51
  92. experimaestro/tests/test_tokens.py +11 -8
  93. experimaestro/tests/test_types.py +24 -21
  94. experimaestro/tests/test_validation.py +67 -110
  95. experimaestro/tests/token_reschedule.py +1 -1
  96. experimaestro/tokens.py +24 -13
  97. experimaestro/tools/diff.py +8 -1
  98. experimaestro/typingutils.py +20 -11
  99. experimaestro/utils/asyncio.py +6 -2
  100. experimaestro/utils/multiprocessing.py +44 -0
  101. experimaestro/utils/resources.py +11 -3
  102. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/METADATA +28 -36
  103. experimaestro-2.0.0a8.dist-info/RECORD +166 -0
  104. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/WHEEL +1 -1
  105. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/entry_points.txt +0 -4
  106. experimaestro/launchers/slurm/cli.py +0 -29
  107. experimaestro/launchers/slurm/configuration.py +0 -597
  108. experimaestro/scheduler/environment.py +0 -94
  109. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  110. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  111. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  112. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  113. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  114. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  115. experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
  116. experimaestro/utils/yaml.py +0 -202
  117. experimaestro-1.5.1.dist-info/RECORD +0 -148
  118. {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info/licenses}/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import inspect
3
3
  import sys
4
- from typing import Set, Union, Dict, Iterator, List, get_args, get_origin
4
+ from typing import Set, TypeVar, Union, Dict, Iterator, List, get_args, get_origin
5
5
  from collections import ChainMap
6
6
  from pathlib import Path
7
7
  import typing
@@ -26,6 +26,7 @@ if typing.TYPE_CHECKING:
26
26
 
27
27
  class Identifier:
28
28
  def __init__(self, name: str):
29
+ assert isinstance(name, str)
29
30
  self.name = name
30
31
 
31
32
  def __hash__(self):
@@ -126,10 +127,16 @@ class Type:
126
127
  if t:
127
128
  return DictType(Type.fromType(t[0]), Type.fromType(t[1]))
128
129
 
129
- # Takes care of generics
130
+ if union_t := typingutils.get_union(key):
131
+ return UnionType([Type.fromType(t) for t in union_t])
132
+
133
+ # Takes care of generics, like List[int], not List
130
134
  if get_origin(key):
131
135
  return GenericType(key)
132
136
 
137
+ if isinstance(key, TypeVar):
138
+ return TypeVarType(key)
139
+
133
140
  raise Exception("No type found for %s", key)
134
141
 
135
142
 
@@ -196,21 +203,17 @@ class ObjectType(Type):
196
203
  """ObjectType contains class-level information about
197
204
  experimaestro configurations and tasks
198
205
 
199
- :param objecttype: The python Type of the associated object
200
- :param configtype: The python Type of the configuration object that uses
201
- 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
202
208
  """
203
209
 
204
- # Those entries should not be copied in the __dict__
205
- FORBIDDEN_KEYS = set(("__dict__", "__weakref__"))
206
-
207
210
  def __init__(
208
211
  self,
209
212
  tp: type,
210
- identifier: Union[str, Identifier] = None,
213
+ identifier: Union[str, Identifier, None] = None,
211
214
  ):
212
215
  """Creates a type"""
213
- from .objects import Config, TypeConfig
216
+ from .objects import Config, ConfigMixin
214
217
 
215
218
  # Task related attributes
216
219
  self.taskcommandfactory = None
@@ -218,12 +221,12 @@ class ObjectType(Type):
218
221
  self._title = None
219
222
  self.submit_hooks = set()
220
223
 
221
- # Get the identifier
224
+ # --- Get the identifier
222
225
  if identifier is None and hasattr(tp, "__xpmid__"):
223
226
  __xpmid__ = getattr(tp, "__xpmid__")
224
227
  if isinstance(__xpmid__, Identifier):
225
228
  identifier = __xpmid__
226
- if inspect.ismethod(__xpmid__):
229
+ elif inspect.ismethod(__xpmid__):
227
230
  identifier = Identifier(__xpmid__())
228
231
  elif "__xpmid__" in tp.__dict__:
229
232
  identifier = Identifier(__xpmid__)
@@ -243,59 +246,41 @@ class ObjectType(Type):
243
246
  # --- Creates the config type and not config type
244
247
 
245
248
  self.originaltype = tp
246
- if not issubclass(tp, Config):
247
- # Adds Config as a base class if not present
248
- __bases__ = () if tp.__bases__ == (object,) else tp.__bases__
249
- __dict__ = dict(tp.__dict__)
250
-
251
- __dict__ = {
252
- key: value
253
- for key, value in tp.__dict__.items()
254
- if key not in ObjectType.FORBIDDEN_KEYS
255
- }
256
- self.basetype = type(tp.__name__, (Config,) + __bases__, __dict__)
257
- self.basetype.__module__ = tp.__module__
258
- self.basetype.__qualname__ = tp.__qualname__
259
- else:
260
- self.basetype = tp
249
+ assert issubclass(tp, Config)
250
+ self.value_type = tp
261
251
 
262
252
  # --- Create the type-specific configuration class (XPMConfig)
263
253
  __configbases__ = tuple(
264
- s.__getxpmtype__().configtype
254
+ s.__getxpmtype__().config_type
265
255
  for s in tp.__bases__
266
256
  if issubclass(s, Config) and (s is not Config)
267
- ) or (TypeConfig,)
257
+ ) or (ConfigMixin,)
268
258
 
269
- *tp_qual, tp_name = self.basetype.__qualname__.split(".")
270
- self.configtype = type(
271
- 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,), {}
272
262
  )
273
- self.configtype.__qualname__ = ".".join(tp_qual + [self.configtype.__name__])
274
- self.configtype.__module__ = tp.__module__
263
+ self.config_type.__qualname__ = ".".join(tp_qual + [self.config_type.__name__])
264
+ self.config_type.__module__ = tp.__module__
275
265
 
276
- # Return type is used by tasks to change the output
277
- if hasattr(self.basetype, "task_outputs") or False:
266
+ # --- Get the return type
267
+ if hasattr(self.value_type, "task_outputs") or False:
278
268
  self.returntype = get_type_hints(
279
- getattr(self.basetype, "task_outputs")
269
+ getattr(self.value_type, "task_outputs")
280
270
  ).get("return", typing.Any)
281
271
  else:
282
- self.returntype = self.basetype
272
+ self.returntype = self.value_type
283
273
 
284
- # Registers ourselves
285
- self.basetype.__xpmtype__ = self
286
- self.configtype.__xpmtype__ = self
274
+ # --- Registers ourselves
275
+ self.value_type.__xpmtype__ = self
276
+ self.config_type.__xpmtype__ = self
287
277
 
288
- # Other initializations
278
+ # --- Other initializations
289
279
  self.__initialized__ = False
290
280
  self._runtype = None
291
281
  self.annotations = []
292
282
  self._deprecated = False
293
283
 
294
- @property
295
- def objecttype(self):
296
- """Returns the object type"""
297
- return self.basetype.XPMValue
298
-
299
284
  def addAnnotation(self, annotation):
300
285
  assert not self.__initialized__
301
286
  self.annotations.append(annotation)
@@ -325,12 +310,17 @@ class ObjectType(Type):
325
310
 
326
311
  # Get the module
327
312
  module = inspect.getmodule(self.originaltype)
328
- if getattr(module, "__file__", None) is None:
313
+ self._module = module.__name__
314
+ self._package = module.__package__
315
+
316
+ if self._module and self._package:
329
317
  self._file = None
330
318
  else:
331
319
  self._file = Path(inspect.getfile(self.originaltype)).absolute()
332
- self._module = module.__name__
333
- self._package = module.__package__
320
+
321
+ assert (
322
+ self._module and self._package
323
+ ) or self._file, f"Could not detect module/file for {self.originaltype}"
334
324
 
335
325
  # The class of the object
336
326
 
@@ -345,15 +335,15 @@ class ObjectType(Type):
345
335
  # Add task
346
336
  if self.taskcommandfactory is not None:
347
337
  self.task = self.taskcommandfactory(self)
348
- elif issubclass(self.basetype, Task):
338
+ elif issubclass(self.value_type, Task):
349
339
  self.task = self.getpythontaskcommand()
350
340
 
351
341
  # Add arguments from type hints
352
342
  from .arguments import TypeAnnotation
353
343
 
354
- if hasattr(self.basetype, "__annotations__"):
355
- typekeys = set(self.basetype.__dict__.get("__annotations__", {}).keys())
356
- 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)
357
347
  for key, typehint in hints.items():
358
348
  # Filter out hints from parent classes
359
349
  if key in typekeys:
@@ -366,19 +356,19 @@ class ObjectType(Type):
366
356
  try:
367
357
  self.addArgument(
368
358
  options.create(
369
- key, self.objecttype, typehint.__args__[0]
359
+ key, self.value_type, typehint.__args__[0]
370
360
  )
371
361
  )
372
362
  except Exception:
373
363
  logger.error(
374
364
  "while adding argument %s of %s",
375
365
  key,
376
- self.objecttype,
366
+ self.value_type,
377
367
  )
378
368
  raise
379
369
 
380
370
  def name(self):
381
- return f"{self.basetype.__module__}.{self.basetype.__qualname__}"
371
+ return f"{self.value_type.__module__}.{self.value_type.__qualname__}"
382
372
 
383
373
  def __parsedoc__(self):
384
374
  """Parse the documentation"""
@@ -388,7 +378,7 @@ class ObjectType(Type):
388
378
  self.__initialize__()
389
379
 
390
380
  # Get description from documentation
391
- __doc__ = self.basetype.__dict__.get("__doc__", None)
381
+ __doc__ = self.value_type.__dict__.get("__doc__", None)
392
382
  if __doc__:
393
383
  parseddoc = parse(__doc__)
394
384
  self._title = parseddoc.short_description
@@ -418,7 +408,7 @@ class ObjectType(Type):
418
408
  argname = None
419
409
 
420
410
  def deprecate(self):
421
- if len(self.basetype.__bases__) != 1:
411
+ if len(self.value_type.__bases__) != 1:
422
412
  raise RuntimeError(
423
413
  "Deprecated configurations must have "
424
414
  "only one parent (the new configuration)"
@@ -427,7 +417,7 @@ class ObjectType(Type):
427
417
 
428
418
  # Uses the parent identifier (and saves the deprecated one for path updates)
429
419
  self._deprecated_identifier = self.identifier
430
- parent = self.basetype.__bases__[0].__getxpmtype__()
420
+ parent = self.value_type.__bases__[0].__getxpmtype__()
431
421
  self.identifier = parent.identifier
432
422
  self._deprecated = True
433
423
 
@@ -442,7 +432,7 @@ class ObjectType(Type):
442
432
  return self._description
443
433
 
444
434
  @property
445
- def title(self) -> Dict[str, Argument]:
435
+ def title(self) -> str:
446
436
  self.__parsedoc__()
447
437
  return self._title or str(self.identifier)
448
438
 
@@ -457,7 +447,7 @@ class ObjectType(Type):
457
447
 
458
448
  # The the attribute for the config type
459
449
  setattr(
460
- self.configtype,
450
+ self.config_type,
461
451
  argument.name,
462
452
  property(
463
453
  lambda _self: _self.__xpm__.get(argument.name),
@@ -476,7 +466,7 @@ class ObjectType(Type):
476
466
  def parents(self) -> Iterator["ObjectType"]:
477
467
  from .objects import Config, Task
478
468
 
479
- for tp in self.basetype.__bases__:
469
+ for tp in self.value_type.__bases__:
480
470
  if issubclass(tp, Config) and tp not in [Config, Task]:
481
471
  yield tp.__xpmtype__
482
472
 
@@ -492,7 +482,7 @@ class ObjectType(Type):
492
482
  if not isinstance(value, Config):
493
483
  raise ValueError(f"{value} is not an experimaestro type or task")
494
484
 
495
- types = self.basetype
485
+ types = self.value_type
496
486
 
497
487
  if not isinstance(value, types):
498
488
  raise ValueError(
@@ -507,7 +497,7 @@ class ObjectType(Type):
507
497
 
508
498
  def fullyqualifiedname(self) -> str:
509
499
  """Returns the fully qualified (Python) name"""
510
- return f"{self.basetype.__module__}.{self.basetype.__qualname__}"
500
+ return f"{self.value_type.__module__}.{self.value_type.__qualname__}"
511
501
 
512
502
 
513
503
  class TypeProxy:
@@ -571,7 +561,7 @@ class PathType(Type):
571
561
  return Path(value.get("$value"))
572
562
 
573
563
  if not isinstance(value, (str, Path)):
574
- raise TypeError("value is not a pathlike value")
564
+ raise TypeError(f"value is not a pathlike value ({type(value)})")
575
565
  return Path(value)
576
566
 
577
567
  @property
@@ -588,6 +578,23 @@ class AnyType(Type):
588
578
  return value
589
579
 
590
580
 
581
+ class TypeVarType(Type):
582
+ def __init__(self, typevar: TypeVar):
583
+ self.typevar = typevar
584
+
585
+ def name(self):
586
+ return str(self.typevar)
587
+
588
+ def validate(self, value):
589
+ return value
590
+
591
+ def __str__(self):
592
+ return f"TypeVar({self.typevar})"
593
+
594
+ def __repr__(self):
595
+ return f"TypeVar({self.typevar})"
596
+
597
+
591
598
  Any = AnyType()
592
599
 
593
600
 
@@ -626,6 +633,32 @@ class EnumType(Type):
626
633
  return f"Enum({self.type})"
627
634
 
628
635
 
636
+ class UnionType(Type):
637
+ def __init__(self, types: List[Type]):
638
+ self.types = types
639
+
640
+ def name(self):
641
+ return "Union[" + ", ".join(t.name() for t in self.types) + "]"
642
+
643
+ def __str__(self):
644
+ return "[" + " | ".join(t.name() for t in self.types) + " ]"
645
+
646
+ def __repr__(self):
647
+ return str(self)
648
+
649
+ def validate(self, value):
650
+ for subtype in self.types:
651
+ try:
652
+ return subtype.validate(value)
653
+ except ValueError:
654
+ pass
655
+ except TypeError:
656
+ pass
657
+
658
+ if not isinstance(value, dict):
659
+ raise ValueError(f"value is not within the types {self}")
660
+
661
+
629
662
  class DictType(Type):
630
663
  def __init__(self, keytype: Type, valuetype: Type):
631
664
  self.keytype = keytype
@@ -663,6 +696,10 @@ class GenericType(Type):
663
696
  def __repr__(self):
664
697
  return repr(self.type)
665
698
 
699
+ def identifier(self):
700
+ """Returns the identifier of the type"""
701
+ return Identifier(f"{self.origin}.{self.type}")
702
+
666
703
  def validate(self, value):
667
704
  # Now, let's check generics...
668
705
  mros = typingutils.generic_mro(type(value))
@@ -670,6 +707,7 @@ class GenericType(Type):
670
707
  (mro for mro in mros if (get_origin(mro) or mro) is self.origin), None
671
708
  )
672
709
  target = get_origin(self.type) or self.type
710
+
673
711
  if matching is None:
674
712
  raise ValueError(
675
713
  f"{type(value)} is not of type {target} ({type(value).__mro__})"