experimaestro 1.4.3__py3-none-any.whl → 1.5.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.

@@ -1,7 +1,8 @@
1
1
  from dataclasses import dataclass
2
- from pathlib import Path, PurePosixPath
2
+ from pathlib import Path, _posix_flavour
3
3
  import io
4
4
  import os
5
+ import re
5
6
  from experimaestro.launcherfinder import LauncherRegistry, YAMLDataClass
6
7
  from fabric import Connection
7
8
  from invoke import Promise
@@ -21,7 +22,7 @@ from experimaestro.tokens import Token
21
22
  # Might be wise to switch to https://github.com/marian-code/ssh-utilities
22
23
 
23
24
 
24
- class SshPath(Path, PurePosixPath):
25
+ class SshPath(Path):
25
26
  """SSH path
26
27
 
27
28
  Absolute:
@@ -31,16 +32,31 @@ class SshPath(Path, PurePosixPath):
31
32
  ssh://[user@]host[:port]/relative/path
32
33
  """
33
34
 
35
+ _flavour = _posix_flavour
36
+
37
+ def __new__(cls, *args, **kwargs):
38
+ return object.__new__(cls)
39
+
40
+ def __init__(self, url: str):
41
+ parsed = urlparse(url)
42
+ assert parsed.scheme == "ssh"
43
+ self._host = parsed.hostname
44
+
45
+ self._parts = re.split(r"/+", parsed.path)
46
+ if parsed.path.startswith("//"):
47
+ self._parts[0] = "/"
48
+ else:
49
+ self._parts = self._parts[1:]
50
+
34
51
  @property
35
52
  def hostpath(self):
36
- # path = "/" if self._parts[:0] + "/".join(self._parts[1:])
37
53
  if self.is_absolute():
38
- return "/" + self._flavour.join(self._parts[1:])
39
- return self._flavour.join(self._parts)
54
+ return "/" + "/".join(self._parts[1:])
55
+ return "/".join(self._parts)
40
56
 
41
57
  @property
42
58
  def host(self):
43
- return self._drv
59
+ return self._host
44
60
 
45
61
  def is_absolute(self):
46
62
  return self._parts and self._parts[0] == "/"
@@ -66,11 +82,15 @@ class SshPath(Path, PurePosixPath):
66
82
 
67
83
  def _make_child(self, args):
68
84
  drv, root, parts = self._parse_args(args)
69
- assert self._drv == drv or drv == "", f"{self._drv} and {drv}"
85
+ assert self._host == drv or drv == "", f"{self._host} and {drv}"
70
86
  drv, root, parts = self._flavour.join_parsed_parts(
71
- "", self._root, self._parts, "", root, parts
87
+ "", "", self._parts, "", root, parts
72
88
  )
73
- return self._from_parsed_parts(self._drv, root, parts)
89
+
90
+ child = object.__new__(SshPath)
91
+ child._parts = parts
92
+ child._host = self._host
93
+ return child
74
94
 
75
95
  def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
76
96
  # FIXME: should probably be wiser
@@ -105,10 +125,10 @@ class SshPath(Path, PurePosixPath):
105
125
  return obj
106
126
 
107
127
  def __repr__(self):
108
- return "SshPath(%s,%s)" % (self._drv, self._flavour.join(self._parts[1:]))
128
+ return "SshPath(%s,%s)" % (self._host, self._flavour.join(self._parts[1:]))
109
129
 
110
130
  def __str__(self):
111
- return "ssh://%s/%s" % (self._drv, self._flavour.join(self._parts[1:]))
131
+ return "ssh://%s/%s" % (self._host, self._flavour.join(self._parts[1:]))
112
132
 
113
133
 
114
134
  @dataclass
@@ -646,7 +646,7 @@ class ConfigInformation:
646
646
  "Cannot set non existing attribute %s in %s" % (k, self.xpmtype)
647
647
  )
648
648
  except Exception:
649
- logger.exception("Error while setting value %s" % k)
649
+ logger.error("Error while setting value %s in %s", k, self.xpmtype)
650
650
  raise
651
651
 
652
652
  def addtag(self, name, value):
@@ -1093,7 +1093,7 @@ class ConfigInformation:
1093
1093
  state_dict = {
1094
1094
  "id": id(self.pyobject),
1095
1095
  "module": self.xpmtype._module,
1096
- "type": self.xpmtype.objecttype.__qualname__,
1096
+ "type": self.xpmtype.basetype.__qualname__,
1097
1097
  "typename": self.xpmtype.name(),
1098
1098
  "identifier": self.identifier.state_dict(),
1099
1099
  }
@@ -1340,7 +1340,10 @@ class ConfigInformation:
1340
1340
  cls = getqualattr(mod, definition["type"])
1341
1341
 
1342
1342
  # Creates an object (or a config)
1343
- o = cls.__new__(cls, __xpmobject__=as_instance)
1343
+ if as_instance:
1344
+ o = cls.XPMValue.__new__(cls.XPMValue)
1345
+ else:
1346
+ o = cls.XPMConfig.__new__(cls.XPMConfig)
1344
1347
  assert definition["id"] not in objects, "Duplicate id %s" % definition["id"]
1345
1348
  objects[definition["id"]] = o
1346
1349
 
@@ -1377,6 +1380,7 @@ class ConfigInformation:
1377
1380
  o.__xpm_stdout__ = basepath / f"{name.lower()}.out"
1378
1381
  o.__xpm_stderr__ = basepath / f"{name.lower()}.err"
1379
1382
  else:
1383
+ o.__init__()
1380
1384
  xpminfo = o.__xpm__ # type: ConfigInformation
1381
1385
 
1382
1386
  meta = definition.get("meta", None)
@@ -1497,12 +1501,7 @@ class ConfigInformation:
1497
1501
 
1498
1502
  if o is None:
1499
1503
  # Creates an object (and not a config)
1500
- o = config.__xpmtype__.objecttype.__new__(
1501
- config.__xpmtype__.objecttype, __xpmobject__=True
1502
- )
1503
-
1504
- # And calls the parameter-less initialization
1505
- o.__init__()
1504
+ o = config.XPMValue()
1506
1505
 
1507
1506
  # Store in cache
1508
1507
  self.objects.add_stub(id(config), o)
@@ -1650,8 +1649,8 @@ class TypeConfig:
1650
1649
  [f"{key}={value}" for key, value in self.__xpm__.values.items()]
1651
1650
  )
1652
1651
  return (
1653
- f"{self.__xpmtype__.objecttype.__module__}."
1654
- f"{self.__xpmtype__.objecttype.__qualname__}({params})"
1652
+ f"{self.__xpmtype__.basetype.__module__}."
1653
+ f"{self.__xpmtype__.basetype.__qualname__}({params})"
1655
1654
  )
1656
1655
 
1657
1656
  def tag(self, name, value):
@@ -1770,6 +1769,11 @@ class TypeConfig:
1770
1769
  self.__xpm__.add_dependencies(*other.__xpm__.dependencies)
1771
1770
 
1772
1771
 
1772
+ class classproperty(property):
1773
+ def __get__(self, owner_self, owner_cls):
1774
+ return self.fget(owner_cls)
1775
+
1776
+
1773
1777
  class Config:
1774
1778
  """Base type for all objects in python interface"""
1775
1779
 
@@ -1781,6 +1785,42 @@ class Config:
1781
1785
  """The __xpm__ object contains all instance specific information about a
1782
1786
  configuration/task"""
1783
1787
 
1788
+ @classproperty
1789
+ def XPMConfig(cls):
1790
+ if issubclass(cls, TypeConfig):
1791
+ return cls
1792
+ return cls.__getxpmtype__().configtype
1793
+
1794
+ @classproperty
1795
+ def C(cls):
1796
+ return cls.XPMConfig
1797
+
1798
+ @classproperty
1799
+ def XPMValue(cls):
1800
+ """Returns the value object for this configuration"""
1801
+ if issubclass(cls, TypeConfig):
1802
+ return cls.__xpmtype__.objecttype
1803
+
1804
+ if value_cls := cls.__dict__.get("__XPMValue__", None):
1805
+ pass
1806
+ else:
1807
+ from .types import XPMValue
1808
+
1809
+ __objectbases__ = tuple(
1810
+ s.XPMValue
1811
+ for s in cls.__bases__
1812
+ if issubclass(s, Config) and (s is not Config)
1813
+ ) or (XPMValue,)
1814
+
1815
+ *tp_qual, tp_name = cls.__qualname__.split(".")
1816
+ value_cls = type(f"{tp_name}.XPMValue", (cls,) + __objectbases__, {})
1817
+ value_cls.__qualname__ = ".".join(tp_qual + [value_cls.__name__])
1818
+ value_cls.__module__ = cls.__module__
1819
+
1820
+ setattr(cls, "__XPMValue__", value_cls)
1821
+
1822
+ return value_cls
1823
+
1784
1824
  @classmethod
1785
1825
  def __getxpmtype__(cls) -> "ObjectType":
1786
1826
  """Get (and create if necessary) the Object type of this"""
@@ -1796,27 +1836,32 @@ class Config:
1796
1836
  raise
1797
1837
  return xpmtype
1798
1838
 
1799
- def __getnewargs_ex__(self):
1800
- # __new__ will be called with those arguments when unserializing
1801
- return ((), {"__xpmobject__": True})
1839
+ def __new__(cls: Type[T], *args, **kwargs) -> T:
1840
+ """Returns an instance of a TypeConfig (for compatibility, use XPMConfig
1841
+ or C if possible)"""
1802
1842
 
1803
- @classmethod
1804
- def c(cls: Type[T], **kwargs) -> T:
1805
- """Allows typing to process easily"""
1806
- return cls.__new__(cls, **kwargs)
1843
+ # If this is an XPMValue, just return a new instance
1844
+ from experimaestro.core.types import XPMValue
1807
1845
 
1808
- def __new__(cls: Type[T], *args, __xpmobject__=False, **kwargs) -> T:
1809
- """Returns an instance of a TypeConfig when called __xpmobject__ is False,
1810
- and otherwise the real object
1811
- """
1846
+ if issubclass(cls, XPMValue):
1847
+ return object.__new__(cls)
1812
1848
 
1813
- if __xpmobject__:
1814
- # __init__ is called directly
1849
+ # If this is the XPMConfig, just return a new instance
1850
+ # __init__ will be called
1851
+ if issubclass(cls, TypeConfig):
1815
1852
  return object.__new__(cls)
1816
1853
 
1817
- # We use the configuration type
1818
- o = object.__new__(cls.__getxpmtype__().configtype)
1819
- o.__init__(*args, **kwargs)
1854
+ # otherwise, we use the configuration type
1855
+ o: TypeConfig = object.__new__(cls.__getxpmtype__().configtype)
1856
+ try:
1857
+ o.__init__(*args, **kwargs)
1858
+ except Exception:
1859
+ caller = inspect.getframeinfo(inspect.stack()[1][0])
1860
+ logger.error(
1861
+ "Init error in %s:%s"
1862
+ % (str(Path(caller.filename).absolute()), caller.lineno)
1863
+ )
1864
+ raise
1820
1865
  return o
1821
1866
 
1822
1867
  def __validate__(self):
@@ -27,13 +27,16 @@ from typing import (
27
27
  Callable,
28
28
  ClassVar,
29
29
  Dict,
30
+ Generic,
30
31
  List,
31
32
  Optional,
32
33
  Set,
34
+ Type,
33
35
  TypeVar,
34
36
  Union,
35
37
  overload,
36
38
  )
39
+ from typing_extensions import Self
37
40
 
38
41
  TConfig = TypeVar("TConfig", bound="Config")
39
42
 
@@ -205,12 +208,15 @@ class TypeConfig:
205
208
  class Config:
206
209
  __xpmtype__: ClassVar[ObjectType]
207
210
  __xpm__: ConfigInformation
211
+ __use_xpmobject__: ClassVar[bool]
212
+
213
+ XPMValue: Type[Self]
214
+ XPMConfig: Union[Type[Self], Type[TypeConfig[Self]]]
215
+ C: Union[Type[Self], Type[TypeConfig[Self]]]
216
+
208
217
  @classmethod
209
218
  def __getxpmtype__(cls) -> ObjectType: ...
210
- def __getnewargs_ex__(self): ...
211
- @classmethod
212
- def c(cls, **kwargs) -> T: ...
213
- def __new__(cls, *args, __xpmobject__: bool = ..., **kwargs) -> T: ...
219
+ def __new__(cls, *args, **kwargs) -> Self: ...
214
220
  def __validate__(self) -> None: ...
215
221
  def __post_init__(self) -> None: ...
216
222
  def __json__(self): ...
@@ -240,6 +246,6 @@ class Task(LightweightTask):
240
246
  def copyconfig(config_or_output: TConfig, **kwargs) -> TConfig: ...
241
247
  def setmeta(config: TConfig, flag: bool) -> TConfig: ...
242
248
 
243
- class TypeConfig:
249
+ class TypeConfig(Generic[T]):
244
250
  def __validate__(self):
245
251
  pass
@@ -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
4
+ from typing import Set, Union, Dict, Iterator, List, get_args, get_origin
5
5
  from collections import ChainMap
6
6
  from pathlib import Path
7
7
  import typing
@@ -126,6 +126,10 @@ class Type:
126
126
  if t:
127
127
  return DictType(Type.fromType(t[0]), Type.fromType(t[1]))
128
128
 
129
+ # Takes care of generics
130
+ if get_origin(key):
131
+ return GenericType(key)
132
+
129
133
  raise Exception("No type found for %s", key)
130
134
 
131
135
 
@@ -179,6 +183,12 @@ class SubmitHook(ABC):
179
183
  return hash((self.__class__, self.spec()))
180
184
 
181
185
 
186
+ class XPMValue:
187
+ """Jut marks a XPMValue"""
188
+
189
+ pass
190
+
191
+
182
192
  class ObjectType(Type):
183
193
  submit_hooks: Set[SubmitHook]
184
194
  """Hooks associated with this configuration"""
@@ -211,6 +221,8 @@ class ObjectType(Type):
211
221
  # Get the identifier
212
222
  if identifier is None and hasattr(tp, "__xpmid__"):
213
223
  __xpmid__ = getattr(tp, "__xpmid__")
224
+ if isinstance(__xpmid__, Identifier):
225
+ identifier = __xpmid__
214
226
  if inspect.ismethod(__xpmid__):
215
227
  identifier = Identifier(__xpmid__())
216
228
  elif "__xpmid__" in tp.__dict__:
@@ -247,7 +259,7 @@ class ObjectType(Type):
247
259
  else:
248
260
  self.basetype = tp
249
261
 
250
- # Create the type-specific configuration class
262
+ # --- Create the type-specific configuration class (XPMConfig)
251
263
  __configbases__ = tuple(
252
264
  s.__getxpmtype__().configtype
253
265
  for s in tp.__bases__
@@ -256,23 +268,18 @@ class ObjectType(Type):
256
268
 
257
269
  *tp_qual, tp_name = self.basetype.__qualname__.split(".")
258
270
  self.configtype = type(
259
- f"{tp_name}_XPMConfig", __configbases__ + (self.basetype,), {}
271
+ f"{tp_name}.XPMConfig", __configbases__ + (self.basetype,), {}
260
272
  )
261
273
  self.configtype.__qualname__ = ".".join(tp_qual + [self.configtype.__name__])
262
274
  self.configtype.__module__ = tp.__module__
263
275
 
264
- # Create the type-specific object class
265
- # (now, the same as basetype - but in the future, remove references)
266
- self.objecttype = self.basetype # type: type
267
- self.basetype._ = self.configtype
268
-
269
276
  # Return type is used by tasks to change the output
270
277
  if hasattr(self.basetype, "task_outputs") or False:
271
278
  self.returntype = get_type_hints(
272
279
  getattr(self.basetype, "task_outputs")
273
280
  ).get("return", typing.Any)
274
281
  else:
275
- self.returntype = self.objecttype
282
+ self.returntype = self.basetype
276
283
 
277
284
  # Registers ourselves
278
285
  self.basetype.__xpmtype__ = self
@@ -284,6 +291,11 @@ class ObjectType(Type):
284
291
  self.annotations = []
285
292
  self._deprecated = False
286
293
 
294
+ @property
295
+ def objecttype(self):
296
+ """Returns the object type"""
297
+ return self.basetype.XPMValue
298
+
287
299
  def addAnnotation(self, annotation):
288
300
  assert not self.__initialized__
289
301
  self.annotations.append(annotation)
@@ -636,3 +648,31 @@ class DictType(Type):
636
648
 
637
649
  def __repr__(self):
638
650
  return str(self)
651
+
652
+
653
+ class GenericType(Type):
654
+ def __init__(self, type: typing.Type):
655
+ self.type = type
656
+ self.origin = get_origin(type)
657
+
658
+ self.args = get_args(type)
659
+
660
+ def name(self):
661
+ return str(self.type)
662
+
663
+ def __repr__(self):
664
+ return repr(self.type)
665
+
666
+ def validate(self, value):
667
+ # Now, let's check generics...
668
+ mros = typingutils.generic_mro(type(value))
669
+ matching = next(
670
+ (mro for mro in mros if (get_origin(mro) or mro) is self.origin), None
671
+ )
672
+ target = get_origin(self.type) or self.type
673
+ if matching is None:
674
+ raise ValueError(
675
+ f"{type(value)} is not of type {target} ({type(value).__mro__})"
676
+ )
677
+
678
+ return value
@@ -0,0 +1,2 @@
1
+ class HandledException(Exception):
2
+ pass
@@ -10,6 +10,7 @@ import omegaconf
10
10
  import yaml
11
11
  from experimaestro import LauncherRegistry, RunMode, experiment
12
12
  from experimaestro.experiments.configuration import ConfigurationBase
13
+ from experimaestro.exceptions import HandledException
13
14
  from experimaestro.settings import get_workspace
14
15
  from omegaconf import OmegaConf, SCMode
15
16
  from termcolor import cprint
@@ -63,7 +64,7 @@ def load(yaml_file: Path):
63
64
  if parent := _data.get("parent", None):
64
65
  data.extend(load(yaml_file.parent / parent))
65
66
 
66
- return data
67
+ return data[::-1]
67
68
 
68
69
 
69
70
  @click.option("--debug", is_flag=True, help="Print debug information")
@@ -110,10 +111,16 @@ def load(yaml_file: Path):
110
111
  help="Working directory - if None, uses the default XPM " "working directory",
111
112
  )
112
113
  @click.option("--conf", "-c", "extra_conf", type=str, multiple=True)
114
+ @click.option(
115
+ "--pre-yaml", type=str, multiple=True, help="Add YAML file after the main one"
116
+ )
117
+ @click.option(
118
+ "--post-yaml", type=str, multiple=True, help="Add YAML file before the main one"
119
+ )
113
120
  @click.argument("args", nargs=-1, type=click.UNPROCESSED)
114
121
  @click.argument("yaml_file", metavar="YAML file", type=str)
115
122
  @click.command()
116
- def experiments_cli(
123
+ def experiments_cli( # noqa: C901
117
124
  yaml_file: str,
118
125
  xp_file: str,
119
126
  host: str,
@@ -123,6 +130,8 @@ def experiments_cli(
123
130
  env: List[Tuple[str, str]],
124
131
  run_mode: RunMode,
125
132
  extra_conf: List[str],
133
+ pre_yaml: List[str],
134
+ post_yaml: List[str],
126
135
  args: List[str],
127
136
  show: bool,
128
137
  debug: bool,
@@ -133,13 +142,17 @@ def experiments_cli(
133
142
  logging.getLogger("xpm.hash").setLevel(logging.INFO)
134
143
 
135
144
  # --- Loads the YAML
136
- yamls = load(Path(yaml_file))
145
+ yamls = []
146
+ for y in pre_yaml:
147
+ yamls.extend(load(Path(y)))
148
+ yamls.extend(load(Path(yaml_file)))
149
+ for y in post_yaml:
150
+ yamls.extend(load(Path(y)))
137
151
 
138
152
  # --- Get the XP file
139
153
  if xp_file is None:
140
- for data in yamls:
141
- if xp_file := data.get("file"):
142
- del data["file"]
154
+ for data in yamls[::-1]:
155
+ if xp_file := data.get("file", None):
143
156
  break
144
157
 
145
158
  if xp_file is None:
@@ -201,15 +214,15 @@ def experiments_cli(
201
214
  cprint(f"Error in configuration:\n\n{e}", "red", file=sys.stderr)
202
215
  sys.exit(1)
203
216
 
217
+ if show:
218
+ print(json.dumps(OmegaConf.to_container(configuration))) # noqa: T201
219
+ sys.exit(0)
220
+
204
221
  # Move to an object container
205
222
  configuration: schema = OmegaConf.to_container(
206
223
  configuration, structured_config_mode=SCMode.INSTANTIATE
207
224
  )
208
225
 
209
- if show:
210
- print(json.dumps(OmegaConf.to_container(configuration))) # noqa: T201
211
- sys.exit(0)
212
-
213
226
  # Get the working directory
214
227
  if workdir is None or not Path(workdir).is_dir():
215
228
  workdir = get_workspace(workdir).path.expanduser().resolve()
@@ -223,9 +236,13 @@ def experiments_cli(
223
236
  for key, value in env:
224
237
  xp.setenv(key, value)
225
238
 
226
- # Run the experiment
227
- helper.xp = xp
228
- helper.run(list(args), configuration)
239
+ try:
240
+ # Run the experiment
241
+ helper.xp = xp
242
+ helper.run(list(args), configuration)
243
+
244
+ # ... and wait
245
+ xp.wait()
229
246
 
230
- # ... and wait
231
- xp.wait()
247
+ except HandledException:
248
+ sys.exit(1)
@@ -20,14 +20,28 @@ def configuration(*args, **kwargs):
20
20
 
21
21
  @configuration()
22
22
  class ConfigurationBase:
23
+ """Base configuration for any experiment"""
24
+
23
25
  id: str = MISSING
24
- """ID of the experiment"""
26
+ """ID of the experiment
25
27
 
26
- description: str = ""
27
- """Description of the experiment"""
28
+ This ID is used by experimaestro when running as the experiment.
29
+ """
28
30
 
29
31
  file: str = "experiment"
30
- """qualified name (relative to the module) for the file containing a run function"""
32
+ """Relative path of the file containing a run function"""
31
33
 
32
34
  parent: Optional[str] = None
33
35
  """Relative path of a YAML file that should be merged"""
36
+
37
+ title: str = ""
38
+ """Short description of the experiment"""
39
+
40
+ subtitle: str = ""
41
+ """Allows to give some more details about the experiment"""
42
+
43
+ paper: str = ""
44
+ """Source paper for this experiment"""
45
+
46
+ description: str = ""
47
+ """Description of the experiment"""
@@ -85,6 +85,10 @@ def load_yaml(loader_cls: Type[Loader], path: Path):
85
85
  if not path.is_file():
86
86
  return None
87
87
 
88
+ logger.warning(
89
+ "Using YAML file to configure launchers is deprecated. Please remove %s using launchers.py",
90
+ path,
91
+ )
88
92
  logger.debug("Loading %s", path)
89
93
  with path.open("rt") as fp:
90
94
  loader = loader_cls(fp)
@@ -51,6 +51,13 @@ class CPUSpecification:
51
51
  def __lt__(self, other: "CPUSpecification"):
52
52
  return self.memory < other.memory and self.cores < other.cores
53
53
 
54
+ def total_memory(self, gpus: int = 0):
55
+ return max(
56
+ self.memory,
57
+ self.mem_per_cpu * self.cores,
58
+ self.cpu_per_gpu * self.mem_per_cpu * gpus,
59
+ )
60
+
54
61
 
55
62
  @define(kw_only=True)
56
63
  class HostSpecification:
@@ -9,6 +9,7 @@ from typing import Any, List, Optional, Set, TypeVar, Union, TYPE_CHECKING
9
9
  import enum
10
10
  import signal
11
11
  import asyncio
12
+ from experimaestro.exceptions import HandledException
12
13
  from experimaestro.notifications import LevelInformation, Reporter
13
14
  from typing import Dict
14
15
  from experimaestro.scheduler.services import Service
@@ -29,7 +30,7 @@ if TYPE_CHECKING:
29
30
  from experimaestro.launchers import Launcher
30
31
 
31
32
 
32
- class FailedExperiment(RuntimeError):
33
+ class FailedExperiment(HandledException):
33
34
  """Raised when an experiment failed"""
34
35
 
35
36
  pass
@@ -70,6 +71,17 @@ class JobState(enum.Enum):
70
71
  return self.value >= JobState.DONE.value
71
72
 
72
73
 
74
+ class JobFailureStatus(enum.Enum):
75
+ #: Job failed
76
+ DEPENDENCY = 0
77
+
78
+ #: Job dependency failed
79
+ FAILED = 1
80
+
81
+ #: Memory
82
+ MEMORY = 2
83
+
84
+
73
85
  class JobLock(Lock):
74
86
  def __init__(self, job):
75
87
  super().__init__()
@@ -130,6 +142,9 @@ class Job(Resource):
130
142
  self.config = config
131
143
  self.state: JobState = JobState.UNSCHEDULED
132
144
 
145
+ #: If a job has failed, indicates the failure status
146
+ self.failure_status: JobFailureStatus = None
147
+
133
148
  # Dependencies
134
149
  self.dependencies: Set[Dependency] = set() # as target
135
150
 
@@ -294,6 +309,7 @@ class Job(Resource):
294
309
  # Job completed
295
310
  if not self.state.finished():
296
311
  self.state = JobState.ERROR
312
+ self.failure_status = JobFailureStatus.DEPENDENCY
297
313
  self._readyEvent.set()
298
314
 
299
315
  if self.unsatisfied == 0:
@@ -846,13 +862,16 @@ class experiment:
846
862
 
847
863
  if self.failedJobs:
848
864
  # Show some more information
865
+ count = 0
849
866
  for job in self.failedJobs.values():
850
- logger.error(
851
- "Job %s failed, check the log file %s",
852
- job.relpath,
853
- job.stderr,
854
- )
855
- raise FailedExperiment(f"{len(self.failedJobs)} failed jobs")
867
+ if job.failure_status != JobFailureStatus.DEPENDENCY:
868
+ count += 1
869
+ logger.error(
870
+ "Job %s failed, check the log file %s",
871
+ job.relpath,
872
+ job.stderr,
873
+ )
874
+ raise FailedExperiment(f"{count} failed jobs")
856
875
 
857
876
  future = asyncio.run_coroutine_threadsafe(awaitcompletion(), self.loop)
858
877
  return future.result()
@@ -922,7 +941,7 @@ class experiment:
922
941
  if exc_type:
923
942
  # import faulthandler
924
943
  # faulthandler.dump_traceback()
925
- logger.exception(
944
+ logger.error(
926
945
  "Not waiting since an exception was thrown"
927
946
  " (some jobs may be running)"
928
947
  )
@@ -4,6 +4,7 @@ import pytest
4
4
  from experimaestro import Config, Task, Annotated, copyconfig, default
5
5
  from experimaestro.core.arguments import Param
6
6
  from experimaestro.core.objects import TypeConfig
7
+ from experimaestro.core.types import XPMValue
7
8
  from experimaestro.generators import pathgenerator
8
9
  from experimaestro.scheduler.workspace import RunMode
9
10
  from experimaestro.tests.utils import TemporaryExperiment
@@ -33,6 +34,10 @@ class C(B):
33
34
  pass
34
35
 
35
36
 
37
+ class D(B, A):
38
+ pass
39
+
40
+
36
41
  class DefaultAnnotationConfig(Config):
37
42
  a: Annotated[A, default(A(x=3))]
38
43
 
@@ -51,9 +56,10 @@ def test_object_config_default():
51
56
 
52
57
  def test_hierarchy():
53
58
  """Test if the object hierarchy is OK"""
54
- OA = A.__xpmtype__.objecttype
55
- OB = B.__xpmtype__.objecttype
56
- OC = C.__xpmtype__.objecttype
59
+ OA = A.__getxpmtype__().objecttype
60
+ OB = B.__getxpmtype__().objecttype
61
+ OC = C.__getxpmtype__().objecttype
62
+ OD = D.__getxpmtype__().objecttype
57
63
 
58
64
  assert issubclass(A, Config)
59
65
  assert issubclass(B, Config)
@@ -65,9 +71,10 @@ def test_hierarchy():
65
71
 
66
72
  assert issubclass(C, B)
67
73
 
68
- assert OA.__bases__ == (Config,)
69
- assert OB.__bases__ == (Config,)
70
- assert OC.__bases__ == (B,)
74
+ assert OA.__bases__ == (A, XPMValue)
75
+ assert OB.__bases__ == (B, XPMValue)
76
+ assert OC.__bases__ == (C, B.XPMValue)
77
+ assert OD.__bases__ == (D, B.XPMValue, A.XPMValue)
71
78
 
72
79
 
73
80
  class CopyConfig(Task):
@@ -232,8 +232,8 @@ def test_redefined_param():
232
232
  class B:
233
233
  x: Param[int] = 3
234
234
 
235
- atx = A._.__xpmtype__.getArgument("x")
236
- btx = B._.__xpmtype__.getArgument("x")
235
+ atx = A.C.__xpmtype__.getArgument("x")
236
+ btx = B.C.__xpmtype__.getArgument("x")
237
237
 
238
238
  assert atx.required
239
239
 
@@ -1,10 +1,21 @@
1
1
  import sys
2
2
  import typing
3
-
4
- if sys.version_info.major == 3 and sys.version_info.minor < 9:
5
- from typing_extensions import _AnnotatedAlias as AnnotatedAlias, get_args
6
- else:
7
- from typing import _AnnotatedAlias as AnnotatedAlias, get_args
3
+ from typing import Generic, Protocol
4
+
5
+ if sys.version_info.major == 3:
6
+ if sys.version_info.minor < 11:
7
+ from typing import _collect_type_vars as _collect_parameters
8
+ else:
9
+ from typing import _collect_parameters
10
+
11
+ if sys.version_info.minor < 9:
12
+ from typing_extensions import (
13
+ _AnnotatedAlias as AnnotatedAlias,
14
+ get_args,
15
+ get_origin,
16
+ )
17
+ else:
18
+ from typing import _AnnotatedAlias as AnnotatedAlias, get_args, get_origin
8
19
 
9
20
  GenericAlias = typing._GenericAlias
10
21
 
@@ -19,7 +30,9 @@ def get_optional(typehint):
19
30
  if isgenericalias(typehint) and typehint.__origin__ == typing.Union:
20
31
  if len(typehint.__args__) == 2:
21
32
  for ix in (0, 1):
22
- if typehint.__args__[ix] == type(None):
33
+ argtype = typehint.__args__[ix]
34
+ origin = get_origin(argtype) or argtype
35
+ if issubclass(origin, type(None)):
23
36
  return typehint.__args__[1 - ix]
24
37
  return None
25
38
 
@@ -63,3 +76,35 @@ def get_dict(typehint):
63
76
  assert len(typehint.__args__) == 2
64
77
  return typehint.__args__
65
78
  return None
79
+
80
+
81
+ # From https://github.com/python/typing/issues/777
82
+
83
+
84
+ def _generic_mro(result, tp):
85
+ origin = typing.get_origin(tp) or tp
86
+
87
+ result[origin] = tp
88
+ if hasattr(origin, "__orig_bases__"):
89
+ parameters = _collect_parameters(origin.__orig_bases__)
90
+ substitution = dict(zip(parameters, get_args(tp)))
91
+ for base in origin.__orig_bases__:
92
+ if get_origin(base) in result:
93
+ continue
94
+ base_parameters = getattr(base, "__parameters__", ())
95
+ if base_parameters:
96
+ base = base[tuple(substitution.get(p, p) for p in base_parameters)]
97
+ _generic_mro(result, base)
98
+
99
+
100
+ def generic_mro(tp):
101
+ origin = get_origin(tp)
102
+ if origin is None and not hasattr(tp, "__orig_bases__"):
103
+ if not isinstance(tp, type):
104
+ raise TypeError(f"{tp!r} is not a type or a generic alias")
105
+ return tp.__mro__
106
+ # sentinel value to avoid to subscript Generic and Protocol
107
+ result = {Generic: Generic, Protocol: Protocol}
108
+ _generic_mro(result, tp)
109
+ cls = origin if origin is not None else tp
110
+ return tuple(result.get(sub_cls, sub_cls) for sub_cls in cls.__mro__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: experimaestro
3
- Version: 1.4.3
3
+ Version: 1.5.0
4
4
  Summary: "Experimaestro is a computer science experiment manager"
5
5
  Home-page: https://github.com/experimaestro/experimaestro-python
6
6
  License: GPL-3
@@ -23,7 +23,6 @@ Classifier: Programming Language :: Python :: 3.12
23
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Requires-Dist: arpeggio (>=2,<3)
25
25
  Requires-Dist: attrs (>=23.1.0,<24.0.0)
26
- Requires-Dist: cached-property (>=1.5.2,<2.0.0) ; python_version < "3.9"
27
26
  Requires-Dist: click (>=8)
28
27
  Requires-Dist: decorator (>=5,<6)
29
28
  Requires-Dist: docstring-parser (>=0.15,<0.16)
@@ -46,7 +45,7 @@ Requires-Dist: rpyc (>=5,<6)
46
45
  Requires-Dist: sortedcontainers (>=2.4,<3.0)
47
46
  Requires-Dist: termcolor (>=2.3)
48
47
  Requires-Dist: tqdm (>=4.66.1,<5.0.0)
49
- Requires-Dist: typing-extensions (>=4.2) ; python_version < "3.11"
48
+ Requires-Dist: typing-extensions (>=4.2) ; python_version < "3.12"
50
49
  Requires-Dist: watchdog (>=2,<3)
51
50
  Project-URL: Documentation, https://experimaestro-python.readthedocs.io/
52
51
  Project-URL: Repository, https://github.com/experimaestro/experimaestro-python
@@ -7,19 +7,20 @@ experimaestro/commandline.py,sha256=NS1ubme8DTJtDS2uWwdHLQiZsl6TSK1LkNxu39c3-cw,
7
7
  experimaestro/compat.py,sha256=dQqE2ZNHLM2wtdfp7fBRYMfC33qNehVf9J6FGRBUQhs,171
8
8
  experimaestro/connectors/__init__.py,sha256=hxcBSeVLk_7oyiIlS3l-9dGg1NGtShwvRD1tS7f8D2M,5461
9
9
  experimaestro/connectors/local.py,sha256=6tlaZb0tvNS2mjsapiVbfY7kIfLICJad137VXBMz-xo,5816
10
- experimaestro/connectors/ssh.py,sha256=A6qObY_phynZVQFdAsyymP9c0fiUHwn04nShROzMrec,7896
10
+ experimaestro/connectors/ssh.py,sha256=hmvU6bCq6ZsB1Zjz273mzb9pdZdTinUhUqZFJTZl8Fg,8290
11
11
  experimaestro/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  experimaestro/core/arguments.py,sha256=dW32opqNEsULYr6nR7Zk8PqHsSCbLPclfXofw27GTpI,5620
13
13
  experimaestro/core/context.py,sha256=Q8_ngiHRBZ0laavXRJNiDvdCprrnROVTWaHfrwMdlG4,2638
14
- experimaestro/core/objects.py,sha256=UeQJmJ4597T89mjxNauGbnnfeiPkG4ifmoZuKDoi2Ds,62627
15
- experimaestro/core/objects.pyi,sha256=D8-kd-NU-pRl9-_ktXUne_k1YH6TG3VGqRaKV8mrZwk,7003
14
+ experimaestro/core/objects.py,sha256=s9dt7U8LDT9XNVY7mowdt1DvuBH19EO4obips1ZBSHg,63989
15
+ experimaestro/core/objects.pyi,sha256=Adi2OKCW0-B9GROlEUgxxBDK6SHUqccTBHTofRRJE9M,7131
16
16
  experimaestro/core/serialization.py,sha256=9tg5ebLF3YeZ_zG9DiTHPLthppvo7io710ohD_dcLTo,3836
17
17
  experimaestro/core/serializers.py,sha256=R_CAMyjjfU1oi-eHU6VlEUixJpFayGqEPaYu7VsD9xA,1197
18
- experimaestro/core/types.py,sha256=jQGlyC1xnsZ5NKct3FELIcXcXQMvNQvauCEuumyfBz8,19253
18
+ experimaestro/core/types.py,sha256=oTXD4UjMVoYn_Usxn2C4h6IGhYDTtekKB3O3hfeOynQ,20176
19
19
  experimaestro/core/utils.py,sha256=JfC3qGUS9b6FUHc2VxIYUI9ysNpXSQ1LjOBkjfZ8n7o,495
20
+ experimaestro/exceptions.py,sha256=cUy83WHM3GeynxmMk6QRr5xsnpqUAdAoc-m3KQVrE2o,44
20
21
  experimaestro/experiments/__init__.py,sha256=GcpDUIbCvhnv6rxFdAp4wTffCVNTv-InY6fbQAlTy-o,159
21
- experimaestro/experiments/cli.py,sha256=ftAo8Km0WM_CuooBvFMZXhNgTtisGkLajEPDWlTF3cg,6543
22
- experimaestro/experiments/configuration.py,sha256=tGaI7VJdXlD4VcBIpuJxnL1uXjrDaz7M4yUXyxwZlP0,834
22
+ experimaestro/experiments/cli.py,sha256=vne6skjbNmMIIgmlEJY0b_xoJIxgIjis07xL4byr-0o,7080
23
+ experimaestro/experiments/configuration.py,sha256=8GRqyLG1leF_NbvbFzqpm0yM24O0WjSNmQzvnuLnxxw,1150
23
24
  experimaestro/filter.py,sha256=DN1PrmS9yXoOa5Xnv001zbxzpdzvcVZFI9xZFKZ1-6g,5794
24
25
  experimaestro/generators.py,sha256=9NQ_TfDfASkArLnO4PF7s5Yoo9KWjlna2DCPzk5gJOI,1230
25
26
  experimaestro/huggingface.py,sha256=gnVlr6SZnbutYz4PLH0Q77n1TRF-uk-dR-3UFzFqAY0,2956
@@ -27,8 +28,8 @@ experimaestro/ipc.py,sha256=ltYqybPm_XfcQC3yiskMfhfI_1dREs-XRu0F83YsNws,1490
27
28
  experimaestro/launcherfinder/__init__.py,sha256=jIeT9uRKsIjUv-oyKt0MhFzXJJrSdpJKwM2vL9Sk5YY,294
28
29
  experimaestro/launcherfinder/base.py,sha256=NptPJ0e8CktdhOPejocSfI_B4mloeH_EmJrbXruUCSA,1020
29
30
  experimaestro/launcherfinder/parser.py,sha256=0qDXgdPce_PsWDy-hKTfxxjXjTAu4FA8moKtyllB2-Q,2129
30
- experimaestro/launcherfinder/registry.py,sha256=KdxQgUn56Iui3vGNXt8D-LiWz0CrxhiY09LYTxYj8PA,8751
31
- experimaestro/launcherfinder/specs.py,sha256=DHX7-07ffe_nrv-HSU8oCl_nd3519zhXkh3ymKBukIA,6250
31
+ experimaestro/launcherfinder/registry.py,sha256=KxlKZDl0PbBIh-JUnn0U8JYSHVt14L3Qox0DKJefwoc,8892
32
+ experimaestro/launcherfinder/specs.py,sha256=G8za6mEmkVxuZY_ab3OhWJIpONpcBMO_iXeB30sUbhI,6448
32
33
  experimaestro/launchers/__init__.py,sha256=lXn544sgJExr6uirILWzAXu_IfmfyqFZOt4OzRnjHXg,2525
33
34
  experimaestro/launchers/direct.py,sha256=VJzQNrUGnh-1Ovt6uw4yYIjXNu45QpR-_6V45lcZAfQ,1967
34
35
  experimaestro/launchers/oar.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -48,7 +49,7 @@ experimaestro/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
49
  experimaestro/rpyc.py,sha256=ZRKol-3tVoeoUITLNFenLF4dhWBLW_FvSV_GvsypmeI,3605
49
50
  experimaestro/run.py,sha256=tH9oG7o8_c7cp0cyBLIzWN7CVYjtz72SChz2X-FjYhs,4455
50
51
  experimaestro/scheduler/__init__.py,sha256=ERmmOxz_9mUkIuccNbzUa5Y6gVLLVDdyc4cCxbCCUbY,20
51
- experimaestro/scheduler/base.py,sha256=l-DPZJQMNHr-pbUwfLJbGS7Wp9jX1m9I9cLwdS5xbZ8,30914
52
+ experimaestro/scheduler/base.py,sha256=1SkwjSbOKTHs_pAB44z63xmcsgYrXsxxluO2MhJW5uU,31450
52
53
  experimaestro/scheduler/dependencies.py,sha256=n9XegwrmjayOIxt3xhuTEBVEBGSq4oeVdzz-FviDGXo,1994
53
54
  experimaestro/scheduler/environment.py,sha256=ZaSHSgAcZBmIj7b_eS1OvNQOuVLFvuw-qvqtYrc3Vms,2393
54
55
  experimaestro/scheduler/services.py,sha256=aCKkNZMULlceabqf-kOs_-C7KPINnjU3Q-I00o5x6iY,2189
@@ -112,9 +113,9 @@ experimaestro/tests/test_findlauncher.py,sha256=twliDFFI75KPuUJE6EgJwDMf8u3ic1LA
112
113
  experimaestro/tests/test_forward.py,sha256=XkZ2iOPETVj-kbTyniOQU9gyHXdfvn89GTwpMq9J6qc,780
113
114
  experimaestro/tests/test_identifier.py,sha256=fnl7jCUAg86npRrS3yeXtb9JysSKhs5czgFzW9yJ9Q8,13397
114
115
  experimaestro/tests/test_instance.py,sha256=awIIMnhiec_qDO6jZBqWDR13ReTzh3arK_60QDY6TLQ,1540
115
- experimaestro/tests/test_objects.py,sha256=wDUW7FXQ9rmB8EXJriJrO--E90Xq2CDIksPdEp8fEoM,1837
116
+ experimaestro/tests/test_objects.py,sha256=6PSG5FtqkiLg2OK26ZzvBYbJYPbFpyDYZtKS-DbiXIs,2037
116
117
  experimaestro/tests/test_outputs.py,sha256=DYzPk5TT_yLumy8SsQbl-S66ivVxJ-ERFrZ68KQZ4KU,781
117
- experimaestro/tests/test_param.py,sha256=YoJI2hhtyrTPEUHgEGZyu1C1pabW0oBTHcaFLKkr-qs,6405
118
+ experimaestro/tests/test_param.py,sha256=FcRF8HbjoW96SR6cTW3fqracLM4BivAsTq0iZvl14Ow,6405
118
119
  experimaestro/tests/test_progress.py,sha256=wtIGQzlV3ldd_wMng11LinVESchW-1J954mCJNlG28E,7580
119
120
  experimaestro/tests/test_serializers.py,sha256=xSCezAM9yH_Ix1wr7j0au9SyBv9DtZ7b0zs2-Ynt-VM,2338
120
121
  experimaestro/tests/test_snippets.py,sha256=rojnyDjtmAMnSuDUj6Bv9XEgdP8oQf2nVc132JF8vsM,3081
@@ -131,7 +132,7 @@ experimaestro/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
131
132
  experimaestro/tools/diff.py,sha256=FBCwupPrSwkEOcqYu0NWe7LTkuOW6r-_btoCel2_k_I,3436
132
133
  experimaestro/tools/documentation.py,sha256=O2UzjzodPqGot3YSe6NYlK7S42XpplakUdqxFpvjqHQ,9184
133
134
  experimaestro/tools/jobs.py,sha256=63bXhJ7RGdczLU_nxu2skGn-9dwgr4r5pD23qH4WeBA,3516
134
- experimaestro/typingutils.py,sha256=gkEhJ9Fir05xh5v45pYKmrGYiLCykGtlU8sL-ydFXS8,1785
135
+ experimaestro/typingutils.py,sha256=Gtsz_nU-t2CBVZP-EeUn-uAlxcbcS9Hp03Xy-TLDY-Q,3334
135
136
  experimaestro/utils/__init__.py,sha256=BdYguxAbR1jOQPV36OgGA31itaMvBJ6WVPV6b4Jn4xw,2434
136
137
  experimaestro/utils/asyncio.py,sha256=zEQQqZW6uHGnFknp_udt9WjjtqLNNMWun9TPL6FOF64,601
137
138
  experimaestro/utils/jobs.py,sha256=42FAdKcn_v_-M6hcQZPUBr9kbDt1eVsk3a4E8Gc4eu8,2394
@@ -140,8 +141,8 @@ experimaestro/utils/resources.py,sha256=gDjkrRjo7GULWyXmNXm_u1uqzEIAoAvJydICk56n
140
141
  experimaestro/utils/settings.py,sha256=jpFMqF0DLL4_P1xGal0zVR5cOrdD8O0Y2IOYvnRgN3k,793
141
142
  experimaestro/utils/yaml.py,sha256=jEjqXqUtJ333wNUdIc0o3LGvdsTQ9AKW9a9CCd-bmGU,6766
142
143
  experimaestro/xpmutils.py,sha256=S21eMbDYsHfvmZ1HmKpq5Pz5O-1HnCLYxKbyTBbASyQ,638
143
- experimaestro-1.4.3.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
144
- experimaestro-1.4.3.dist-info/METADATA,sha256=jFUznTSnjqfDBRU98s8mFP3sNBRzX5W9rJPOu3p8nMc,6338
145
- experimaestro-1.4.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
146
- experimaestro-1.4.3.dist-info/entry_points.txt,sha256=PhaEili_fDgn5q7rBJGip_uhGkRBq5l3Yuhg91zkcbk,574
147
- experimaestro-1.4.3.dist-info/RECORD,,
144
+ experimaestro-1.5.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
145
+ experimaestro-1.5.0.dist-info/METADATA,sha256=Mf0BErVRpCxCp2hnBnbH42ZE1cuK56TUSCO-WG4xMBQ,6265
146
+ experimaestro-1.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
147
+ experimaestro-1.5.0.dist-info/entry_points.txt,sha256=PhaEili_fDgn5q7rBJGip_uhGkRBq5l3Yuhg91zkcbk,574
148
+ experimaestro-1.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any