experimaestro 1.11.1__py3-none-any.whl → 2.0.0a3__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/annotations.py +1 -1
- experimaestro/cli/__init__.py +10 -11
- experimaestro/cli/progress.py +269 -0
- experimaestro/core/arguments.py +20 -1
- experimaestro/core/identifier.py +11 -2
- experimaestro/core/objects/config.py +119 -97
- experimaestro/core/objects/config_walk.py +3 -1
- experimaestro/core/types.py +35 -57
- experimaestro/launcherfinder/registry.py +3 -3
- experimaestro/mkdocs/base.py +6 -8
- experimaestro/notifications.py +12 -3
- experimaestro/progress.py +406 -0
- experimaestro/settings.py +4 -2
- experimaestro/tests/launchers/common.py +2 -2
- experimaestro/tests/restart.py +1 -1
- experimaestro/tests/test_checkers.py +2 -2
- experimaestro/tests/test_dependencies.py +12 -12
- experimaestro/tests/test_experiment.py +3 -3
- experimaestro/tests/test_file_progress.py +425 -0
- experimaestro/tests/test_file_progress_integration.py +477 -0
- experimaestro/tests/test_generators.py +68 -0
- experimaestro/tests/test_identifier.py +90 -81
- experimaestro/tests/test_instance.py +16 -9
- experimaestro/tests/test_objects.py +9 -32
- experimaestro/tests/test_outputs.py +6 -6
- experimaestro/tests/test_param.py +14 -14
- experimaestro/tests/test_progress.py +4 -4
- experimaestro/tests/test_serializers.py +5 -5
- experimaestro/tests/test_tags.py +15 -15
- experimaestro/tests/test_tasks.py +40 -36
- experimaestro/tests/test_tokens.py +8 -6
- experimaestro/tests/test_types.py +10 -10
- experimaestro/tests/test_validation.py +19 -19
- experimaestro/tests/token_reschedule.py +1 -1
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0a3.dist-info}/METADATA +1 -1
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0a3.dist-info}/RECORD +39 -34
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0a3.dist-info}/LICENSE +0 -0
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0a3.dist-info}/WHEEL +0 -0
- {experimaestro-1.11.1.dist-info → experimaestro-2.0.0a3.dist-info}/entry_points.txt +0 -0
|
@@ -25,7 +25,6 @@ from typing import (
|
|
|
25
25
|
Optional,
|
|
26
26
|
Set,
|
|
27
27
|
Tuple,
|
|
28
|
-
Type,
|
|
29
28
|
TypeVar,
|
|
30
29
|
Union,
|
|
31
30
|
overload,
|
|
@@ -122,11 +121,11 @@ class ConfigInformation:
|
|
|
122
121
|
def __init__(self, pyobject: "ConfigMixin"):
|
|
123
122
|
# The underlying pyobject and XPM type
|
|
124
123
|
self.pyobject = pyobject
|
|
125
|
-
self.xpmtype = pyobject.__xpmtype__
|
|
124
|
+
self.xpmtype: "ObjectType" = pyobject.__xpmtype__
|
|
126
125
|
self.values = {}
|
|
127
126
|
|
|
128
127
|
# Meta-informations
|
|
129
|
-
self._tags = {}
|
|
128
|
+
self._tags: dict[str, Any] = {}
|
|
130
129
|
self._initinfo = ""
|
|
131
130
|
|
|
132
131
|
self._taskoutput = None
|
|
@@ -142,7 +141,7 @@ class ConfigInformation:
|
|
|
142
141
|
#: True when this configuration was loaded from disk
|
|
143
142
|
self.loaded = False
|
|
144
143
|
|
|
145
|
-
#
|
|
144
|
+
# Explicitly added dependencies
|
|
146
145
|
self.dependencies = []
|
|
147
146
|
|
|
148
147
|
# Concrete type variables resolutions
|
|
@@ -170,6 +169,34 @@ class ConfigInformation:
|
|
|
170
169
|
self._sealed = False
|
|
171
170
|
self._meta = None
|
|
172
171
|
|
|
172
|
+
# This contains the list of generated values (using context) in this
|
|
173
|
+
# configuration or any sub-configuration, is generated. This prevents
|
|
174
|
+
# problem when a configuration with generated values is re-used.
|
|
175
|
+
self._generated_values = []
|
|
176
|
+
|
|
177
|
+
def get_generated_paths(
|
|
178
|
+
self, path: list[str] = None, paths: list[str] = None
|
|
179
|
+
) -> list[str]:
|
|
180
|
+
"""Get the list of generated paths, useful to track down those
|
|
181
|
+
|
|
182
|
+
:param path: The current path
|
|
183
|
+
:param paths: The list of generated paths so far, defaults to None
|
|
184
|
+
:return: The full list of generated paths
|
|
185
|
+
"""
|
|
186
|
+
paths = [] if paths is None else paths
|
|
187
|
+
path = [] if path is None else path
|
|
188
|
+
|
|
189
|
+
for key in self._generated_values:
|
|
190
|
+
value = self.values[key]
|
|
191
|
+
if isinstance(value, ConfigMixin) and value.__xpm__._generated_values:
|
|
192
|
+
path.append(key)
|
|
193
|
+
value.__xpm__.get_generated_paths(path, paths)
|
|
194
|
+
path.pop()
|
|
195
|
+
else:
|
|
196
|
+
paths.append(".".join(path + [key]))
|
|
197
|
+
|
|
198
|
+
return paths
|
|
199
|
+
|
|
173
200
|
def set_meta(self, value: Optional[bool]):
|
|
174
201
|
"""Sets the meta flag"""
|
|
175
202
|
assert not self._sealed, "Configuration is sealed"
|
|
@@ -198,9 +225,26 @@ class ConfigInformation:
|
|
|
198
225
|
if self._sealed and not bypass:
|
|
199
226
|
raise AttributeError(f"Object is read-only (trying to set {k})")
|
|
200
227
|
|
|
228
|
+
if not isinstance(v, ConfigMixin) and isinstance(v, Config):
|
|
229
|
+
raise AttributeError(
|
|
230
|
+
"Configuration (and not objects) should be used. Consider using .C(...)"
|
|
231
|
+
)
|
|
232
|
+
|
|
201
233
|
try:
|
|
202
234
|
argument = self.xpmtype.arguments.get(k, None)
|
|
203
235
|
if argument:
|
|
236
|
+
if (
|
|
237
|
+
isinstance(v, ConfigMixin)
|
|
238
|
+
and v.__xpm__._generated_values
|
|
239
|
+
and v.__xpm__.task is None
|
|
240
|
+
and not argument.ignore_generated
|
|
241
|
+
):
|
|
242
|
+
raise AttributeError(
|
|
243
|
+
f"Cannot set {k} to a configuration with generated values. "
|
|
244
|
+
"Here is the list of paths to help you: "
|
|
245
|
+
f"""{', '.join(v.__xpm__.get_generated_paths([k]))}"""
|
|
246
|
+
)
|
|
247
|
+
|
|
204
248
|
if not bypass and (
|
|
205
249
|
(isinstance(argument.generator, Generator)) or argument.constant
|
|
206
250
|
):
|
|
@@ -326,12 +370,23 @@ class ConfigInformation:
|
|
|
326
370
|
Arguments:
|
|
327
371
|
- context: the generation context
|
|
328
372
|
"""
|
|
373
|
+
if generated_keys := [
|
|
374
|
+
k
|
|
375
|
+
for k, v in self.values.items()
|
|
376
|
+
if isinstance(v, Config)
|
|
377
|
+
and v.__xpm__.task is None
|
|
378
|
+
and v.__xpm__._generated_values
|
|
379
|
+
]:
|
|
380
|
+
raise AttributeError(
|
|
381
|
+
"Cannot seal a configuration with generated values:"
|
|
382
|
+
f"""{",".join(generated_keys)} in {context.currentpath}"""
|
|
383
|
+
)
|
|
329
384
|
|
|
330
385
|
class Sealer(ConfigWalk):
|
|
331
|
-
def preprocess(self, config:
|
|
386
|
+
def preprocess(self, config: ConfigMixin):
|
|
332
387
|
return not config.__xpm__._sealed, config
|
|
333
388
|
|
|
334
|
-
def postprocess(self, stub, config:
|
|
389
|
+
def postprocess(self, stub, config: ConfigMixin, values):
|
|
335
390
|
# Generate values
|
|
336
391
|
from experimaestro.generators import Generator
|
|
337
392
|
|
|
@@ -344,22 +399,42 @@ class ConfigInformation:
|
|
|
344
399
|
continue
|
|
345
400
|
value = argument.generator()
|
|
346
401
|
else:
|
|
402
|
+
# Generate a value
|
|
347
403
|
sig = inspect.signature(argument.generator)
|
|
348
404
|
if len(sig.parameters) == 0:
|
|
349
405
|
value = argument.generator()
|
|
350
406
|
elif len(sig.parameters) == 2:
|
|
407
|
+
# Only in that case do we need to flag this configuration
|
|
408
|
+
# as containing generated values
|
|
409
|
+
if not argument.ignore_generated:
|
|
410
|
+
config.__xpm__._generated_values.append(k)
|
|
411
|
+
else:
|
|
412
|
+
logging.warning("Ignoring %s", k)
|
|
351
413
|
value = argument.generator(self.context, config)
|
|
352
414
|
else:
|
|
353
415
|
assert (
|
|
354
416
|
False
|
|
355
417
|
), "generator has either two parameters (context and config), or none"
|
|
356
418
|
config.__xpm__.set(k, value, bypass=True)
|
|
419
|
+
else:
|
|
420
|
+
value = config.__xpm__.values.get(k)
|
|
357
421
|
except Exception:
|
|
358
422
|
logger.error(
|
|
359
423
|
"While setting %s of %s", argument.name, config.__xpmtype__
|
|
360
424
|
)
|
|
361
425
|
raise
|
|
362
426
|
|
|
427
|
+
# Propagate the generated value flag
|
|
428
|
+
if (
|
|
429
|
+
value is not None
|
|
430
|
+
and isinstance(value, ConfigMixin)
|
|
431
|
+
and value.__xpm__._generated_values
|
|
432
|
+
):
|
|
433
|
+
if not argument.ignore_generated:
|
|
434
|
+
config.__xpm__._generated_values.append(k)
|
|
435
|
+
else:
|
|
436
|
+
logging.warning("Ignoring %s", k)
|
|
437
|
+
|
|
363
438
|
config.__xpm__._sealed = True
|
|
364
439
|
|
|
365
440
|
Sealer(context, recurse_task=True)(self.pyobject)
|
|
@@ -657,13 +732,6 @@ class ConfigInformation:
|
|
|
657
732
|
|
|
658
733
|
print(file=sys.stderr) # noqa: T201
|
|
659
734
|
|
|
660
|
-
# Handle an output configuration # FIXME: remove
|
|
661
|
-
def mark_output(config: "Config"):
|
|
662
|
-
"""Sets a dependency on the job"""
|
|
663
|
-
assert not isinstance(config, Task), "Cannot set a dependency on a task"
|
|
664
|
-
config.__xpm__.task = self.pyobject
|
|
665
|
-
return config
|
|
666
|
-
|
|
667
735
|
# Mark this configuration also
|
|
668
736
|
self.task = self.pyobject
|
|
669
737
|
|
|
@@ -677,6 +745,9 @@ class ConfigInformation:
|
|
|
677
745
|
def mark_output(self, config: "Config"):
|
|
678
746
|
"""Sets a dependency on the job"""
|
|
679
747
|
assert not isinstance(config, Task), "Cannot set a dependency on a task"
|
|
748
|
+
assert isinstance(
|
|
749
|
+
config, ConfigMixin
|
|
750
|
+
), "Only configurations can be marked as dependent on a task"
|
|
680
751
|
config.__xpm__.task = self.pyobject
|
|
681
752
|
return config
|
|
682
753
|
|
|
@@ -762,7 +833,7 @@ class ConfigInformation:
|
|
|
762
833
|
state_dict = {
|
|
763
834
|
"id": id(self.pyobject),
|
|
764
835
|
"module": self.xpmtype._module,
|
|
765
|
-
"type": self.xpmtype.
|
|
836
|
+
"type": self.xpmtype.value_type.__qualname__,
|
|
766
837
|
"typename": self.xpmtype.name(),
|
|
767
838
|
"identifier": self.identifier.state_dict(),
|
|
768
839
|
}
|
|
@@ -1022,7 +1093,7 @@ class ConfigInformation:
|
|
|
1022
1093
|
|
|
1023
1094
|
# Creates an object (or a config)
|
|
1024
1095
|
if as_instance:
|
|
1025
|
-
o = cls.
|
|
1096
|
+
o = cls.__new__(cls)
|
|
1026
1097
|
else:
|
|
1027
1098
|
o = cls.XPMConfig.__new__(cls.XPMConfig)
|
|
1028
1099
|
assert definition["id"] not in objects, "Duplicate id %s" % definition["id"]
|
|
@@ -1183,7 +1254,7 @@ class ConfigInformation:
|
|
|
1183
1254
|
|
|
1184
1255
|
if o is None:
|
|
1185
1256
|
# Creates an object (and not a config)
|
|
1186
|
-
o = config.
|
|
1257
|
+
o = config.__xpmtype__.value_type()
|
|
1187
1258
|
|
|
1188
1259
|
# Store in cache
|
|
1189
1260
|
self.objects.add_stub(id(config), o)
|
|
@@ -1242,6 +1313,9 @@ def clone(v):
|
|
|
1242
1313
|
if isinstance(v, Enum):
|
|
1243
1314
|
return v
|
|
1244
1315
|
|
|
1316
|
+
if isinstance(v, tuple):
|
|
1317
|
+
return tuple(clone(x) for x in v)
|
|
1318
|
+
|
|
1245
1319
|
if isinstance(v, Config):
|
|
1246
1320
|
# Create a new instance
|
|
1247
1321
|
kwargs = {
|
|
@@ -1260,6 +1334,11 @@ class ConfigMixin:
|
|
|
1260
1334
|
"""Class for configuration objects"""
|
|
1261
1335
|
|
|
1262
1336
|
__xpmtype__: ObjectType
|
|
1337
|
+
"""The associated XPM type"""
|
|
1338
|
+
|
|
1339
|
+
__xpm__: ConfigInformation
|
|
1340
|
+
"""The __xpm__ object contains all instance specific information about a
|
|
1341
|
+
configuration/task"""
|
|
1263
1342
|
|
|
1264
1343
|
def __init__(self, **kwargs):
|
|
1265
1344
|
"""Initialize the configuration with the given parameters"""
|
|
@@ -1310,8 +1389,8 @@ class ConfigMixin:
|
|
|
1310
1389
|
[f"{key}={value}" for key, value in self.__xpm__.values.items()]
|
|
1311
1390
|
)
|
|
1312
1391
|
return (
|
|
1313
|
-
f"{self.__xpmtype__.
|
|
1314
|
-
f"{self.__xpmtype__.
|
|
1392
|
+
f"{self.__xpmtype__.value_type.__module__}."
|
|
1393
|
+
f"{self.__xpmtype__.value_type.__qualname__}({params})"
|
|
1315
1394
|
)
|
|
1316
1395
|
|
|
1317
1396
|
def tag(self, name, value):
|
|
@@ -1340,9 +1419,20 @@ class ConfigMixin:
|
|
|
1340
1419
|
return self
|
|
1341
1420
|
|
|
1342
1421
|
def instance(
|
|
1343
|
-
self,
|
|
1422
|
+
self,
|
|
1423
|
+
context: ConfigWalkContext = None,
|
|
1424
|
+
*,
|
|
1425
|
+
objects: ObjectStore = None,
|
|
1426
|
+
keep: bool = True,
|
|
1344
1427
|
) -> T:
|
|
1345
|
-
"""Return an instance with the current values
|
|
1428
|
+
"""Return an instance with the current values
|
|
1429
|
+
|
|
1430
|
+
:param context: The context when computing the instance
|
|
1431
|
+
:param objects: The previously built objects (so that we avoid
|
|
1432
|
+
re-creating instances of past configurations)
|
|
1433
|
+
:param keep: register a configuration in the __config__ field of the
|
|
1434
|
+
instance
|
|
1435
|
+
"""
|
|
1346
1436
|
if context is None:
|
|
1347
1437
|
from experimaestro.xpmutils import EmptyContext
|
|
1348
1438
|
|
|
@@ -1351,7 +1441,11 @@ class ConfigMixin:
|
|
|
1351
1441
|
assert isinstance(
|
|
1352
1442
|
context, ConfigWalkContext
|
|
1353
1443
|
), f"{context.__class__} is not an instance of ConfigWalkContext"
|
|
1354
|
-
|
|
1444
|
+
|
|
1445
|
+
instance = self.__xpm__.fromConfig(context, objects=objects) # type: ignore
|
|
1446
|
+
if keep:
|
|
1447
|
+
object.__setattr__(instance, "__config__", self)
|
|
1448
|
+
return instance
|
|
1355
1449
|
|
|
1356
1450
|
def submit(
|
|
1357
1451
|
self,
|
|
@@ -1397,6 +1491,9 @@ class ConfigMixin:
|
|
|
1397
1491
|
return clone(self)
|
|
1398
1492
|
|
|
1399
1493
|
def add_pretasks(self, *tasks: "LightweightTask"):
|
|
1494
|
+
assert all(
|
|
1495
|
+
[isinstance(task, ConfigMixin) for task in tasks]
|
|
1496
|
+
), "One of the parameters is not a configuration object"
|
|
1400
1497
|
assert all(
|
|
1401
1498
|
[isinstance(task, LightweightTask) for task in tasks]
|
|
1402
1499
|
), "One of the pre-tasks are not lightweight tasks"
|
|
@@ -1441,52 +1538,17 @@ class Config:
|
|
|
1441
1538
|
"""The object type holds all the information about a specific subclass
|
|
1442
1539
|
experimaestro metadata"""
|
|
1443
1540
|
|
|
1444
|
-
__xpm__: ConfigInformation
|
|
1445
|
-
"""The __xpm__ object contains all instance specific information about a
|
|
1446
|
-
configuration/task"""
|
|
1447
|
-
|
|
1448
1541
|
@classproperty
|
|
1449
1542
|
def XPMConfig(cls):
|
|
1450
1543
|
if issubclass(cls, ConfigMixin):
|
|
1451
1544
|
return cls
|
|
1452
|
-
return cls.__getxpmtype__().
|
|
1453
|
-
|
|
1454
|
-
@classproperty
|
|
1455
|
-
def XPMValue(cls):
|
|
1456
|
-
"""Returns the value object for this configuration"""
|
|
1457
|
-
if issubclass(cls, ConfigMixin):
|
|
1458
|
-
return cls.__xpmtype__.objecttype
|
|
1459
|
-
|
|
1460
|
-
if value_cls := cls.__dict__.get("__XPMValue__", None):
|
|
1461
|
-
pass
|
|
1462
|
-
else:
|
|
1463
|
-
from ..types import XPMValue
|
|
1464
|
-
|
|
1465
|
-
__objectbases__ = tuple(
|
|
1466
|
-
s.XPMValue
|
|
1467
|
-
for s in cls.__bases__
|
|
1468
|
-
if issubclass(s, Config) and (s is not Config)
|
|
1469
|
-
) or (XPMValue,)
|
|
1470
|
-
|
|
1471
|
-
*tp_qual, tp_name = cls.__qualname__.split(".")
|
|
1472
|
-
value_cls = type(f"{tp_name}.XPMValue", (cls,) + __objectbases__, {})
|
|
1473
|
-
value_cls.__qualname__ = ".".join(tp_qual + [value_cls.__name__])
|
|
1474
|
-
value_cls.__module__ = cls.__module__
|
|
1475
|
-
|
|
1476
|
-
setattr(cls, "__XPMValue__", value_cls)
|
|
1477
|
-
|
|
1478
|
-
return value_cls
|
|
1545
|
+
return cls.__getxpmtype__().config_type
|
|
1479
1546
|
|
|
1480
1547
|
@classproperty
|
|
1481
1548
|
def C(cls):
|
|
1482
1549
|
"""Alias for XPMConfig"""
|
|
1483
1550
|
return cls.XPMConfig
|
|
1484
1551
|
|
|
1485
|
-
@classproperty
|
|
1486
|
-
def V(cls):
|
|
1487
|
-
"""Alias for XPMValue"""
|
|
1488
|
-
return cls.XPMValue
|
|
1489
|
-
|
|
1490
1552
|
@classmethod
|
|
1491
1553
|
def __getxpmtype__(cls) -> "ObjectType":
|
|
1492
1554
|
"""Get (and create if necessary) the Object type associated
|
|
@@ -1503,46 +1565,6 @@ class Config:
|
|
|
1503
1565
|
raise
|
|
1504
1566
|
return xpmtype
|
|
1505
1567
|
|
|
1506
|
-
def __new__(cls: Type[T], *args, **kwargs) -> T:
|
|
1507
|
-
"""Returns an instance of a ConfigMixin (for compatibility, use XPMConfig
|
|
1508
|
-
or C if possible)
|
|
1509
|
-
|
|
1510
|
-
:deprecated: Use Config.C or Config.XPMConfig to construct a new
|
|
1511
|
-
configuration, and Config.V (or Config.XPMValue) for a new value
|
|
1512
|
-
"""
|
|
1513
|
-
# If this is an XPMValue, just return a new instance
|
|
1514
|
-
from experimaestro.core.types import XPMValue
|
|
1515
|
-
|
|
1516
|
-
if issubclass(cls, XPMValue):
|
|
1517
|
-
return object.__new__(cls)
|
|
1518
|
-
|
|
1519
|
-
# If this is the XPMConfig, just return a new instance
|
|
1520
|
-
# __init__ will be called
|
|
1521
|
-
if issubclass(cls, ConfigMixin):
|
|
1522
|
-
return object.__new__(cls)
|
|
1523
|
-
|
|
1524
|
-
# Log a deprecation warning for this way of creating a configuration
|
|
1525
|
-
caller = inspect.getframeinfo(inspect.stack()[1][0])
|
|
1526
|
-
logger.warning(
|
|
1527
|
-
"Creating a configuration using Config.__new__ is deprecated, and will be removed in a future version. "
|
|
1528
|
-
"Use Config.C or Config.XPMConfig to create a new configuration. "
|
|
1529
|
-
"Issue created at %s:%s",
|
|
1530
|
-
str(Path(caller.filename).absolute()),
|
|
1531
|
-
caller.lineno,
|
|
1532
|
-
)
|
|
1533
|
-
|
|
1534
|
-
# otherwise, we use the configuration type
|
|
1535
|
-
o: ConfigMixin = object.__new__(cls.__getxpmtype__().configtype)
|
|
1536
|
-
try:
|
|
1537
|
-
o.__init__(*args, **kwargs)
|
|
1538
|
-
except Exception:
|
|
1539
|
-
logger.error(
|
|
1540
|
-
"Init error in %s:%s"
|
|
1541
|
-
% (str(Path(caller.filename).absolute()), caller.lineno)
|
|
1542
|
-
)
|
|
1543
|
-
raise
|
|
1544
|
-
return o
|
|
1545
|
-
|
|
1546
1568
|
def __validate__(self):
|
|
1547
1569
|
"""Validate the values"""
|
|
1548
1570
|
pass
|
|
@@ -71,6 +71,7 @@ class ConfigWalk:
|
|
|
71
71
|
return self.context.push(str(i))
|
|
72
72
|
|
|
73
73
|
def map(self, k: str):
|
|
74
|
+
"""Provides a path context when processing a tree"""
|
|
74
75
|
return self.context.push(k)
|
|
75
76
|
|
|
76
77
|
def stub(self, config):
|
|
@@ -123,7 +124,8 @@ class ConfigWalk:
|
|
|
123
124
|
and self.recurse_task
|
|
124
125
|
and x.__xpm__.task is not x
|
|
125
126
|
):
|
|
126
|
-
self(
|
|
127
|
+
with self.map("__task__"):
|
|
128
|
+
self(x.__xpm__.task)
|
|
127
129
|
|
|
128
130
|
processed = self.postprocess(stub, x, result)
|
|
129
131
|
self.visited[xid] = processed
|
experimaestro/core/types.py
CHANGED
|
@@ -203,18 +203,14 @@ class ObjectType(Type):
|
|
|
203
203
|
"""ObjectType contains class-level information about
|
|
204
204
|
experimaestro configurations and tasks
|
|
205
205
|
|
|
206
|
-
:param
|
|
207
|
-
:param
|
|
208
|
-
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
|
|
209
208
|
"""
|
|
210
209
|
|
|
211
|
-
# Those entries should not be copied in the __dict__
|
|
212
|
-
FORBIDDEN_KEYS = set(("__dict__", "__weakref__"))
|
|
213
|
-
|
|
214
210
|
def __init__(
|
|
215
211
|
self,
|
|
216
212
|
tp: type,
|
|
217
|
-
identifier: Union[str, Identifier] = None,
|
|
213
|
+
identifier: Union[str, Identifier, None] = None,
|
|
218
214
|
):
|
|
219
215
|
"""Creates a type"""
|
|
220
216
|
from .objects import Config, ConfigMixin
|
|
@@ -225,7 +221,7 @@ class ObjectType(Type):
|
|
|
225
221
|
self._title = None
|
|
226
222
|
self.submit_hooks = set()
|
|
227
223
|
|
|
228
|
-
# Get the identifier
|
|
224
|
+
# --- Get the identifier
|
|
229
225
|
if identifier is None and hasattr(tp, "__xpmid__"):
|
|
230
226
|
__xpmid__ = getattr(tp, "__xpmid__")
|
|
231
227
|
if isinstance(__xpmid__, Identifier):
|
|
@@ -250,59 +246,41 @@ class ObjectType(Type):
|
|
|
250
246
|
# --- Creates the config type and not config type
|
|
251
247
|
|
|
252
248
|
self.originaltype = tp
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
__bases__ = () if tp.__bases__ == (object,) else tp.__bases__
|
|
256
|
-
__dict__ = dict(tp.__dict__)
|
|
257
|
-
|
|
258
|
-
__dict__ = {
|
|
259
|
-
key: value
|
|
260
|
-
for key, value in tp.__dict__.items()
|
|
261
|
-
if key not in ObjectType.FORBIDDEN_KEYS
|
|
262
|
-
}
|
|
263
|
-
self.basetype = type(tp.__name__, (Config,) + __bases__, __dict__)
|
|
264
|
-
self.basetype.__module__ = tp.__module__
|
|
265
|
-
self.basetype.__qualname__ = tp.__qualname__
|
|
266
|
-
else:
|
|
267
|
-
self.basetype = tp
|
|
249
|
+
assert issubclass(tp, Config)
|
|
250
|
+
self.value_type = tp
|
|
268
251
|
|
|
269
252
|
# --- Create the type-specific configuration class (XPMConfig)
|
|
270
253
|
__configbases__ = tuple(
|
|
271
|
-
s.__getxpmtype__().
|
|
254
|
+
s.__getxpmtype__().config_type
|
|
272
255
|
for s in tp.__bases__
|
|
273
256
|
if issubclass(s, Config) and (s is not Config)
|
|
274
257
|
) or (ConfigMixin,)
|
|
275
258
|
|
|
276
|
-
*tp_qual, tp_name = self.
|
|
277
|
-
self.
|
|
278
|
-
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,), {}
|
|
279
262
|
)
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
263
|
+
self.config_type.__qualname__ = ".".join(tp_qual + [self.config_type.__name__])
|
|
264
|
+
self.config_type.__module__ = tp.__module__
|
|
282
265
|
|
|
283
|
-
#
|
|
284
|
-
if hasattr(self.
|
|
266
|
+
# --- Get the return type
|
|
267
|
+
if hasattr(self.value_type, "task_outputs") or False:
|
|
285
268
|
self.returntype = get_type_hints(
|
|
286
|
-
getattr(self.
|
|
269
|
+
getattr(self.value_type, "task_outputs")
|
|
287
270
|
).get("return", typing.Any)
|
|
288
271
|
else:
|
|
289
|
-
self.returntype = self.
|
|
272
|
+
self.returntype = self.value_type
|
|
290
273
|
|
|
291
|
-
# Registers ourselves
|
|
292
|
-
self.
|
|
293
|
-
self.
|
|
274
|
+
# --- Registers ourselves
|
|
275
|
+
self.value_type.__xpmtype__ = self
|
|
276
|
+
self.config_type.__xpmtype__ = self
|
|
294
277
|
|
|
295
|
-
# Other initializations
|
|
278
|
+
# --- Other initializations
|
|
296
279
|
self.__initialized__ = False
|
|
297
280
|
self._runtype = None
|
|
298
281
|
self.annotations = []
|
|
299
282
|
self._deprecated = False
|
|
300
283
|
|
|
301
|
-
@property
|
|
302
|
-
def objecttype(self):
|
|
303
|
-
"""Returns the object type"""
|
|
304
|
-
return self.basetype.XPMValue
|
|
305
|
-
|
|
306
284
|
def addAnnotation(self, annotation):
|
|
307
285
|
assert not self.__initialized__
|
|
308
286
|
self.annotations.append(annotation)
|
|
@@ -357,15 +335,15 @@ class ObjectType(Type):
|
|
|
357
335
|
# Add task
|
|
358
336
|
if self.taskcommandfactory is not None:
|
|
359
337
|
self.task = self.taskcommandfactory(self)
|
|
360
|
-
elif issubclass(self.
|
|
338
|
+
elif issubclass(self.value_type, Task):
|
|
361
339
|
self.task = self.getpythontaskcommand()
|
|
362
340
|
|
|
363
341
|
# Add arguments from type hints
|
|
364
342
|
from .arguments import TypeAnnotation
|
|
365
343
|
|
|
366
|
-
if hasattr(self.
|
|
367
|
-
typekeys = set(self.
|
|
368
|
-
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)
|
|
369
347
|
for key, typehint in hints.items():
|
|
370
348
|
# Filter out hints from parent classes
|
|
371
349
|
if key in typekeys:
|
|
@@ -378,19 +356,19 @@ class ObjectType(Type):
|
|
|
378
356
|
try:
|
|
379
357
|
self.addArgument(
|
|
380
358
|
options.create(
|
|
381
|
-
key, self.
|
|
359
|
+
key, self.value_type, typehint.__args__[0]
|
|
382
360
|
)
|
|
383
361
|
)
|
|
384
362
|
except Exception:
|
|
385
363
|
logger.error(
|
|
386
364
|
"while adding argument %s of %s",
|
|
387
365
|
key,
|
|
388
|
-
self.
|
|
366
|
+
self.value_type,
|
|
389
367
|
)
|
|
390
368
|
raise
|
|
391
369
|
|
|
392
370
|
def name(self):
|
|
393
|
-
return f"{self.
|
|
371
|
+
return f"{self.value_type.__module__}.{self.value_type.__qualname__}"
|
|
394
372
|
|
|
395
373
|
def __parsedoc__(self):
|
|
396
374
|
"""Parse the documentation"""
|
|
@@ -400,7 +378,7 @@ class ObjectType(Type):
|
|
|
400
378
|
self.__initialize__()
|
|
401
379
|
|
|
402
380
|
# Get description from documentation
|
|
403
|
-
__doc__ = self.
|
|
381
|
+
__doc__ = self.value_type.__dict__.get("__doc__", None)
|
|
404
382
|
if __doc__:
|
|
405
383
|
parseddoc = parse(__doc__)
|
|
406
384
|
self._title = parseddoc.short_description
|
|
@@ -430,7 +408,7 @@ class ObjectType(Type):
|
|
|
430
408
|
argname = None
|
|
431
409
|
|
|
432
410
|
def deprecate(self):
|
|
433
|
-
if len(self.
|
|
411
|
+
if len(self.value_type.__bases__) != 1:
|
|
434
412
|
raise RuntimeError(
|
|
435
413
|
"Deprecated configurations must have "
|
|
436
414
|
"only one parent (the new configuration)"
|
|
@@ -439,7 +417,7 @@ class ObjectType(Type):
|
|
|
439
417
|
|
|
440
418
|
# Uses the parent identifier (and saves the deprecated one for path updates)
|
|
441
419
|
self._deprecated_identifier = self.identifier
|
|
442
|
-
parent = self.
|
|
420
|
+
parent = self.value_type.__bases__[0].__getxpmtype__()
|
|
443
421
|
self.identifier = parent.identifier
|
|
444
422
|
self._deprecated = True
|
|
445
423
|
|
|
@@ -454,7 +432,7 @@ class ObjectType(Type):
|
|
|
454
432
|
return self._description
|
|
455
433
|
|
|
456
434
|
@property
|
|
457
|
-
def title(self) ->
|
|
435
|
+
def title(self) -> str:
|
|
458
436
|
self.__parsedoc__()
|
|
459
437
|
return self._title or str(self.identifier)
|
|
460
438
|
|
|
@@ -469,7 +447,7 @@ class ObjectType(Type):
|
|
|
469
447
|
|
|
470
448
|
# The the attribute for the config type
|
|
471
449
|
setattr(
|
|
472
|
-
self.
|
|
450
|
+
self.config_type,
|
|
473
451
|
argument.name,
|
|
474
452
|
property(
|
|
475
453
|
lambda _self: _self.__xpm__.get(argument.name),
|
|
@@ -488,7 +466,7 @@ class ObjectType(Type):
|
|
|
488
466
|
def parents(self) -> Iterator["ObjectType"]:
|
|
489
467
|
from .objects import Config, Task
|
|
490
468
|
|
|
491
|
-
for tp in self.
|
|
469
|
+
for tp in self.value_type.__bases__:
|
|
492
470
|
if issubclass(tp, Config) and tp not in [Config, Task]:
|
|
493
471
|
yield tp.__xpmtype__
|
|
494
472
|
|
|
@@ -504,7 +482,7 @@ class ObjectType(Type):
|
|
|
504
482
|
if not isinstance(value, Config):
|
|
505
483
|
raise ValueError(f"{value} is not an experimaestro type or task")
|
|
506
484
|
|
|
507
|
-
types = self.
|
|
485
|
+
types = self.value_type
|
|
508
486
|
|
|
509
487
|
if not isinstance(value, types):
|
|
510
488
|
raise ValueError(
|
|
@@ -519,7 +497,7 @@ class ObjectType(Type):
|
|
|
519
497
|
|
|
520
498
|
def fullyqualifiedname(self) -> str:
|
|
521
499
|
"""Returns the fully qualified (Python) name"""
|
|
522
|
-
return f"{self.
|
|
500
|
+
return f"{self.value_type.__module__}.{self.value_type.__qualname__}"
|
|
523
501
|
|
|
524
502
|
|
|
525
503
|
class TypeProxy:
|
|
@@ -6,7 +6,7 @@ from typing import ClassVar, Dict, Optional, Set, Type, Union
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import typing
|
|
8
8
|
from omegaconf import DictConfig, OmegaConf, SCMode
|
|
9
|
-
import
|
|
9
|
+
from importlib.metadata import entry_points
|
|
10
10
|
from experimaestro.utils import logger
|
|
11
11
|
from .base import ConnectorConfiguration, TokenConfiguration
|
|
12
12
|
from .specs import HostRequirement, RequirementUnion
|
|
@@ -75,10 +75,10 @@ class LauncherRegistry:
|
|
|
75
75
|
self.find_launcher_fn = None
|
|
76
76
|
|
|
77
77
|
# Use entry points for connectors and launchers
|
|
78
|
-
for entry_point in
|
|
78
|
+
for entry_point in entry_points(group="experimaestro.connectors"):
|
|
79
79
|
entry_point.load().init_registry(self)
|
|
80
80
|
|
|
81
|
-
for entry_point in
|
|
81
|
+
for entry_point in entry_points(group="experimaestro.tokens"):
|
|
82
82
|
entry_point.load().init_registry(self)
|
|
83
83
|
|
|
84
84
|
# Register the find launcher function if it exists
|