experimaestro 1.6.1__py3-none-any.whl → 1.15.2__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.
Files changed (98) hide show
  1. experimaestro/__init__.py +14 -3
  2. experimaestro/annotations.py +13 -3
  3. experimaestro/cli/filter.py +19 -5
  4. experimaestro/cli/jobs.py +12 -5
  5. experimaestro/commandline.py +3 -7
  6. experimaestro/connectors/__init__.py +27 -12
  7. experimaestro/connectors/local.py +19 -10
  8. experimaestro/connectors/ssh.py +1 -1
  9. experimaestro/core/arguments.py +35 -3
  10. experimaestro/core/callbacks.py +52 -0
  11. experimaestro/core/context.py +8 -9
  12. experimaestro/core/identifier.py +301 -0
  13. experimaestro/core/objects/__init__.py +44 -0
  14. experimaestro/core/{objects.py → objects/config.py} +364 -716
  15. experimaestro/core/objects/config_utils.py +58 -0
  16. experimaestro/core/objects/config_walk.py +151 -0
  17. experimaestro/core/objects.pyi +15 -45
  18. experimaestro/core/serialization.py +63 -9
  19. experimaestro/core/serializers.py +1 -8
  20. experimaestro/core/types.py +61 -6
  21. experimaestro/experiments/cli.py +79 -29
  22. experimaestro/experiments/configuration.py +3 -0
  23. experimaestro/generators.py +6 -1
  24. experimaestro/ipc.py +4 -1
  25. experimaestro/launcherfinder/parser.py +8 -3
  26. experimaestro/launcherfinder/registry.py +29 -10
  27. experimaestro/launcherfinder/specs.py +49 -10
  28. experimaestro/launchers/slurm/base.py +51 -13
  29. experimaestro/mkdocs/__init__.py +1 -1
  30. experimaestro/notifications.py +2 -1
  31. experimaestro/run.py +3 -1
  32. experimaestro/scheduler/base.py +114 -6
  33. experimaestro/scheduler/dynamic_outputs.py +184 -0
  34. experimaestro/scheduler/state.py +75 -0
  35. experimaestro/scheduler/workspace.py +2 -1
  36. experimaestro/scriptbuilder.py +13 -2
  37. experimaestro/server/data/0c35d18bf06992036b69.woff2 +0 -0
  38. experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
  39. experimaestro/server/data/219aa9140e099e6c72ed.woff2 +0 -0
  40. experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
  41. experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
  42. experimaestro/server/data/3a4004a46a653d4b2166.woff +0 -0
  43. experimaestro/server/data/3baa5b8f3469222b822d.woff +0 -0
  44. experimaestro/server/data/4d73cb90e394b34b7670.woff +0 -0
  45. experimaestro/server/data/4ef4218c522f1eb6b5b1.woff2 +0 -0
  46. experimaestro/server/data/5d681e2edae8c60630db.woff +0 -0
  47. experimaestro/server/data/6f420cf17cc0d7676fad.woff2 +0 -0
  48. experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
  49. experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
  50. experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
  51. experimaestro/server/data/c380809fd3677d7d6903.woff2 +0 -0
  52. experimaestro/server/data/f882956fd323fd322f31.woff +0 -0
  53. experimaestro/server/data/favicon.ico +0 -0
  54. experimaestro/server/data/index.css +22963 -0
  55. experimaestro/server/data/index.css.map +1 -0
  56. experimaestro/server/data/index.html +27 -0
  57. experimaestro/server/data/index.js +101770 -0
  58. experimaestro/server/data/index.js.map +1 -0
  59. experimaestro/server/data/login.html +22 -0
  60. experimaestro/server/data/manifest.json +15 -0
  61. experimaestro/settings.py +2 -2
  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 +16 -4
  70. experimaestro/tests/restart.py +9 -4
  71. experimaestro/tests/tasks/all.py +23 -10
  72. experimaestro/tests/tasks/foreign.py +2 -4
  73. experimaestro/tests/test_dependencies.py +0 -6
  74. experimaestro/tests/test_experiment.py +73 -0
  75. experimaestro/tests/test_findlauncher.py +11 -4
  76. experimaestro/tests/test_forward.py +5 -5
  77. experimaestro/tests/test_generators.py +93 -0
  78. experimaestro/tests/test_identifier.py +114 -99
  79. experimaestro/tests/test_instance.py +6 -21
  80. experimaestro/tests/test_objects.py +20 -4
  81. experimaestro/tests/test_param.py +60 -22
  82. experimaestro/tests/test_serializers.py +24 -64
  83. experimaestro/tests/test_tags.py +5 -11
  84. experimaestro/tests/test_tasks.py +10 -23
  85. experimaestro/tests/test_tokens.py +3 -2
  86. experimaestro/tests/test_types.py +20 -17
  87. experimaestro/tests/test_validation.py +48 -91
  88. experimaestro/tokens.py +16 -5
  89. experimaestro/typingutils.py +8 -8
  90. experimaestro/utils/asyncio.py +6 -2
  91. experimaestro/utils/multiprocessing.py +44 -0
  92. experimaestro/utils/resources.py +7 -3
  93. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/METADATA +27 -34
  94. experimaestro-1.15.2.dist-info/RECORD +159 -0
  95. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/WHEEL +1 -1
  96. experimaestro-1.6.1.dist-info/RECORD +0 -122
  97. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info}/entry_points.txt +0 -0
  98. {experimaestro-1.6.1.dist-info → experimaestro-1.15.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,58 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any, Dict, Set
3
+
4
+
5
+ def getqualattr(module, qualname):
6
+ """Get a qualified attributed value"""
7
+ cls = module
8
+ for part in qualname.split("."):
9
+ cls = getattr(cls, part)
10
+ return cls
11
+
12
+
13
+ @contextmanager
14
+ def add_to_path(p):
15
+ """Temporarily add a path to sys.path"""
16
+ import sys
17
+
18
+ old_path = sys.path
19
+ sys.path = sys.path[:]
20
+ sys.path.insert(0, p)
21
+ try:
22
+ yield
23
+ finally:
24
+ sys.path = old_path
25
+
26
+
27
+ class ObjectStore:
28
+ def __init__(self):
29
+ self.store: Dict[int, Any] = {}
30
+ self.constructed: Set[int] = set()
31
+
32
+ def set_constructed(self, identifier: int):
33
+ self.constructed.add(identifier)
34
+
35
+ def is_constructed(self, identifier: int):
36
+ return identifier in self.constructed
37
+
38
+ def retrieve(self, identifier: int):
39
+ return self.store.get(identifier, None)
40
+
41
+ def add_stub(self, identifier: int, stub: Any):
42
+ self.store[identifier] = stub
43
+
44
+
45
+ class SealedError(Exception):
46
+ """Exception when trying to modify a sealed configuration"""
47
+
48
+ pass
49
+
50
+
51
+ class TaggedValue:
52
+ def __init__(self, value):
53
+ self.value = value
54
+
55
+
56
+ class classproperty(property):
57
+ def __get__(self, owner_self, owner_cls):
58
+ return self.fget(owner_cls)
@@ -0,0 +1,151 @@
1
+ from pathlib import Path
2
+ from enum import Enum
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ Tuple,
7
+ )
8
+ from contextlib import contextmanager
9
+
10
+
11
+ class ConfigWalkContext:
12
+ """Context when generating values in configurations"""
13
+
14
+ @property
15
+ def path(self):
16
+ """Returns the path of the job directory"""
17
+ raise NotImplementedError()
18
+
19
+ def __init__(self):
20
+ self._configpath = None
21
+
22
+ @property
23
+ def task(self):
24
+ return None
25
+
26
+ def currentpath(self) -> Path:
27
+ """Returns the configuration folder"""
28
+ if self._configpath:
29
+ return self.path / self._configpath
30
+ return self.path
31
+
32
+ @contextmanager
33
+ def push(self, key: str):
34
+ """Push a new key to contextualize paths"""
35
+ p = self._configpath
36
+ try:
37
+ self._configpath = (Path("out") if p is None else p) / key
38
+ yield key
39
+ finally:
40
+ self._configpath = p
41
+
42
+
43
+ class ConfigWalk:
44
+ """Allows to perform an operation on all nested configurations"""
45
+
46
+ def __init__(self, context: ConfigWalkContext = None, recurse_task=False):
47
+ """
48
+
49
+ :param recurse_task: Recurse into linked tasks
50
+ :param context: The context, by default only tracks the position in the
51
+ config tree
52
+ """
53
+ self.recurse_task = recurse_task
54
+ self.context = ConfigWalkContext() if context is None else context
55
+
56
+ # Stores already visited nodes
57
+ self.visited = {}
58
+
59
+ def preprocess(self, config) -> Tuple[bool, Any]:
60
+ """Returns a tuple boolean/value
61
+
62
+ The boolean value is used to stop the processing if False.
63
+ The value is returned
64
+ """
65
+ return True, None
66
+
67
+ def postprocess(self, stub, config, values: Dict[str, Any]):
68
+ return stub
69
+
70
+ def list(self, i: int):
71
+ return self.context.push(str(i))
72
+
73
+ def map(self, k: str):
74
+ """Provides a path context when processing a tree"""
75
+ return self.context.push(k)
76
+
77
+ def stub(self, config):
78
+ return config
79
+
80
+ def __call__(self, x):
81
+ from experimaestro.core.objects import Config
82
+ from experimaestro.core.objects import ConfigInformation # noqa: F401
83
+
84
+ if isinstance(x, Config):
85
+ info = x.__xpm__ # type: ConfigInformation
86
+
87
+ # Avoid loops
88
+ xid = id(x)
89
+ if xid in self.visited:
90
+ return self.visited[xid]
91
+
92
+ # Get a stub
93
+ stub = self.stub(x)
94
+ self.visited[xid] = stub
95
+
96
+ # Pre-process
97
+ flag, value = self.preprocess(x)
98
+
99
+ if not flag:
100
+ # Stop processing and returns value
101
+ return value
102
+
103
+ # Process all the arguments
104
+ result = {}
105
+ for arg, v in info.xpmvalues():
106
+ if v is not None:
107
+ with self.map(arg.name):
108
+ result[arg.name] = self(v)
109
+ else:
110
+ result[arg.name] = None
111
+
112
+ # Deals with init tasks
113
+ if info.init_tasks:
114
+ with self.map("__init_tasks__"):
115
+ self(info.init_tasks)
116
+
117
+ # Process task if different
118
+ if (
119
+ x.__xpm__.task is not None
120
+ and self.recurse_task
121
+ and x.__xpm__.task is not x
122
+ ):
123
+ with self.map("__task__"):
124
+ self(x.__xpm__.task)
125
+
126
+ processed = self.postprocess(stub, x, result)
127
+ self.visited[xid] = processed
128
+ return processed
129
+
130
+ if x is None:
131
+ return None
132
+
133
+ if isinstance(x, list):
134
+ result = []
135
+ for i, sv in enumerate(x):
136
+ with self.list(i):
137
+ result.append(self(sv))
138
+ return result
139
+
140
+ if isinstance(x, dict):
141
+ result = {}
142
+ for key, value in x.items():
143
+ assert isinstance(key, (str, float, int))
144
+ with self.map(key):
145
+ result[key] = self(value)
146
+ return result
147
+
148
+ if isinstance(x, (float, int, str, Path, Enum)):
149
+ return x
150
+
151
+ raise NotImplementedError(f"Cannot handle a value of type {type(x)}")
@@ -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
@@ -40,40 +41,8 @@ from typing_extensions import Self
40
41
 
41
42
  TConfig = TypeVar("TConfig", bound="Config")
42
43
 
43
- class Identifier:
44
- main: Incomplete
45
- sub: Incomplete
46
- def __init__(self, main: bytes, sub: Optional[bytes] = ...) -> None: ...
47
- def all(self): ...
48
- def state_dict(self): ...
49
- @staticmethod
50
- def from_state_dict(data: Union[Dict[str, str], str]): ...
51
-
52
- def is_ignored(value): ...
53
- def remove_meta(value): ...
54
-
55
44
  class ObjectStore: ...
56
45
 
57
- class HashComputer:
58
- OBJECT_ID: bytes
59
- INT_ID: bytes
60
- FLOAT_ID: bytes
61
- STR_ID: bytes
62
- PATH_ID: bytes
63
- NAME_ID: bytes
64
- NONE_ID: bytes
65
- LIST_ID: bytes
66
- TASK_ID: bytes
67
- DICT_ID: bytes
68
- ENUM_ID: bytes
69
- def __init__(self) -> None: ...
70
- def identifier(self) -> Identifier: ...
71
- def update(self, value, myself: bool = ...): ...
72
-
73
- def updatedependencies(
74
- dependencies, value: Config, path: List[str], taskids: Set[int]
75
- ): ...
76
-
77
46
  class TaggedValue:
78
47
  value: Incomplete
79
48
  def __init__(self, value) -> None: ...
@@ -108,6 +77,10 @@ class ConfigWalk(ConfigProcessing):
108
77
  def map(self, k: str): ...
109
78
 
110
79
  def getqualattr(module, qualname): ...
80
+ @define(frozen=True)
81
+ class WatchedOutput:
82
+ config: "Config"
83
+ method_name: str
111
84
 
112
85
  class ConfigInformation:
113
86
  LOADING: bool
@@ -116,7 +89,8 @@ class ConfigInformation:
116
89
  values: Dict[str, Any]
117
90
  job: Job
118
91
  dependencies: Incomplete
119
- def __init__(self, pyobject: TypeConfig) -> None: ...
92
+ watched_outputs: List[WatchedOutput]
93
+ def __init__(self, pyobject: ConfigMixin) -> None: ...
120
94
  def set_meta(self, value: Optional[bool]): ...
121
95
  @property
122
96
  def meta(self): ...
@@ -156,7 +130,7 @@ class ConfigInformation:
156
130
  definitions: List[Dict],
157
131
  as_instance: bool = ...,
158
132
  save_directory: Optional[Path] = ...,
159
- ) -> TypeConfig: ...
133
+ ) -> ConfigMixin: ...
160
134
  @overload
161
135
  @staticmethod
162
136
  def fromParameters(
@@ -177,7 +151,7 @@ class ConfigInformation:
177
151
  def clone(v): ...
178
152
  def cache(fn, name: str): ...
179
153
 
180
- class TypeConfig:
154
+ class ConfigMixin:
181
155
  __xpmtype__: ObjectType
182
156
  __xpm__: Incomplete
183
157
  def __init__(self, **kwargs) -> None: ...
@@ -194,7 +168,7 @@ class TypeConfig:
194
168
  *,
195
169
  workspace: Incomplete | None = ...,
196
170
  launcher: Incomplete | None = ...,
197
- run_mode: RunMode = ...
171
+ run_mode: RunMode = ...,
198
172
  ): ...
199
173
  def stdout(self): ...
200
174
  def stderr(self): ...
@@ -211,8 +185,8 @@ class Config:
211
185
  __use_xpmobject__: ClassVar[bool]
212
186
 
213
187
  XPMValue: Type[Self]
214
- XPMConfig: Union[Type[Self], Type[TypeConfig[Self]]]
215
- C: Union[Type[Self], Type[TypeConfig[Self]]]
188
+ XPMConfig: Union[Type[Self], Type[ConfigMixin[Self]]]
189
+ C: Union[Type[Self], Type[ConfigMixin[Self]]]
216
190
 
217
191
  @classmethod
218
192
  def __getxpmtype__(cls) -> ObjectType: ...
@@ -221,17 +195,13 @@ class Config:
221
195
  def __post_init__(self) -> None: ...
222
196
  def __json__(self): ...
223
197
  def __identifier__(self) -> Identifier: ...
224
- def add_pretasks(self, *tasks: "LightweightTask"): ...
225
- def add_pretasks_from(self, configs: "Config"): ...
226
198
  def copy_dependencies(self, other: "Config"): ...
227
- @property
228
- def pre_tasks(self) -> List["LightweightTask"]: ...
229
199
 
230
200
  class LightweightTask(Config):
231
201
  def execute(self) -> None: ...
232
202
 
233
203
  class Task(LightweightTask):
234
- __tags__: Dict[str, str]
204
+ # __tags__: Dict[str, str]
235
205
 
236
206
  def submit(
237
207
  self,
@@ -239,13 +209,13 @@ class Task(LightweightTask):
239
209
  workspace: Incomplete | None = ...,
240
210
  launcher: Incomplete | None = ...,
241
211
  run_mode: RunMode = ...,
242
- init_tasks: List["LightweightTask"] = []
212
+ init_tasks: List["LightweightTask"] = [],
243
213
  ): ...
244
214
  def task_outputs(self, dep: Callable[[Config], None]) -> Any: ...
245
215
 
246
216
  def copyconfig(config_or_output: TConfig, **kwargs) -> TConfig: ...
247
217
  def setmeta(config: TConfig, flag: bool) -> TConfig: ...
248
218
 
249
- class TypeConfig(Generic[T]):
219
+ class ConfigMixin(Generic[T]):
250
220
  def __validate__(self):
251
221
  pass
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  from pathlib import Path
3
- from typing import Any, Dict, List, Tuple, Union, TYPE_CHECKING
3
+ from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
4
4
  from experimaestro.core.context import (
5
5
  SerializationContext,
6
6
  SerializedPath,
@@ -17,7 +17,7 @@ def json_object(context: SerializationContext, value: Any, objects=[]):
17
17
 
18
18
  if isinstance(value, Config):
19
19
  value.__xpm__.__get_objects__(objects, context)
20
- elif isinstance(value, list):
20
+ elif isinstance(value, (list, tuple)):
21
21
  for el in value:
22
22
  ConfigInformation.__collect_objects__(el, objects, context)
23
23
  elif isinstance(value, dict):
@@ -36,28 +36,38 @@ def state_dict(context: SerializationContext, obj: Any):
36
36
  :param context: The serialization context
37
37
  :param obj: the object to serialize
38
38
  """
39
- objects = []
39
+ objects: list[Any] = []
40
40
  data = json_object(context, obj, objects)
41
41
  return {"objects": objects, "data": data}
42
42
 
43
43
 
44
- def save(obj: Any, save_directory: Path):
44
+ def save_definition(obj: Any, context: SerializationContext, path: Path):
45
+ data = state_dict(context, obj)
46
+ with path.open("wt") as out:
47
+ json.dump(data, out)
48
+
49
+
50
+ def save(obj: Any, save_directory: Optional[Path]):
45
51
  """Saves an object into a disk file
46
52
 
53
+ The serialization process also stores in the given folder the different
54
+ files or folders that are registered as Path parameters (or
55
+ meta-parameters).
56
+
47
57
  :param save_directory: The directory in which the object and its data will
48
58
  be saved (by default, the object is saved in "definition.json")
49
59
  """
50
60
  context = SerializationContext(save_directory=save_directory)
51
-
52
- data = state_dict(context, obj)
53
- with (save_directory / "definition.json").open("wt") as out:
54
- json.dump(data, out)
61
+ save_definition(
62
+ obj, context, save_directory / "definition.json" if save_directory else None
63
+ )
55
64
 
56
65
 
57
66
  def get_data_loader(path: Union[str, Path, SerializedPathLoader]):
58
67
  if path is None:
59
68
 
60
- def data_loader():
69
+ def data_loader(_: Union[str, Path, SerializedPathLoader]):
70
+ # Just raise an exception
61
71
  raise RuntimeError("No serialization path was given")
62
72
 
63
73
  return data_loader
@@ -126,3 +136,47 @@ def from_task_dir(
126
136
  content["data"] = {"type": "python", "value": content["objects"][-1]["id"]}
127
137
 
128
138
  return from_state_dict(content, as_instance=as_instance)
139
+
140
+
141
+ def serialize(
142
+ obj: Any, save_directory: Path, *, init_tasks: list["LightweightTask"] = []
143
+ ):
144
+ """Saves an object into a disk file, including initialization tasks
145
+
146
+ The serialization process also stores in the given folder the different
147
+ files or folders that are registered as Path parameters (or
148
+ meta-parameters).
149
+
150
+ :param save_directory: The directory in which the object and its data will
151
+ be saved (by default, the object is saved in "definition.json")
152
+ :param init_tasks: The optional
153
+ """
154
+ context = SerializationContext(save_directory=save_directory)
155
+ save_definition((obj, init_tasks), context, save_directory / "definition.json")
156
+
157
+
158
+ def deserialize(
159
+ path: Union[str, Path, SerializedPathLoader],
160
+ as_instance: bool = False,
161
+ ) -> tuple[Any, List["LightweightTask"]] | Any:
162
+ """Load data from disk, and initialize the object
163
+
164
+ :param path: A directory or a function that transforms relative file path
165
+ into absolute ones
166
+ :param as_instance: returns instances instead of configuration objects
167
+ :returns: either the object (as_instance is true), or a tuple
168
+ """
169
+ data_loader = get_data_loader(path)
170
+
171
+ with data_loader("definition.json").open("rt") as fh:
172
+ content = json.load(fh)
173
+
174
+ object, init_tasks = from_state_dict(content, data_loader, as_instance=as_instance)
175
+
176
+ if as_instance:
177
+ for init_task in init_tasks:
178
+ init_task.execute()
179
+
180
+ return object
181
+
182
+ return object, init_tasks
@@ -1,10 +1,8 @@
1
- from typing import List, TypeVar, Callable, Any
2
- from pathlib import Path
1
+ from typing import List, TypeVar
3
2
  from experimaestro import Param
4
3
 
5
4
  from .objects import Config, LightweightTask
6
5
  from .arguments import DataPath
7
- from experimaestro import copyconfig
8
6
 
9
7
 
10
8
  class SerializationLWTask(LightweightTask):
@@ -39,8 +37,3 @@ class PathSerializationLWTask(SerializationLWTask):
39
37
 
40
38
  path: DataPath
41
39
  """Path containing the data"""
42
-
43
- @classmethod
44
- def construct(cls, value: T, path: Path, dep: Callable[[Config], Any]) -> T:
45
- value = copyconfig(value)
46
- return value.add_pretasks(dep(cls(value=value, path=path)))
@@ -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
 
@@ -210,7 +217,7 @@ class ObjectType(Type):
210
217
  identifier: Union[str, Identifier] = None,
211
218
  ):
212
219
  """Creates a type"""
213
- from .objects import Config, TypeConfig
220
+ from .objects import Config, ConfigMixin
214
221
 
215
222
  # Task related attributes
216
223
  self.taskcommandfactory = None
@@ -223,7 +230,7 @@ class ObjectType(Type):
223
230
  __xpmid__ = getattr(tp, "__xpmid__")
224
231
  if isinstance(__xpmid__, Identifier):
225
232
  identifier = __xpmid__
226
- if inspect.ismethod(__xpmid__):
233
+ elif inspect.ismethod(__xpmid__):
227
234
  identifier = Identifier(__xpmid__())
228
235
  elif "__xpmid__" in tp.__dict__:
229
236
  identifier = Identifier(__xpmid__)
@@ -264,7 +271,7 @@ class ObjectType(Type):
264
271
  s.__getxpmtype__().configtype
265
272
  for s in tp.__bases__
266
273
  if issubclass(s, Config) and (s is not Config)
267
- ) or (TypeConfig,)
274
+ ) or (ConfigMixin,)
268
275
 
269
276
  *tp_qual, tp_name = self.basetype.__qualname__.split(".")
270
277
  self.configtype = type(
@@ -576,7 +583,7 @@ class PathType(Type):
576
583
  return Path(value.get("$value"))
577
584
 
578
585
  if not isinstance(value, (str, Path)):
579
- raise TypeError("value is not a pathlike value")
586
+ raise TypeError(f"value is not a pathlike value ({type(value)})")
580
587
  return Path(value)
581
588
 
582
589
  @property
@@ -593,6 +600,23 @@ class AnyType(Type):
593
600
  return value
594
601
 
595
602
 
603
+ class TypeVarType(Type):
604
+ def __init__(self, typevar: TypeVar):
605
+ self.typevar = typevar
606
+
607
+ def name(self):
608
+ return str(self.typevar)
609
+
610
+ def validate(self, value):
611
+ return value
612
+
613
+ def __str__(self):
614
+ return f"TypeVar({self.typevar})"
615
+
616
+ def __repr__(self):
617
+ return f"TypeVar({self.typevar})"
618
+
619
+
596
620
  Any = AnyType()
597
621
 
598
622
 
@@ -631,6 +655,32 @@ class EnumType(Type):
631
655
  return f"Enum({self.type})"
632
656
 
633
657
 
658
+ class UnionType(Type):
659
+ def __init__(self, types: List[Type]):
660
+ self.types = types
661
+
662
+ def name(self):
663
+ return "Union[" + ", ".join(t.name() for t in self.types) + "]"
664
+
665
+ def __str__(self):
666
+ return "[" + " | ".join(t.name() for t in self.types) + " ]"
667
+
668
+ def __repr__(self):
669
+ return str(self)
670
+
671
+ def validate(self, value):
672
+ for subtype in self.types:
673
+ try:
674
+ return subtype.validate(value)
675
+ except ValueError:
676
+ pass
677
+ except TypeError:
678
+ pass
679
+
680
+ if not isinstance(value, dict):
681
+ raise ValueError(f"value is not within the types {self}")
682
+
683
+
634
684
  class DictType(Type):
635
685
  def __init__(self, keytype: Type, valuetype: Type):
636
686
  self.keytype = keytype
@@ -668,6 +718,10 @@ class GenericType(Type):
668
718
  def __repr__(self):
669
719
  return repr(self.type)
670
720
 
721
+ def identifier(self):
722
+ """Returns the identifier of the type"""
723
+ return Identifier(f"{self.origin}.{self.type}")
724
+
671
725
  def validate(self, value):
672
726
  # Now, let's check generics...
673
727
  mros = typingutils.generic_mro(type(value))
@@ -675,6 +729,7 @@ class GenericType(Type):
675
729
  (mro for mro in mros if (get_origin(mro) or mro) is self.origin), None
676
730
  )
677
731
  target = get_origin(self.type) or self.type
732
+
678
733
  if matching is None:
679
734
  raise ValueError(
680
735
  f"{type(value)} is not of type {target} ({type(value).__mro__})"