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.
- experimaestro/__init__.py +14 -4
- experimaestro/__main__.py +3 -423
- experimaestro/annotations.py +14 -4
- experimaestro/cli/__init__.py +311 -0
- experimaestro/{filter.py → cli/filter.py} +23 -9
- experimaestro/cli/jobs.py +268 -0
- experimaestro/cli/progress.py +269 -0
- experimaestro/click.py +0 -35
- experimaestro/commandline.py +3 -7
- experimaestro/connectors/__init__.py +29 -14
- experimaestro/connectors/local.py +19 -10
- experimaestro/connectors/ssh.py +27 -8
- experimaestro/core/arguments.py +45 -3
- experimaestro/core/callbacks.py +52 -0
- experimaestro/core/context.py +8 -9
- experimaestro/core/identifier.py +310 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +399 -772
- experimaestro/core/objects/config_utils.py +58 -0
- experimaestro/core/objects/config_walk.py +151 -0
- experimaestro/core/objects.pyi +15 -45
- experimaestro/core/serialization.py +63 -9
- experimaestro/core/serializers.py +1 -8
- experimaestro/core/types.py +104 -66
- experimaestro/experiments/cli.py +154 -72
- experimaestro/experiments/configuration.py +10 -1
- experimaestro/generators.py +6 -1
- experimaestro/ipc.py +4 -1
- experimaestro/launcherfinder/__init__.py +1 -1
- experimaestro/launcherfinder/base.py +2 -18
- experimaestro/launcherfinder/parser.py +8 -3
- experimaestro/launcherfinder/registry.py +52 -140
- experimaestro/launcherfinder/specs.py +49 -10
- experimaestro/launchers/direct.py +0 -47
- experimaestro/launchers/slurm/base.py +54 -14
- experimaestro/mkdocs/__init__.py +1 -1
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/notifications.py +38 -12
- experimaestro/progress.py +406 -0
- experimaestro/run.py +24 -3
- experimaestro/scheduler/__init__.py +18 -1
- experimaestro/scheduler/base.py +108 -808
- experimaestro/scheduler/dynamic_outputs.py +184 -0
- experimaestro/scheduler/experiment.py +387 -0
- experimaestro/scheduler/jobs.py +475 -0
- experimaestro/scheduler/signal_handler.py +32 -0
- experimaestro/scheduler/state.py +75 -0
- experimaestro/scheduler/workspace.py +27 -8
- experimaestro/scriptbuilder.py +18 -3
- experimaestro/server/__init__.py +36 -5
- experimaestro/server/data/1815e00441357e01619e.ttf +0 -0
- experimaestro/server/data/2463b90d9a316e4e5294.woff2 +0 -0
- experimaestro/server/data/2582b0e4bcf85eceead0.ttf +0 -0
- experimaestro/server/data/89999bdf5d835c012025.woff2 +0 -0
- experimaestro/server/data/914997e1bdfc990d0897.ttf +0 -0
- experimaestro/server/data/c210719e60948b211a12.woff2 +0 -0
- experimaestro/server/data/index.css +5187 -5068
- experimaestro/server/data/index.css.map +1 -1
- experimaestro/server/data/index.js +68887 -68064
- experimaestro/server/data/index.js.map +1 -1
- experimaestro/settings.py +45 -5
- experimaestro/sphinx/__init__.py +7 -17
- experimaestro/taskglobals.py +7 -2
- experimaestro/tests/core/__init__.py +0 -0
- experimaestro/tests/core/test_generics.py +206 -0
- experimaestro/tests/definitions_types.py +5 -3
- experimaestro/tests/launchers/bin/sbatch +34 -7
- experimaestro/tests/launchers/bin/srun +5 -0
- experimaestro/tests/launchers/common.py +17 -5
- experimaestro/tests/launchers/config_slurm/launchers.py +25 -0
- experimaestro/tests/restart.py +10 -5
- experimaestro/tests/tasks/all.py +23 -10
- experimaestro/tests/tasks/foreign.py +2 -4
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_dependencies.py +11 -17
- experimaestro/tests/test_experiment.py +73 -0
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_findlauncher.py +12 -5
- experimaestro/tests/test_forward.py +5 -5
- experimaestro/tests/test_generators.py +93 -0
- experimaestro/tests/test_identifier.py +182 -158
- experimaestro/tests/test_instance.py +19 -27
- experimaestro/tests/test_objects.py +13 -20
- experimaestro/tests/test_outputs.py +6 -6
- experimaestro/tests/test_param.py +68 -30
- experimaestro/tests/test_progress.py +4 -4
- experimaestro/tests/test_serializers.py +24 -64
- experimaestro/tests/test_ssh.py +7 -0
- experimaestro/tests/test_tags.py +50 -21
- experimaestro/tests/test_tasks.py +42 -51
- experimaestro/tests/test_tokens.py +11 -8
- experimaestro/tests/test_types.py +24 -21
- experimaestro/tests/test_validation.py +67 -110
- experimaestro/tests/token_reschedule.py +1 -1
- experimaestro/tokens.py +24 -13
- experimaestro/tools/diff.py +8 -1
- experimaestro/typingutils.py +20 -11
- experimaestro/utils/asyncio.py +6 -2
- experimaestro/utils/multiprocessing.py +44 -0
- experimaestro/utils/resources.py +11 -3
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/METADATA +28 -36
- experimaestro-2.0.0a8.dist-info/RECORD +166 -0
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/WHEEL +1 -1
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info}/entry_points.txt +0 -4
- experimaestro/launchers/slurm/cli.py +0 -29
- experimaestro/launchers/slurm/configuration.py +0 -597
- experimaestro/scheduler/environment.py +0 -94
- experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
- experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
- experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
- experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
- experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
- experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
- experimaestro/tests/launchers/config_slurm/launchers.yaml +0 -134
- experimaestro/utils/yaml.py +0 -202
- experimaestro-1.5.1.dist-info/RECORD +0 -148
- {experimaestro-1.5.1.dist-info → experimaestro-2.0.0a8.dist-info/licenses}/LICENSE +0 -0
experimaestro/core/types.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
200
|
-
:param
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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__().
|
|
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 (
|
|
257
|
+
) or (ConfigMixin,)
|
|
268
258
|
|
|
269
|
-
*tp_qual, tp_name = self.
|
|
270
|
-
self.
|
|
271
|
-
f"{tp_name}.XPMConfig", __configbases__ + (self.
|
|
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.
|
|
274
|
-
self.
|
|
263
|
+
self.config_type.__qualname__ = ".".join(tp_qual + [self.config_type.__name__])
|
|
264
|
+
self.config_type.__module__ = tp.__module__
|
|
275
265
|
|
|
276
|
-
#
|
|
277
|
-
if hasattr(self.
|
|
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.
|
|
269
|
+
getattr(self.value_type, "task_outputs")
|
|
280
270
|
).get("return", typing.Any)
|
|
281
271
|
else:
|
|
282
|
-
self.returntype = self.
|
|
272
|
+
self.returntype = self.value_type
|
|
283
273
|
|
|
284
|
-
# Registers ourselves
|
|
285
|
-
self.
|
|
286
|
-
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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.
|
|
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.
|
|
355
|
-
typekeys = set(self.
|
|
356
|
-
hints = get_type_hints(self.
|
|
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.
|
|
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.
|
|
366
|
+
self.value_type,
|
|
377
367
|
)
|
|
378
368
|
raise
|
|
379
369
|
|
|
380
370
|
def name(self):
|
|
381
|
-
return f"{self.
|
|
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.
|
|
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.
|
|
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.
|
|
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) ->
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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__})"
|