experimaestro 1.6.1__py3-none-any.whl → 1.7.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 (76) hide show
  1. experimaestro/__init__.py +3 -1
  2. experimaestro/annotations.py +13 -3
  3. experimaestro/cli/filter.py +3 -3
  4. experimaestro/cli/jobs.py +1 -1
  5. experimaestro/connectors/__init__.py +17 -8
  6. experimaestro/connectors/local.py +8 -3
  7. experimaestro/core/arguments.py +26 -3
  8. experimaestro/core/objects.py +90 -6
  9. experimaestro/core/objects.pyi +7 -1
  10. experimaestro/core/types.py +33 -2
  11. experimaestro/experiments/cli.py +18 -10
  12. experimaestro/generators.py +6 -1
  13. experimaestro/ipc.py +4 -1
  14. experimaestro/launcherfinder/registry.py +7 -4
  15. experimaestro/notifications.py +1 -1
  16. experimaestro/run.py +1 -1
  17. experimaestro/scheduler/base.py +98 -6
  18. experimaestro/scheduler/dynamic_outputs.py +184 -0
  19. experimaestro/scheduler/workspace.py +2 -1
  20. experimaestro/scriptbuilder.py +10 -1
  21. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  22. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  23. experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  24. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  25. experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  26. experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  27. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  28. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  29. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  30. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  31. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  32. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  33. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  34. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  35. experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  36. experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  37. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  38. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  39. experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  40. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  41. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  42. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  43. experimaestro/server/data/favicon.ico +0 -0
  44. experimaestro/server/data/index.css +22963 -0
  45. experimaestro/server/data/index.css.map +1 -0
  46. experimaestro/server/data/index.html +27 -0
  47. experimaestro/server/data/index.js +101770 -0
  48. experimaestro/server/data/index.js.map +1 -0
  49. experimaestro/server/data/login.html +22 -0
  50. experimaestro/server/data/manifest.json +15 -0
  51. experimaestro/settings.py +2 -2
  52. experimaestro/taskglobals.py +7 -2
  53. experimaestro/tests/definitions_types.py +5 -3
  54. experimaestro/tests/launchers/bin/sbatch +18 -5
  55. experimaestro/tests/launchers/common.py +11 -3
  56. experimaestro/tests/restart.py +6 -3
  57. experimaestro/tests/tasks/all.py +16 -10
  58. experimaestro/tests/tasks/foreign.py +2 -4
  59. experimaestro/tests/test_forward.py +5 -5
  60. experimaestro/tests/test_identifier.py +61 -66
  61. experimaestro/tests/test_instance.py +3 -6
  62. experimaestro/tests/test_param.py +40 -22
  63. experimaestro/tests/test_tags.py +5 -11
  64. experimaestro/tests/test_tokens.py +3 -2
  65. experimaestro/tests/test_types.py +17 -14
  66. experimaestro/tests/test_validation.py +48 -91
  67. experimaestro/tokens.py +16 -5
  68. experimaestro/typingutils.py +7 -0
  69. experimaestro/utils/asyncio.py +6 -2
  70. experimaestro/utils/resources.py +7 -3
  71. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0rc0.dist-info}/METADATA +3 -4
  72. experimaestro-1.7.0rc0.dist-info/RECORD +153 -0
  73. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0rc0.dist-info}/WHEEL +1 -1
  74. experimaestro-1.6.1.dist-info/RECORD +0 -122
  75. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0rc0.dist-info}/LICENSE +0 -0
  76. {experimaestro-1.6.1.dist-info → experimaestro-1.7.0rc0.dist-info}/entry_points.txt +0 -0
experimaestro/__init__.py CHANGED
@@ -36,15 +36,17 @@ from .core.arguments import (
36
36
  DataPath,
37
37
  Annotated,
38
38
  Constant,
39
+ field,
39
40
  # Annotations helpers
40
41
  help,
41
42
  default,
42
43
  )
43
- from .generators import pathgenerator
44
+ from .generators import pathgenerator, PathGenerator
44
45
  from .core.objects import (
45
46
  Config,
46
47
  copyconfig,
47
48
  setmeta,
49
+ DependentMarker,
48
50
  Task,
49
51
  LightweightTask,
50
52
  ObjectStore,
@@ -8,7 +8,7 @@ import experimaestro.core.objects as objects
8
8
  import experimaestro.core.types as types
9
9
  from experimaestro.generators import PathGenerator
10
10
 
11
- from .core.arguments import Argument as CoreArgument
11
+ from .core.arguments import Argument as CoreArgument, field
12
12
  from .core.objects import Config
13
13
  from .core.types import Any, Identifier, TypeProxy, Type, ObjectType
14
14
  from .utils import logger
@@ -134,12 +134,22 @@ class param:
134
134
  self.type = Type.fromType(type) if type else None
135
135
  self.help = help
136
136
  self.ignored = ignored
137
- self.default = default
138
137
  self.required = required
139
- self.generator = None
140
138
  self.checker = checker
141
139
  self.constant = constant
142
140
 
141
+ self.generator = None
142
+ self.default = None
143
+
144
+ # Set default or generator
145
+ if isinstance(default, field):
146
+ if default.default is not None:
147
+ self.default = default
148
+ elif default.default_factory is not None:
149
+ self.generator = default.default_factory
150
+ else:
151
+ self.default = default
152
+
143
153
  def __call__(self, tp):
144
154
  # Don't annotate in task mode
145
155
  tp.__getxpmtype__().addAnnotation(self)
@@ -3,7 +3,7 @@ import pyparsing as pp
3
3
  from pathlib import Path
4
4
  import json
5
5
  from experimaestro.compat import cached_property
6
- import regex
6
+ import re
7
7
  from experimaestro.scheduler import JobState
8
8
 
9
9
 
@@ -87,7 +87,7 @@ class NotInExpr(BaseInExpr):
87
87
  class RegexExpr:
88
88
  def __init__(self, tokens):
89
89
  self.var, expr = tokens
90
- self.regex = regex.compile(expr)
90
+ self.regex = re.compile(expr)
91
91
 
92
92
  def __repr__(self):
93
93
  return f"""REGEX[{self.varname}, {self.value}]"""
@@ -103,7 +103,7 @@ class RegexExpr:
103
103
  if not value:
104
104
  return False
105
105
 
106
- return self.regex.match(value)
106
+ return self.re.match(value)
107
107
 
108
108
 
109
109
  class ConstantString:
experimaestro/cli/jobs.py CHANGED
@@ -69,7 +69,7 @@ def process(
69
69
  if (p / "jobs.bak").is_dir():
70
70
  cprint(f" Experiment {p.name} has not finished yet", "red")
71
71
  if (not perform) and (kill or clean):
72
- cprint(" Preventing kill/clean (use --force if you want to)", "yellow")
72
+ cprint(" Preventing kill/clean (use --perform if you want to)", "yellow")
73
73
  kill = False
74
74
  clean = False
75
75
 
@@ -9,7 +9,8 @@ This module contains :
9
9
  """
10
10
 
11
11
  import enum
12
- from typing import Any, Dict, Mapping, Type, Union
12
+ import logging
13
+ from typing import Any, Dict, Mapping, Type, Union, Optional
13
14
  from pathlib import Path
14
15
  from experimaestro.utils import logger
15
16
  from experimaestro.locking import Lock
@@ -86,12 +87,12 @@ class Process:
86
87
  @staticmethod
87
88
  def fromDefinition(connector: "Connector", definition: Dict[str, Any]) -> "Process":
88
89
  """Retrieves a process from a serialized definition"""
89
- handler = Process.handler(definition["type"])
90
+ handler_type = definition["type"]
91
+ handler = Process.handler(handler_type)
92
+ assert handler is not None, f"No handler of type {handler_type}"
90
93
  try:
91
94
  return handler.fromspec(connector, definition)
92
95
  except Exception as e:
93
- import logging
94
-
95
96
  logging.exception("Could not retrieve job from specification")
96
97
  raise e
97
98
 
@@ -101,7 +102,11 @@ class Process:
101
102
  if Process.HANDLERS is None:
102
103
  Process.HANDLERS = {}
103
104
  for ep in pkg_resources.iter_entry_points(group="experimaestro.process"):
104
- Process.HANDLERS[ep.name] = ep.load()
105
+ logging.debug("Adding process handler for type %s", ep.name)
106
+ handler = ep.load()
107
+ Process.HANDLERS[ep.name] = handler
108
+ if handler is None:
109
+ logging.error("Handler of type %s is null", ep.name)
105
110
 
106
111
  return Process.HANDLERS.get(key, None)
107
112
 
@@ -117,10 +122,14 @@ class Process:
117
122
  """True is the process is truly running (I/O)"""
118
123
  return (await self.aio_state()).running
119
124
 
120
- async def aio_code(self):
121
- """Returns a future containing the returned code"""
125
+ async def aio_code(self) -> Optional[int]:
126
+ """Returns a future containing the returned code
127
+
128
+ Returns None if the process has already finished – and no information is
129
+ known about the process.
130
+ """
122
131
  code = await asyncThreadcheck("aio_code", self.wait)
123
- logger.debug("Got for return code %s: %s", self, code)
132
+ logger.debug("Got return code %s for %s", code, self)
124
133
  return code
125
134
 
126
135
  def kill(self):
@@ -2,6 +2,7 @@
2
2
  """
3
3
 
4
4
  import subprocess
5
+ from typing import Optional
5
6
  from pathlib import Path, WindowsPath, PosixPath
6
7
  import os
7
8
  import threading
@@ -29,11 +30,13 @@ class PsutilProcess(Process):
29
30
  def __init__(self, pid: int):
30
31
  self._process = psutil.Process(pid)
31
32
 
32
- def wait(self) -> int:
33
+ def wait(self) -> Optional[int]:
33
34
  logger.debug("Waiting (psutil) for process with PID %s", self._process.pid)
34
35
  code = self._process.wait()
35
36
  logger.debug(
36
- "Finished to wait (psutil) for process with PID %s", self._process.pid
37
+ "Finished to wait (psutil) for process with PID %s: code %s",
38
+ self._process.pid,
39
+ code,
37
40
  )
38
41
  return code
39
42
 
@@ -57,7 +60,9 @@ class LocalProcess(Process):
57
60
  logger.debug("Waiting (python) for process with PID %s", self._process.pid)
58
61
  code = self._process.wait()
59
62
  logger.debug(
60
- "Finished to wait (python) for process with PID %s", self._process.pid
63
+ "Finished to wait (python) for process with PID %s: %s",
64
+ self._process.pid,
65
+ code,
61
66
  )
62
67
  return code
63
68
 
@@ -1,6 +1,6 @@
1
1
  """Management of the arguments (params, options, etc) associated with the XPM objects"""
2
2
 
3
- from typing import Optional, TypeVar, TYPE_CHECKING
3
+ from typing import Optional, TypeVar, TYPE_CHECKING, Callable, Any
4
4
  from experimaestro.typingutils import get_optional
5
5
  from pathlib import Path
6
6
  import sys
@@ -75,11 +75,22 @@ class Argument:
75
75
  self.constant = constant
76
76
  self.ignored = self.type.ignore if ignored is None else ignored
77
77
  self.required = required
78
- self.default = default
79
- self.generator = generator
80
78
  self.objecttype = None
81
79
  self.is_data = is_data
82
80
 
81
+ self.generator = generator
82
+ self.default = None
83
+
84
+ if default is not None:
85
+ assert self.generator is None, "generator and default are exclusive options"
86
+ if isinstance(default, field):
87
+ if default.default is not None:
88
+ self.default = default.default
89
+ elif default.default_factory is not None:
90
+ self.generator = default.default_factory
91
+ else:
92
+ self.default = default
93
+
83
94
  assert (
84
95
  not self.constant or self.default is not None
85
96
  ), "Cannot be constant without default"
@@ -170,6 +181,18 @@ DataPath = Annotated[Path, dataHint]
170
181
  """Annotates a path that should be kept to restore an object to its state"""
171
182
 
172
183
 
184
+ class field:
185
+ """Extra information for a given experimaestro field (param or meta)"""
186
+
187
+ def __init__(self, *, default: Any = None, default_factory: Callable = None):
188
+ assert not (
189
+ (default is not None) and (default_factory is not None)
190
+ ), "default and default_factory are mutually exclusive options"
191
+
192
+ self.default_factory = default_factory
193
+ self.default = default
194
+
195
+
173
196
  class help(TypeAnnotation):
174
197
  def __init__(self, text: str):
175
198
  self.text = text
@@ -3,6 +3,10 @@
3
3
  from functools import cached_property
4
4
  import json
5
5
 
6
+ from attr import define
7
+
8
+ from experimaestro import taskglobals
9
+
6
10
  try:
7
11
  from types import NoneType
8
12
  except Exception:
@@ -21,6 +25,7 @@ import inspect
21
25
  import importlib
22
26
  from typing import (
23
27
  Any,
28
+ Callable,
24
29
  ClassVar,
25
30
  Dict,
26
31
  Iterator,
@@ -136,6 +141,8 @@ class ConfigPath:
136
141
 
137
142
  hash_logger = logging.getLogger("xpm.hash")
138
143
 
144
+ DependentMarker = Callable[["Config"], None]
145
+
139
146
 
140
147
  class HashComputer:
141
148
  """This class is in charge of computing a config/task identifier"""
@@ -555,6 +562,24 @@ class ObjectStore:
555
562
  self.store[identifier] = stub
556
563
 
557
564
 
565
+ @define()
566
+ class WatchedOutput:
567
+ #: The enclosing job
568
+ job: "Job"
569
+
570
+ #: The configuration containing the watched output
571
+ config: "ConfigInformation"
572
+
573
+ #: The watched output (name)
574
+ method_name: str
575
+
576
+ #: The watched output method (called with the JSON event)
577
+ method: Callable
578
+
579
+ #: The callback to call (with the output of the previous method)
580
+ callback: Callable
581
+
582
+
558
583
  class ConfigInformation:
559
584
  """Holds experimaestro information for a config (or task) instance"""
560
585
 
@@ -595,6 +620,9 @@ class ConfigInformation:
595
620
  # Initialization tasks
596
621
  self.init_tasks: List["LightweightTask"] = []
597
622
 
623
+ # Watched outputs
624
+ self.watched_outputs: List[WatchedOutput] = []
625
+
598
626
  # Cached information
599
627
 
600
628
  self._full_identifier = None
@@ -724,9 +752,16 @@ class ConfigInformation:
724
752
  # Generate values
725
753
  for k, argument in config.__xpmtype__.arguments.items():
726
754
  if argument.generator:
727
- config.__xpm__.set(
728
- k, argument.generator(self.context, config), bypass=True
729
- )
755
+ sig = inspect.signature(argument.generator)
756
+ if len(sig.parameters) == 2:
757
+ value = argument.generator(self.context, config)
758
+ elif len(sig.parameters) == 0:
759
+ value = argument.generator()
760
+ else:
761
+ assert (
762
+ False
763
+ ), "generator has either two parameters (context and config), or none"
764
+ config.__xpm__.set(k, value, bypass=True)
730
765
 
731
766
  config.__xpm__._sealed = True
732
767
 
@@ -899,6 +934,19 @@ class ConfigInformation:
899
934
  # Now, seal the object
900
935
  self.seal(context)
901
936
 
937
+ def watch_output(self, method, callback):
938
+ """Watch the task output linked with a given method
939
+
940
+ :param method: The method to watch
941
+ :param callback: The callback
942
+ """
943
+ watched = WatchedOutput(
944
+ self, method.__self__, method.__name__, method, callback
945
+ )
946
+ self.watched_outputs.append(watched)
947
+ if self.job:
948
+ self.job.watch_output(watched)
949
+
902
950
  def submit(
903
951
  self,
904
952
  workspace: "Workspace",
@@ -1000,7 +1048,7 @@ class ConfigInformation:
1000
1048
 
1001
1049
  print(file=sys.stderr) # noqa: T201
1002
1050
 
1003
- # Handle an output configuration
1051
+ # Handle an output configuration # FIXME: remove
1004
1052
  def mark_output(config: "Config"):
1005
1053
  """Sets a dependency on the job"""
1006
1054
  assert not isinstance(config, Task), "Cannot set a dependency on a task"
@@ -1011,12 +1059,18 @@ class ConfigInformation:
1011
1059
  self.task = self.pyobject
1012
1060
 
1013
1061
  if hasattr(self.pyobject, "task_outputs"):
1014
- self._taskoutput = self.pyobject.task_outputs(mark_output)
1062
+ self._taskoutput = self.pyobject.task_outputs(self.mark_output)
1015
1063
  else:
1016
1064
  self._taskoutput = self.task = self.pyobject
1017
1065
 
1018
1066
  return self._taskoutput
1019
1067
 
1068
+ def mark_output(self, config: "Config"):
1069
+ """Sets a dependency on the job"""
1070
+ assert not isinstance(config, Task), "Cannot set a dependency on a task"
1071
+ config.__xpm__.task = self.pyobject
1072
+ return config
1073
+
1020
1074
  # --- Serialization
1021
1075
 
1022
1076
  @staticmethod
@@ -1345,7 +1399,9 @@ class ConfigInformation:
1345
1399
  mod = importlib.import_module(module_name)
1346
1400
  except ModuleNotFoundError:
1347
1401
  # More hints on the nature of the error
1348
- logging.warning("(1) Either the python path is wrong – %s", ":".join(sys.path))
1402
+ logging.warning(
1403
+ "(1) Either the python path is wrong – %s", ":".join(sys.path)
1404
+ )
1349
1405
  logging.warning("(2) There is not __init__.py in your module")
1350
1406
  raise
1351
1407
 
@@ -1790,6 +1846,10 @@ class classproperty(property):
1790
1846
  class Config:
1791
1847
  """Base type for all objects in python interface"""
1792
1848
 
1849
+ __xpmid__: ClassVar[Optional[str]]
1850
+ """Optional configuration ID, mostly useful when moving a class to another
1851
+ package to avoid changes in computed task identifiers"""
1852
+
1793
1853
  __xpmtype__: ClassVar[ObjectType]
1794
1854
  """The object type holds all the information about a specific subclass
1795
1855
  experimaestro metadata"""
@@ -1914,6 +1974,22 @@ class Config:
1914
1974
  """Access pre-tasks"""
1915
1975
  raise AssertionError("Pre-tasks can be accessed only during configuration")
1916
1976
 
1977
+ def register_task_output(self, method, *args, **kwargs):
1978
+ # Determine the path for this...
1979
+ path = taskglobals.Env.instance().xpm_path / "task-outputs.jsonl"
1980
+ path.parent.mkdir(parents=True, exist_ok=True)
1981
+
1982
+ data = json.dumps(
1983
+ {
1984
+ "key": f"{self.__xpmidentifier__}/{method.__name__}",
1985
+ "args": args,
1986
+ "kwargs": kwargs,
1987
+ }
1988
+ )
1989
+ with path.open("at") as fp:
1990
+ fp.writelines([data, "\n"])
1991
+ fp.flush()
1992
+
1917
1993
 
1918
1994
  class LightweightTask(Config):
1919
1995
  """A task that can be run before or after a real task to modify its behaviour"""
@@ -1931,6 +2007,14 @@ class Task(LightweightTask):
1931
2007
  def submit(self):
1932
2008
  raise AssertionError("This method can only be used during configuration")
1933
2009
 
2010
+ def watch_output(self, method, callback):
2011
+ """Sets up a callback
2012
+
2013
+ :param method: a method within a configuration
2014
+ :param callback: the callback
2015
+ """
2016
+ self.__xpm__.watch_output(method, callback)
2017
+
1934
2018
 
1935
2019
  # --- Utility functions
1936
2020
 
@@ -1,4 +1,5 @@
1
1
  from abc import ABC
2
+ from attrs import define
2
3
  import typing_extensions
3
4
 
4
5
  from experimaestro.core.types import ObjectType
@@ -108,6 +109,10 @@ class ConfigWalk(ConfigProcessing):
108
109
  def map(self, k: str): ...
109
110
 
110
111
  def getqualattr(module, qualname): ...
112
+ @define(frozen=True)
113
+ class WatchedOutput:
114
+ config: "Config"
115
+ method_name: str
111
116
 
112
117
  class ConfigInformation:
113
118
  LOADING: bool
@@ -116,6 +121,7 @@ class ConfigInformation:
116
121
  values: Dict[str, Any]
117
122
  job: Job
118
123
  dependencies: Incomplete
124
+ watched_outputs: List[WatchedOutput]
119
125
  def __init__(self, pyobject: TypeConfig) -> None: ...
120
126
  def set_meta(self, value: Optional[bool]): ...
121
127
  @property
@@ -231,7 +237,7 @@ class LightweightTask(Config):
231
237
  def execute(self) -> None: ...
232
238
 
233
239
  class Task(LightweightTask):
234
- __tags__: Dict[str, str]
240
+ # __tags__: Dict[str, str]
235
241
 
236
242
  def submit(
237
243
  self,
@@ -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,6 +127,9 @@ class Type:
126
127
  if t:
127
128
  return DictType(Type.fromType(t[0]), Type.fromType(t[1]))
128
129
 
130
+ if union_t := typingutils.get_union(key):
131
+ return UnionType([Type.fromType(t) for t in union_t])
132
+
129
133
  # Takes care of generics
130
134
  if get_origin(key):
131
135
  return GenericType(key)
@@ -223,7 +227,7 @@ class ObjectType(Type):
223
227
  __xpmid__ = getattr(tp, "__xpmid__")
224
228
  if isinstance(__xpmid__, Identifier):
225
229
  identifier = __xpmid__
226
- if inspect.ismethod(__xpmid__):
230
+ elif inspect.ismethod(__xpmid__):
227
231
  identifier = Identifier(__xpmid__())
228
232
  elif "__xpmid__" in tp.__dict__:
229
233
  identifier = Identifier(__xpmid__)
@@ -576,7 +580,7 @@ class PathType(Type):
576
580
  return Path(value.get("$value"))
577
581
 
578
582
  if not isinstance(value, (str, Path)):
579
- raise TypeError("value is not a pathlike value")
583
+ raise TypeError(f"value is not a pathlike value ({type(value)})")
580
584
  return Path(value)
581
585
 
582
586
  @property
@@ -631,6 +635,32 @@ class EnumType(Type):
631
635
  return f"Enum({self.type})"
632
636
 
633
637
 
638
+ class UnionType(Type):
639
+ def __init__(self, types: List[Type]):
640
+ self.types = types
641
+
642
+ def name(self):
643
+ return "Union[" + ", ".join(t.name() for t in self.types) + "]"
644
+
645
+ def __str__(self):
646
+ return "[" + " | ".join(t.name() for t in self.types) + " ]"
647
+
648
+ def __repr__(self):
649
+ return str(self)
650
+
651
+ def validate(self, value):
652
+ for subtype in self.types:
653
+ try:
654
+ return subtype.validate(value)
655
+ except ValueError:
656
+ pass
657
+ except TypeError:
658
+ pass
659
+
660
+ if not isinstance(value, dict):
661
+ raise ValueError(f"value is not within the types {self}")
662
+
663
+
634
664
  class DictType(Type):
635
665
  def __init__(self, keytype: Type, valuetype: Type):
636
666
  self.keytype = keytype
@@ -675,6 +705,7 @@ class GenericType(Type):
675
705
  (mro for mro in mros if (get_origin(mro) or mro) is self.origin), None
676
706
  )
677
707
  target = get_origin(self.type) or self.type
708
+
678
709
  if matching is None:
679
710
  raise ValueError(
680
711
  f"{type(value)} is not of type {target} ({type(value).__mro__})"
@@ -59,7 +59,7 @@ class ExperimentCallable(Protocol):
59
59
  class ConfigurationLoader:
60
60
  def __init__(self):
61
61
  self.yamls = []
62
- self.pythonpath = set()
62
+ self.python_path = set()
63
63
 
64
64
  def load(self, yaml_file: Path):
65
65
  """Loads a YAML file, and parents one if they exist"""
@@ -76,9 +76,9 @@ class ConfigurationLoader:
76
76
  for path in _data.get("pythonpath", []):
77
77
  path = Path(path)
78
78
  if path.is_absolute():
79
- self.pythonpath.add(path.resolve())
79
+ self.python_path.add(path.resolve())
80
80
  else:
81
- self.pythonpath.add((yaml_file.parent / path).resolve())
81
+ self.python_path.add((yaml_file.parent / path).resolve())
82
82
 
83
83
 
84
84
  @click.option("--debug", is_flag=True, help="Print debug information")
@@ -181,7 +181,7 @@ def experiments_cli( # noqa: C901
181
181
  configuration.merge_with(OmegaConf.from_dotlist(extra_conf))
182
182
 
183
183
  # --- Get the XP file
184
- pythonpath = list(conf_loader.pythonpath)
184
+ python_path = list(conf_loader.python_path)
185
185
  if module_name is None:
186
186
  module_name = configuration.get("module", None)
187
187
 
@@ -192,9 +192,9 @@ def experiments_cli( # noqa: C901
192
192
  not module_name
193
193
  ), "Module name and experiment file are mutually exclusive options"
194
194
  xp_file = Path(xp_file)
195
- if not pythonpath:
196
- pythonpath.append(xp_file.parent)
197
- logging.info("Using python path: %s", ", ".join(str(s) for s in pythonpath))
195
+ if not python_path:
196
+ python_path.append(xp_file.parent)
197
+ logging.info("Using python path: %s", ", ".join(str(s) for s in python_path))
198
198
 
199
199
  assert (
200
200
  module_name or xp_file
@@ -209,7 +209,7 @@ def experiments_cli( # noqa: C901
209
209
  # --- Finds the "run" function
210
210
 
211
211
  # Modifies the Python path
212
- for path in pythonpath:
212
+ for path in python_path:
213
213
  sys.path.append(str(path))
214
214
 
215
215
  if xp_file:
@@ -226,7 +226,11 @@ def experiments_cli( # noqa: C901
226
226
  )
227
227
  else:
228
228
  # Module
229
- mod = importlib.import_module(module_name)
229
+ try:
230
+ mod = importlib.import_module(module_name)
231
+ except ModuleNotFoundError as e:
232
+ logging.error("Module not found: %s with python path %s", e, sys.path)
233
+ raise
230
234
 
231
235
  helper = getattr(mod, "run", None)
232
236
 
@@ -265,10 +269,11 @@ def experiments_cli( # noqa: C901
265
269
 
266
270
  # Define the workspace
267
271
  ws_env = find_workspace(workdir=workdir, workspace=workspace)
272
+
268
273
  workdir = ws_env.path
269
274
 
270
275
  logging.info("Using working directory %s", str(workdir.resolve()))
271
-
276
+
272
277
  # --- Runs the experiment
273
278
  with experiment(
274
279
  ws_env, configuration.id, host=host, port=port, run_mode=run_mode
@@ -278,6 +283,9 @@ def experiments_cli( # noqa: C901
278
283
  for key, value in env:
279
284
  xp.setenv(key, value)
280
285
 
286
+ # Sets the python path
287
+ xp.workspace.python_path.extend(python_path)
288
+
281
289
  try:
282
290
  # Run the experiment
283
291
  helper.xp = xp
@@ -1,11 +1,12 @@
1
1
  import inspect
2
2
  from pathlib import Path
3
+ from abc import ABC, abstractmethod
3
4
  from typing import Callable, Union
4
5
  from experimaestro.core.arguments import ArgumentOptions, TypeAnnotation
5
6
  from experimaestro.core.objects import ConfigWalkContext, Config
6
7
 
7
8
 
8
- class Generator:
9
+ class Generator(ABC):
9
10
  """Base class for all generators"""
10
11
 
11
12
  def isoutput(self):
@@ -13,6 +14,10 @@ class Generator:
13
14
  path within the job folder)"""
14
15
  return False
15
16
 
17
+ @abstractmethod
18
+ def __call__(self, context: ConfigWalkContext, config: Config):
19
+ ...
20
+
16
21
 
17
22
  class PathGenerator(Generator):
18
23
  """Generates a path"""
experimaestro/ipc.py CHANGED
@@ -7,6 +7,7 @@ import sys
7
7
  import logging
8
8
  from .utils import logger
9
9
  from watchdog.observers import Observer
10
+ from watchdog.observers.api import ObservedWatch
10
11
  from watchdog.events import FileSystemEventHandler
11
12
 
12
13
 
@@ -20,7 +21,9 @@ class IPCom:
20
21
  self.observer.start()
21
22
  self.pid = os.getpid()
22
23
 
23
- def fswatch(self, watcher: FileSystemEventHandler, path: Path, recursive=False):
24
+ def fswatch(
25
+ self, watcher: FileSystemEventHandler, path: Path, recursive=False
26
+ ) -> ObservedWatch:
24
27
  if not self.observer.is_alive():
25
28
  logging.error("Observer is not alive")
26
29
 
@@ -78,13 +78,16 @@ class LauncherRegistry:
78
78
 
79
79
  from importlib import util
80
80
 
81
- spec = util.spec_from_file_location("xpm_launchers_conf", launchers_py)
82
- module = util.module_from_spec(spec)
83
- spec.loader.exec_module(module)
81
+ with launchers_py.__fspath__() as fp:
82
+ spec = util.spec_from_file_location("xpm_launchers_conf", fp)
83
+ module = util.module_from_spec(spec)
84
+ spec.loader.exec_module(module)
84
85
 
85
86
  self.find_launcher_fn = getattr(module, "find_launcher", None)
86
87
  if self.find_launcher_fn is None:
87
- logger.warn("No find_launcher() function was found in %s", launchers_py)
88
+ logger.warning(
89
+ "No find_launcher() function was found in %s", launchers_py
90
+ )
88
91
 
89
92
  # Read the configuration file
90
93
  self.connectors = load_yaml(