experimaestro 1.8.0rc6__py3-none-any.whl → 1.8.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of experimaestro might be problematic. Click here for more details.

Files changed (27) hide show
  1. experimaestro/core/identifier.py +296 -0
  2. experimaestro/core/objects/__init__.py +44 -0
  3. experimaestro/core/{objects.py → objects/config.py} +157 -549
  4. experimaestro/core/objects/config_utils.py +58 -0
  5. experimaestro/core/objects/config_walk.py +150 -0
  6. experimaestro/core/objects.pyi +6 -38
  7. experimaestro/core/types.py +28 -4
  8. experimaestro/notifications.py +1 -0
  9. experimaestro/scheduler/base.py +1 -0
  10. experimaestro/tests/core/__init__.py +0 -0
  11. experimaestro/tests/core/test_generics.py +206 -0
  12. experimaestro/tests/restart.py +3 -1
  13. experimaestro/tests/test_instance.py +3 -3
  14. experimaestro/tests/test_objects.py +20 -4
  15. experimaestro/tests/test_serializers.py +3 -3
  16. experimaestro/tests/test_types.py +2 -2
  17. {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/METADATA +5 -5
  18. {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/RECORD +21 -21
  19. experimaestro/server/data/016b4a6cdced82ab3aa1.ttf +0 -0
  20. experimaestro/server/data/50701fbb8177c2dde530.ttf +0 -0
  21. experimaestro/server/data/878f31251d960bd6266f.woff2 +0 -0
  22. experimaestro/server/data/b041b1fa4fe241b23445.woff2 +0 -0
  23. experimaestro/server/data/b6879d41b0852f01ed5b.woff2 +0 -0
  24. experimaestro/server/data/d75e3fd1eb12e9bd6655.ttf +0 -0
  25. {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/LICENSE +0 -0
  26. {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/WHEEL +0 -0
  27. {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/entry_points.txt +0 -0
@@ -1,25 +1,17 @@
1
1
  """Configuration and tasks"""
2
2
 
3
- from functools import cached_property
4
3
  import json
5
4
 
6
5
  from attr import define
6
+ import fasteners
7
7
 
8
8
  from experimaestro import taskglobals
9
9
 
10
- try:
11
- from types import NoneType
12
- except Exception:
13
- # compatibility: python-3.8
14
- NoneType = type(None)
15
10
  from termcolor import cprint
16
- import os
17
11
  from pathlib import Path
18
12
  import hashlib
19
13
  import logging
20
- import struct
21
14
  import io
22
- import fasteners
23
15
  from enum import Enum
24
16
  import inspect
25
17
  import importlib
@@ -42,306 +34,33 @@ from typing import (
42
34
  import sys
43
35
  import experimaestro
44
36
  from experimaestro.utils import logger
45
- from contextlib import contextmanager
46
- from experimaestro.core.types import DeprecatedAttribute, ObjectType
47
- from .context import SerializationContext, SerializedPath, SerializedPathLoader
37
+ from experimaestro.core.types import DeprecatedAttribute, ObjectType, TypeVarType
38
+ from ..context import SerializationContext, SerializedPath, SerializedPathLoader
48
39
 
49
40
  if TYPE_CHECKING:
50
- from .callbacks import TaskEventListener
41
+ from ..callbacks import TaskEventListener
42
+ from ..identifier import Identifier
51
43
  from experimaestro.scheduler.base import Job
52
44
  from experimaestro.scheduler.workspace import RunMode
53
45
  from experimaestro.launchers import Launcher
54
46
  from experimaestro.scheduler import Workspace
55
47
 
56
- T = TypeVar("T", bound="Config")
57
-
58
-
59
- class Identifier:
60
- def __init__(self, main: bytes):
61
- self.main = main
62
- self.has_loops = False
63
-
64
- @cached_property
65
- def all(self):
66
- """Returns the overall identifier"""
67
- return self.main
68
-
69
- def __hash__(self) -> int:
70
- return hash(self.main)
71
-
72
- def state_dict(self):
73
- return self.main.hex()
74
-
75
- def __eq__(self, other: "Identifier"):
76
- return self.main == other.main
77
-
78
- @staticmethod
79
- def from_state_dict(data: Union[Dict[str, str], str]):
80
- if isinstance(data, str):
81
- return Identifier(bytes.fromhex(data))
82
-
83
- return Identifier(bytes.fromhex(data["main"]))
84
-
85
- def __repr__(self):
86
- return self.main.hex()
87
-
88
-
89
- def is_ignored(value):
90
- """Returns True if the value should be ignored by itself"""
91
- return value is not None and isinstance(value, Config) and (value.__xpm__.meta)
92
-
93
-
94
- def remove_meta(value):
95
- """Cleanup a dict/list by removing ignored values"""
96
- if isinstance(value, list):
97
- return [el for el in value if not is_ignored(el)]
98
- if isinstance(value, dict):
99
- return {key: value for key, value in value.items() if not is_ignored(value)}
100
- return value
101
-
102
-
103
- class ConfigPath:
104
- """Used to keep track of cycles when computing a hash"""
105
-
106
- def __init__(self):
107
- self.loops: List[bool] = []
108
- """Indicates whether a loop was detected up to this node"""
109
-
110
- self.config2index = {}
111
- """Associate an index in the list with a configuration"""
112
-
113
- def detect_loop(self, config) -> Optional[int]:
114
- """If there is a loop, return the relative index and update the path"""
115
- index = self.config2index.get(id(config), None)
116
- if index is not None:
117
- for i in range(index, self.depth):
118
- self.loops[i] = True
119
- return self.depth - index
120
-
121
- def has_loop(self):
122
- return self.loops[-1]
123
-
124
- @property
125
- def depth(self):
126
- return len(self.loops)
127
-
128
- @contextmanager
129
- def push(self, config):
130
- config_id = id(config)
131
- assert config_id not in self.config2index
132
-
133
- self.config2index[config_id] = self.depth
134
- self.loops.append(False)
135
-
136
- try:
137
- yield
138
- finally:
139
- self.loops.pop()
140
- del self.config2index[config_id]
48
+ from .config_walk import ConfigWalk, ConfigWalkContext
49
+ from .config_utils import (
50
+ getqualattr,
51
+ add_to_path,
52
+ SealedError,
53
+ TaggedValue,
54
+ ObjectStore,
55
+ classproperty,
56
+ )
141
57
 
58
+ T = TypeVar("T", bound="Config")
142
59
 
143
- hash_logger = logging.getLogger("xpm.hash")
144
60
 
145
61
  DependentMarker = Callable[["Config"], None]
146
62
 
147
63
 
148
- class HashComputer:
149
- """This class is in charge of computing a config/task identifier"""
150
-
151
- OBJECT_ID = b"\x00"
152
- INT_ID = b"\x01"
153
- FLOAT_ID = b"\x02"
154
- STR_ID = b"\x03"
155
- PATH_ID = b"\x04"
156
- NAME_ID = b"\x05"
157
- NONE_ID = b"\x06"
158
- LIST_ID = b"\x07"
159
- TASK_ID = b"\x08"
160
- DICT_ID = b"\x09"
161
- ENUM_ID = b"\x0a"
162
- CYCLE_REFERENCE = b"\x0b"
163
- INIT_TASKS = b"\x0c"
164
-
165
- def __init__(self, config: "Config", config_path: ConfigPath, *, version=None):
166
- # Hasher for parameters
167
- self._hasher = hashlib.sha256()
168
- self.config = config
169
- self.config_path = config_path
170
- self.version = version or int(os.environ.get("XPM_HASH_COMPUTER", 2))
171
- if hash_logger.isEnabledFor(logging.DEBUG):
172
- hash_logger.debug(
173
- "starting hash (%s): %s", hash(str(self.config)), self.config
174
- )
175
-
176
- def identifier(self) -> Identifier:
177
- main = self._hasher.digest()
178
- if hash_logger.isEnabledFor(logging.DEBUG):
179
- hash_logger.debug("hash (%s): %s", hash(str(self.config)), str(main))
180
- return Identifier(main)
181
-
182
- def _hashupdate(self, bytes: bytes):
183
- """Update the hash computers with some bytes"""
184
- if hash_logger.isEnabledFor(logging.DEBUG):
185
- hash_logger.debug(
186
- "updating hash (%s): %s", hash(str(self.config)), str(bytes)
187
- )
188
- self._hasher.update(bytes)
189
-
190
- def update(self, value, *, myself=False): # noqa: C901
191
- """Update the hash
192
-
193
- :param value: The value to add to the hash
194
- :param myself: True if the value is the configuration for which we wish
195
- to compute the identifier, defaults to False
196
- :raises NotImplementedError: If the value cannot be processed
197
- """
198
- if value is None:
199
- self._hashupdate(HashComputer.NONE_ID)
200
- elif isinstance(value, float):
201
- self._hashupdate(HashComputer.FLOAT_ID)
202
- self._hashupdate(struct.pack("!d", value))
203
- elif isinstance(value, int):
204
- self._hashupdate(HashComputer.INT_ID)
205
- self._hashupdate(struct.pack("!q", value))
206
- elif isinstance(value, str):
207
- self._hashupdate(HashComputer.STR_ID)
208
- self._hashupdate(value.encode("utf-8"))
209
- elif isinstance(value, list):
210
- values = [el for el in value if not is_ignored(el)]
211
- self._hashupdate(HashComputer.LIST_ID)
212
- self._hashupdate(struct.pack("!d", len(values)))
213
- for x in values:
214
- self.update(x)
215
- elif isinstance(value, Enum):
216
- self._hashupdate(HashComputer.ENUM_ID)
217
- k = value.__class__
218
- self._hashupdate(
219
- f"{k.__module__}.{k.__qualname__ }:{value.name}".encode("utf-8"),
220
- )
221
- elif isinstance(value, dict):
222
- self._hashupdate(HashComputer.DICT_ID)
223
- items = [
224
- (key, value) for key, value in value.items() if not is_ignored(value)
225
- ]
226
- items.sort(key=lambda x: x[0])
227
- for key, value in items:
228
- self.update(key)
229
- self.update(value)
230
-
231
- # Handles configurations
232
- elif isinstance(value, Config):
233
- # Encodes the identifier
234
- self._hashupdate(HashComputer.OBJECT_ID)
235
-
236
- # If we encode another config, then
237
- if not myself:
238
- if loop_ix := self.config_path.detect_loop(value):
239
- # Loop detected: use cycle reference
240
- self._hashupdate(HashComputer.CYCLE_REFERENCE)
241
- self._hashupdate(struct.pack("!q", loop_ix))
242
-
243
- else:
244
- # Just use the object identifier
245
- value_id = HashComputer.compute(
246
- value, version=self.version, config_path=self.config_path
247
- )
248
- self._hashupdate(value_id.all)
249
-
250
- # And that's it!
251
- return
252
-
253
- # Process tasks
254
- if value.__xpm__.task is not None and (value.__xpm__.task is not value):
255
- hash_logger.debug("Computing hash for task %s", value.__xpm__.task)
256
- self._hashupdate(HashComputer.TASK_ID)
257
- self.update(value.__xpm__.task)
258
-
259
- xpmtype = value.__xpmtype__
260
- self._hashupdate(xpmtype.identifier.name.encode("utf-8"))
261
-
262
- # Process arguments (sort by name to ensure uniqueness)
263
- arguments = sorted(xpmtype.arguments.values(), key=lambda a: a.name)
264
- for argument in arguments:
265
- # Ignored argument
266
- if argument.ignored:
267
- argvalue = value.__xpm__.values.get(argument.name, None)
268
-
269
- # ... unless meta is set to false
270
- if (
271
- argvalue is None
272
- or not isinstance(argvalue, Config)
273
- or (argvalue.__xpm__.meta is not False)
274
- ):
275
- continue
276
-
277
- if argument.generator:
278
- continue
279
-
280
- # Argument value
281
- # Skip if the argument is not a constant, and
282
- # - optional argument: both value and default are None
283
- # - the argument value is equal to the default value
284
- argvalue = getattr(value, argument.name, None)
285
- if not argument.constant and (
286
- (
287
- not argument.required
288
- and argument.default is None
289
- and argvalue is None
290
- )
291
- or (
292
- argument.default is not None
293
- and argument.default == remove_meta(argvalue)
294
- )
295
- ):
296
- # No update if same value (and not constant)
297
- continue
298
-
299
- if (
300
- argvalue is not None
301
- and isinstance(argvalue, Config)
302
- and argvalue.__xpm__.meta
303
- ):
304
- continue
305
-
306
- # Hash name
307
- self.update(argument.name)
308
-
309
- # Hash value
310
- self._hashupdate(HashComputer.NAME_ID)
311
- self.update(argvalue)
312
-
313
- else:
314
- raise NotImplementedError("Cannot compute hash of type %s" % type(value))
315
-
316
- @staticmethod
317
- def compute(
318
- config: "Config", config_path: ConfigPath = None, version=None
319
- ) -> Identifier:
320
- """Compute the identifier for a configuration
321
-
322
- :param config: the configuration for which we compute the identifier
323
- :param config_path: used to track down cycles between configurations
324
- :param version: version for the hash computation (None for the last one)
325
- """
326
-
327
- # Try to use the cached value first
328
- # (if there are no loops)
329
- if config.__xpm__._sealed:
330
- identifier = config.__xpm__._raw_identifier
331
- if identifier is not None and not identifier.has_loops:
332
- return identifier
333
-
334
- config_path = config_path or ConfigPath()
335
-
336
- with config_path.push(config):
337
- self = HashComputer(config, config_path, version=version)
338
- self.update(config, myself=True)
339
- identifier = self.identifier()
340
- identifier.has_loop = config_path.has_loop()
341
-
342
- return identifier
343
-
344
-
345
64
  def updatedependencies(
346
65
  dependencies, value: "Config", path: List[str], taskids: Set[int]
347
66
  ):
@@ -370,199 +89,9 @@ def updatedependencies(
370
89
  raise NotImplementedError("update dependencies for type %s" % type(value))
371
90
 
372
91
 
373
- class SealedError(Exception):
374
- """Exception when trying to modify a sealed configuration"""
375
-
376
- pass
377
-
378
-
379
- class TaggedValue:
380
- def __init__(self, value):
381
- self.value = value
382
-
383
-
384
- @contextmanager
385
- def add_to_path(p):
386
- """Temporarily add a path to sys.path"""
387
- import sys
388
-
389
- old_path = sys.path
390
- sys.path = sys.path[:]
391
- sys.path.insert(0, p)
392
- try:
393
- yield
394
- finally:
395
- sys.path = old_path
396
-
397
-
398
- class ConfigWalkContext:
399
- """Context when generating values in configurations"""
400
-
401
- @property
402
- def path(self):
403
- """Returns the path of the job directory"""
404
- raise NotImplementedError()
405
-
406
- def __init__(self):
407
- self._configpath = None
408
-
409
- @property
410
- def task(self):
411
- return None
412
-
413
- def currentpath(self) -> Path:
414
- """Returns the configuration folder"""
415
- if self._configpath:
416
- return self.path / self._configpath
417
- return self.path
418
-
419
- @contextmanager
420
- def push(self, key: str):
421
- """Push a new key to contextualize paths"""
422
- p = self._configpath
423
- try:
424
- self._configpath = (Path("out") if p is None else p) / key
425
- yield key
426
- finally:
427
- self._configpath = p
428
-
429
-
430
92
  NOT_SET = object()
431
93
 
432
94
 
433
- class ConfigWalk:
434
- """Allows to perform an operation on all nested configurations"""
435
-
436
- def __init__(self, context: ConfigWalkContext = None, recurse_task=False):
437
- """
438
-
439
- :param recurse_task: Recurse into linked tasks
440
- :param context: The context, by default only tracks the position in the
441
- config tree
442
- """
443
- self.recurse_task = recurse_task
444
- self.context = ConfigWalkContext() if context is None else context
445
-
446
- # Stores already visited nodes
447
- self.visited = {}
448
-
449
- def preprocess(self, config: "Config") -> Tuple[bool, Any]:
450
- """Returns a tuple boolean/value
451
-
452
- The boolean value is used to stop the processing if False.
453
- The value is returned
454
- """
455
- return True, None
456
-
457
- def postprocess(self, stub, config: "Config", values: Dict[str, Any]):
458
- return stub
459
-
460
- def list(self, i: int):
461
- return self.context.push(str(i))
462
-
463
- def map(self, k: str):
464
- return self.context.push(k)
465
-
466
- def stub(self, config: "Config"):
467
- return config
468
-
469
- def __call__(self, x):
470
- if isinstance(x, Config):
471
- info = x.__xpm__ # type: ConfigInformation
472
-
473
- # Avoid loops
474
- xid = id(x)
475
- if xid in self.visited:
476
- return self.visited[xid]
477
-
478
- # Get a stub
479
- stub = self.stub(x)
480
- self.visited[xid] = stub
481
-
482
- # Pre-process
483
- flag, value = self.preprocess(x)
484
-
485
- if not flag:
486
- # Stop processing and returns value
487
- return value
488
-
489
- # Process all the arguments
490
- result = {}
491
- for arg, v in info.xpmvalues():
492
- if v is not None:
493
- with self.map(arg.name):
494
- result[arg.name] = self(v)
495
- else:
496
- result[arg.name] = None
497
-
498
- # Deals with pre-tasks
499
- if info.pre_tasks:
500
- with self.map("__pre_tasks__"):
501
- self(info.pre_tasks)
502
-
503
- if info.init_tasks:
504
- with self.map("__init_tasks__"):
505
- self(info.init_tasks)
506
-
507
- # Process task if different
508
- if (
509
- x.__xpm__.task is not None
510
- and self.recurse_task
511
- and x.__xpm__.task is not x
512
- ):
513
- self(x.__xpm__.task)
514
-
515
- processed = self.postprocess(stub, x, result)
516
- self.visited[xid] = processed
517
- return processed
518
-
519
- if isinstance(x, list):
520
- result = []
521
- for i, sv in enumerate(x):
522
- with self.list(i):
523
- result.append(self(sv))
524
- return result
525
-
526
- if isinstance(x, dict):
527
- result = {}
528
- for key, value in x.items():
529
- assert isinstance(key, (str, float, int))
530
- with self.map(key):
531
- result[key] = self(value)
532
- return result
533
-
534
- if isinstance(x, (float, int, str, Path, Enum)):
535
- return x
536
-
537
- raise NotImplementedError(f"Cannot handle a value of type {type(x)}")
538
-
539
-
540
- def getqualattr(module, qualname):
541
- """Get a qualified attributed value"""
542
- cls = module
543
- for part in qualname.split("."):
544
- cls = getattr(cls, part)
545
- return cls
546
-
547
-
548
- class ObjectStore:
549
- def __init__(self):
550
- self.store: Dict[int, Any] = {}
551
- self.constructed: Set[int] = set()
552
-
553
- def set_constructed(self, identifier: int):
554
- self.constructed.add(identifier)
555
-
556
- def is_constructed(self, identifier: int):
557
- return identifier in self.constructed
558
-
559
- def retrieve(self, identifier: int):
560
- return self.store.get(identifier, None)
561
-
562
- def add_stub(self, identifier: int, stub: Any):
563
- self.store[identifier] = stub
564
-
565
-
566
95
  @define()
567
96
  class WatchedOutput:
568
97
  #: The enclosing job
@@ -590,7 +119,7 @@ class ConfigInformation:
590
119
  # Set to true when loading from JSON
591
120
  LOADING: ClassVar[bool] = False
592
121
 
593
- def __init__(self, pyobject: "TypeConfig"):
122
+ def __init__(self, pyobject: "ConfigMixin"):
594
123
  # The underlying pyobject and XPM type
595
124
  self.pyobject = pyobject
596
125
  self.xpmtype = pyobject.__xpmtype__ # type: ObjectType
@@ -616,6 +145,10 @@ class ConfigInformation:
616
145
  # Explicitely added dependencies
617
146
  self.dependencies = []
618
147
 
148
+ # Concrete type variables resolutions
149
+ # This is used to check typevars coherence
150
+ self.concrete_typevars: Dict[TypeVar, type] = {}
151
+
619
152
  # Lightweight tasks
620
153
  self.pre_tasks: List["LightweightTask"] = []
621
154
 
@@ -655,6 +188,8 @@ class ConfigInformation:
655
188
  return object.__getattribute__(self.pyobject, name)
656
189
 
657
190
  def set(self, k, v, bypass=False):
191
+ from experimaestro.generators import Generator
192
+
658
193
  # Not an argument, bypass
659
194
  if k not in self.xpmtype.arguments:
660
195
  setattr(self.pyobject, k, v)
@@ -666,10 +201,19 @@ class ConfigInformation:
666
201
  try:
667
202
  argument = self.xpmtype.arguments.get(k, None)
668
203
  if argument:
669
- if not bypass and (argument.generator or argument.constant):
204
+ if not bypass and (
205
+ (isinstance(argument.generator, Generator)) or argument.constant
206
+ ):
670
207
  raise AttributeError("Property %s is read-only" % (k))
671
208
  if v is not None:
672
209
  self.values[k] = argument.validate(v)
210
+ # Check for type variables
211
+ if type(argument.type) is TypeVarType:
212
+ self.check_typevar(argument.type.typevar, type(v))
213
+ if isinstance(v, Config):
214
+ # If the value is a Config, fuse type variables
215
+ v.__xpm__.fuse_concrete_typevars(self.concrete_typevars)
216
+ self.fuse_concrete_typevars(v.__xpm__.concrete_typevars)
673
217
  elif argument.required:
674
218
  raise AttributeError("Cannot set required attribute to None")
675
219
  else:
@@ -682,6 +226,43 @@ class ConfigInformation:
682
226
  logger.error("Error while setting value %s in %s", k, self.xpmtype)
683
227
  raise
684
228
 
229
+ def fuse_concrete_typevars(self, typevars: Dict[TypeVar, type]):
230
+ """Fuses concrete type variables with the current ones"""
231
+ for typevar, v in typevars.items():
232
+ self.check_typevar(typevar, v)
233
+
234
+ def check_typevar(self, typevar: TypeVar, v: type):
235
+ """Check if a type variable is coherent with the current typevars bindings,
236
+ updates the bindings if necessary"""
237
+ if typevar not in self.concrete_typevars:
238
+ self.concrete_typevars[typevar] = v
239
+ return
240
+
241
+ concrete_typevar = self.concrete_typevars[typevar]
242
+ bound = typevar.__bound__
243
+ # Check that v is a subclass of the typevar OR that typevar is a subclass of v
244
+ # Then set the concrete type variable to the most generic type
245
+
246
+ # First, limiting to the specified bound
247
+ if bound is not None:
248
+ if not issubclass(v, bound):
249
+ raise TypeError(
250
+ f"Type variable {typevar} is bound to {bound}, but tried to set it to {v}"
251
+ )
252
+
253
+ if issubclass(v, concrete_typevar):
254
+ # v is a subclass of the typevar, keep the typevar
255
+ return
256
+ if issubclass(concrete_typevar, v):
257
+ # typevar is a subclass of v, keep v
258
+ self.concrete_typevars[typevar] = v
259
+ return
260
+ raise TypeError(
261
+ f"Type variable {typevar} is already set to {self.concrete_typevars[typevar]}, "
262
+ f"but tried to set it to {v}"
263
+ f" (current typevars bindings: {self.concrete_typevars})"
264
+ )
265
+
685
266
  def addtag(self, name, value):
686
267
  self._tags[name] = value
687
268
 
@@ -752,18 +333,29 @@ class ConfigInformation:
752
333
 
753
334
  def postprocess(self, stub, config: Config, values):
754
335
  # Generate values
336
+ from experimaestro.generators import Generator
337
+
755
338
  for k, argument in config.__xpmtype__.arguments.items():
756
- if argument.generator:
757
- sig = inspect.signature(argument.generator)
758
- if len(sig.parameters) == 2:
759
- value = argument.generator(self.context, config)
760
- elif len(sig.parameters) == 0:
761
- value = argument.generator()
762
- else:
763
- assert (
764
- False
765
- ), "generator has either two parameters (context and config), or none"
766
- config.__xpm__.set(k, value, bypass=True)
339
+ try:
340
+ if argument.generator:
341
+ if not isinstance(argument.generator, Generator):
342
+ value = argument.generator()
343
+ else:
344
+ sig = inspect.signature(argument.generator)
345
+ if len(sig.parameters) == 0:
346
+ value = argument.generator()
347
+ elif len(sig.parameters) == 2:
348
+ value = argument.generator(self.context, config)
349
+ else:
350
+ assert (
351
+ False
352
+ ), "generator has either two parameters (context and config), or none"
353
+ config.__xpm__.set(k, value, bypass=True)
354
+ except Exception:
355
+ logger.error(
356
+ "While setting %s of %s", argument.name, config.__xpmtype__
357
+ )
358
+ raise
767
359
 
768
360
  config.__xpm__._sealed = True
769
361
 
@@ -805,6 +397,7 @@ class ConfigInformation:
805
397
 
806
398
  def identifiers(self, only_raw: bool):
807
399
  """Computes the unique identifier"""
400
+ from ..identifier import IdentifierComputer, Identifier
808
401
 
809
402
  raw_identifier = self._raw_identifier
810
403
  full_identifier = self._full_identifier
@@ -812,7 +405,7 @@ class ConfigInformation:
812
405
  # Computes raw identifier if needed
813
406
  if raw_identifier is None or not self._sealed:
814
407
  # Get the main identifier
815
- raw_identifier = HashComputer.compute(self.pyobject)
408
+ raw_identifier = IdentifierComputer.compute(self.pyobject)
816
409
  if self._sealed:
817
410
  self._raw_identifier = raw_identifier
818
411
 
@@ -833,7 +426,7 @@ class ConfigInformation:
833
426
 
834
427
  # Adds init tasks
835
428
  if self.init_tasks:
836
- hasher.update(HashComputer.INIT_TASKS)
429
+ hasher.update(IdentifierComputer.INIT_TASKS)
837
430
  for init_task in self.init_tasks:
838
431
  hasher.update(init_task.__xpm__.raw_identifier.all)
839
432
 
@@ -847,13 +440,13 @@ class ConfigInformation:
847
440
  return raw_identifier, full_identifier
848
441
 
849
442
  @property
850
- def raw_identifier(self) -> Identifier:
443
+ def raw_identifier(self) -> "Identifier":
851
444
  """Computes the unique identifier (without task modifiers)"""
852
445
  raw_identifier, _ = self.identifiers(True)
853
446
  return raw_identifier
854
447
 
855
448
  @property
856
- def full_identifier(self) -> Identifier:
449
+ def full_identifier(self) -> "Identifier":
857
450
  """Computes the unique identifier (with task modifiers)"""
858
451
  _, full_identifier = self.identifiers(False)
859
452
  return full_identifier
@@ -954,7 +547,7 @@ class ConfigInformation:
954
547
 
955
548
  :param callback: _description_
956
549
  """
957
- from .callbacks import TaskEventListener
550
+ from ..callbacks import TaskEventListener
958
551
 
959
552
  TaskEventListener.on_completed(self, callback)
960
553
 
@@ -968,7 +561,7 @@ class ConfigInformation:
968
561
  ):
969
562
  from experimaestro.scheduler import experiment, JobContext
970
563
  from experimaestro.scheduler.workspace import RunMode
971
- from .callbacks import TaskEventListener
564
+ from ..callbacks import TaskEventListener
972
565
 
973
566
  # --- Prepare the object
974
567
 
@@ -1355,7 +948,7 @@ class ConfigInformation:
1355
948
  as_instance=True,
1356
949
  save_directory: Optional[Path] = None,
1357
950
  discard_id: bool = False,
1358
- ) -> "TypeConfig":
951
+ ) -> "ConfigMixin":
1359
952
  ...
1360
953
 
1361
954
  @overload
@@ -1390,6 +983,7 @@ class ConfigInformation:
1390
983
  o = None
1391
984
  objects = {}
1392
985
  import experimaestro.taskglobals as taskglobals
986
+ from ..identifier import Identifier
1393
987
 
1394
988
  # Loop over all the definitions and create objects
1395
989
  for definition in definitions:
@@ -1656,28 +1250,7 @@ def clone(v):
1656
1250
  raise NotImplementedError("Clone not implemented for type %s" % type(v))
1657
1251
 
1658
1252
 
1659
- def cache(fn, name: str):
1660
- def __call__(config, *args, **kwargs):
1661
- import experimaestro.taskglobals as taskglobals
1662
-
1663
- # Get path and create directory if needed
1664
- hexid = config.__xpmidentifier__ # type: Identifier
1665
- typename = config.__xpmtypename__ # type: str
1666
- dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
1667
-
1668
- if not dir.exists():
1669
- dir.mkdir(parents=True, exist_ok=True)
1670
-
1671
- path = dir / name
1672
- ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
1673
- with ipc_lock:
1674
- r = fn(config, path, *args, **kwargs)
1675
- return r
1676
-
1677
- return __call__
1678
-
1679
-
1680
- class TypeConfig:
1253
+ class ConfigMixin:
1681
1254
  """Class for configuration objects"""
1682
1255
 
1683
1256
  __xpmtype__: ObjectType
@@ -1828,7 +1401,7 @@ class TypeConfig:
1828
1401
 
1829
1402
  def add_pretasks_from(self, *configs: "Config"):
1830
1403
  assert all(
1831
- [isinstance(config, TypeConfig) for config in configs]
1404
+ [isinstance(config, ConfigMixin) for config in configs]
1832
1405
  ), "One of the parameters is not a configuration object"
1833
1406
  for config in configs:
1834
1407
  self.add_pretasks(*config.__xpm__.pre_tasks)
@@ -1851,11 +1424,6 @@ class TypeConfig:
1851
1424
  self.__xpm__.add_dependencies(*other.__xpm__.dependencies)
1852
1425
 
1853
1426
 
1854
- class classproperty(property):
1855
- def __get__(self, owner_self, owner_cls):
1856
- return self.fget(owner_cls)
1857
-
1858
-
1859
1427
  class Config:
1860
1428
  """Base type for all objects in python interface"""
1861
1429
 
@@ -1873,24 +1441,20 @@ class Config:
1873
1441
 
1874
1442
  @classproperty
1875
1443
  def XPMConfig(cls):
1876
- if issubclass(cls, TypeConfig):
1444
+ if issubclass(cls, ConfigMixin):
1877
1445
  return cls
1878
1446
  return cls.__getxpmtype__().configtype
1879
1447
 
1880
- @classproperty
1881
- def C(cls):
1882
- return cls.XPMConfig
1883
-
1884
1448
  @classproperty
1885
1449
  def XPMValue(cls):
1886
1450
  """Returns the value object for this configuration"""
1887
- if issubclass(cls, TypeConfig):
1451
+ if issubclass(cls, ConfigMixin):
1888
1452
  return cls.__xpmtype__.objecttype
1889
1453
 
1890
1454
  if value_cls := cls.__dict__.get("__XPMValue__", None):
1891
1455
  pass
1892
1456
  else:
1893
- from .types import XPMValue
1457
+ from ..types import XPMValue
1894
1458
 
1895
1459
  __objectbases__ = tuple(
1896
1460
  s.XPMValue
@@ -1907,9 +1471,20 @@ class Config:
1907
1471
 
1908
1472
  return value_cls
1909
1473
 
1474
+ @classproperty
1475
+ def C(cls):
1476
+ """Alias for XPMConfig"""
1477
+ return cls.XPMConfig
1478
+
1479
+ @classproperty
1480
+ def V(cls):
1481
+ """Alias for XPMValue"""
1482
+ return cls.XPMValue
1483
+
1910
1484
  @classmethod
1911
1485
  def __getxpmtype__(cls) -> "ObjectType":
1912
- """Get (and create if necessary) the Object type of this"""
1486
+ """Get (and create if necessary) the Object type associated
1487
+ with thie Config object"""
1913
1488
  xpmtype = cls.__dict__.get("__xpmtype__", None)
1914
1489
  if xpmtype is None:
1915
1490
  from experimaestro.core.types import ObjectType
@@ -1923,9 +1498,12 @@ class Config:
1923
1498
  return xpmtype
1924
1499
 
1925
1500
  def __new__(cls: Type[T], *args, **kwargs) -> T:
1926
- """Returns an instance of a TypeConfig (for compatibility, use XPMConfig
1927
- or C if possible)"""
1501
+ """Returns an instance of a ConfigMixin (for compatibility, use XPMConfig
1502
+ or C if possible)
1928
1503
 
1504
+ :deprecated: Use Config.C or Config.XPMConfig to construct a new
1505
+ configuration, and Config.V (or Config.XPMValue) for a new value
1506
+ """
1929
1507
  # If this is an XPMValue, just return a new instance
1930
1508
  from experimaestro.core.types import XPMValue
1931
1509
 
@@ -1934,15 +1512,24 @@ class Config:
1934
1512
 
1935
1513
  # If this is the XPMConfig, just return a new instance
1936
1514
  # __init__ will be called
1937
- if issubclass(cls, TypeConfig):
1515
+ if issubclass(cls, ConfigMixin):
1938
1516
  return object.__new__(cls)
1939
1517
 
1518
+ # Log a deprecation warning for this way of creating a configuration
1519
+ caller = inspect.getframeinfo(inspect.stack()[1][0])
1520
+ logger.warning(
1521
+ "Creating a configuration using Config.__new__ is deprecated, and will be removed in a future version. "
1522
+ "Use Config.C or Config.XPMConfig to create a new configuration. "
1523
+ "Issue created at %s:%s",
1524
+ str(Path(caller.filename).absolute()),
1525
+ caller.lineno,
1526
+ )
1527
+
1940
1528
  # otherwise, we use the configuration type
1941
- o: TypeConfig = object.__new__(cls.__getxpmtype__().configtype)
1529
+ o: ConfigMixin = object.__new__(cls.__getxpmtype__().configtype)
1942
1530
  try:
1943
1531
  o.__init__(*args, **kwargs)
1944
1532
  except Exception:
1945
- caller = inspect.getframeinfo(inspect.stack()[1][0])
1946
1533
  logger.error(
1947
1534
  "Init error in %s:%s"
1948
1535
  % (str(Path(caller.filename).absolute()), caller.lineno)
@@ -1963,8 +1550,8 @@ class Config:
1963
1550
  """Returns a JSON version of the object (if possible)"""
1964
1551
  return self.__xpm__.__json__()
1965
1552
 
1966
- def __identifier__(self) -> Identifier:
1967
- return self.__xpm__.identifier
1553
+ def __identifier__(self) -> "Identifier":
1554
+ return self.__xpm__.full_identifier
1968
1555
 
1969
1556
  def add_pretasks(self, *tasks: "LightweightTask"):
1970
1557
  """Add pre-tasks"""
@@ -2069,3 +1656,24 @@ def setmeta(config: Config, flag: bool):
2069
1656
  """Flags the configuration as a meta-parameter"""
2070
1657
  config.__xpm__.set_meta(flag)
2071
1658
  return config
1659
+
1660
+
1661
+ def cache(fn, name: str):
1662
+ def __call__(config, *args, **kwargs):
1663
+ import experimaestro.taskglobals as taskglobals
1664
+
1665
+ # Get path and create directory if needed
1666
+ hexid = config.__xpmidentifier__ # type: Identifier
1667
+ typename = config.__xpmtypename__ # type: str
1668
+ dir = taskglobals.Env.instance().wspath / "config" / typename / hexid.all.hex()
1669
+
1670
+ if not dir.exists():
1671
+ dir.mkdir(parents=True, exist_ok=True)
1672
+
1673
+ path = dir / name
1674
+ ipc_lock = fasteners.InterProcessLock(path.with_suffix(path.suffix + ".lock"))
1675
+ with ipc_lock:
1676
+ r = fn(config, path, *args, **kwargs)
1677
+ return r
1678
+
1679
+ return __call__