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.
- experimaestro/core/identifier.py +296 -0
- experimaestro/core/objects/__init__.py +44 -0
- experimaestro/core/{objects.py → objects/config.py} +157 -549
- experimaestro/core/objects/config_utils.py +58 -0
- experimaestro/core/objects/config_walk.py +150 -0
- experimaestro/core/objects.pyi +6 -38
- experimaestro/core/types.py +28 -4
- experimaestro/notifications.py +1 -0
- experimaestro/scheduler/base.py +1 -0
- experimaestro/tests/core/__init__.py +0 -0
- experimaestro/tests/core/test_generics.py +206 -0
- experimaestro/tests/restart.py +3 -1
- experimaestro/tests/test_instance.py +3 -3
- experimaestro/tests/test_objects.py +20 -4
- experimaestro/tests/test_serializers.py +3 -3
- experimaestro/tests/test_types.py +2 -2
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/METADATA +5 -5
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/RECORD +21 -21
- 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-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/LICENSE +0 -0
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/WHEEL +0 -0
- {experimaestro-1.8.0rc6.dist-info → experimaestro-1.8.3.dist-info}/entry_points.txt +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,150 @@
|
|
|
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
|
+
return self.context.push(k)
|
|
75
|
+
|
|
76
|
+
def stub(self, config):
|
|
77
|
+
return config
|
|
78
|
+
|
|
79
|
+
def __call__(self, x):
|
|
80
|
+
from experimaestro.core.objects import Config
|
|
81
|
+
from experimaestro.core.objects import ConfigInformation # noqa: F401
|
|
82
|
+
|
|
83
|
+
if isinstance(x, Config):
|
|
84
|
+
info = x.__xpm__ # type: ConfigInformation
|
|
85
|
+
|
|
86
|
+
# Avoid loops
|
|
87
|
+
xid = id(x)
|
|
88
|
+
if xid in self.visited:
|
|
89
|
+
return self.visited[xid]
|
|
90
|
+
|
|
91
|
+
# Get a stub
|
|
92
|
+
stub = self.stub(x)
|
|
93
|
+
self.visited[xid] = stub
|
|
94
|
+
|
|
95
|
+
# Pre-process
|
|
96
|
+
flag, value = self.preprocess(x)
|
|
97
|
+
|
|
98
|
+
if not flag:
|
|
99
|
+
# Stop processing and returns value
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
# Process all the arguments
|
|
103
|
+
result = {}
|
|
104
|
+
for arg, v in info.xpmvalues():
|
|
105
|
+
if v is not None:
|
|
106
|
+
with self.map(arg.name):
|
|
107
|
+
result[arg.name] = self(v)
|
|
108
|
+
else:
|
|
109
|
+
result[arg.name] = None
|
|
110
|
+
|
|
111
|
+
# Deals with pre-tasks
|
|
112
|
+
if info.pre_tasks:
|
|
113
|
+
with self.map("__pre_tasks__"):
|
|
114
|
+
self(info.pre_tasks)
|
|
115
|
+
|
|
116
|
+
if info.init_tasks:
|
|
117
|
+
with self.map("__init_tasks__"):
|
|
118
|
+
self(info.init_tasks)
|
|
119
|
+
|
|
120
|
+
# Process task if different
|
|
121
|
+
if (
|
|
122
|
+
x.__xpm__.task is not None
|
|
123
|
+
and self.recurse_task
|
|
124
|
+
and x.__xpm__.task is not x
|
|
125
|
+
):
|
|
126
|
+
self(x.__xpm__.task)
|
|
127
|
+
|
|
128
|
+
processed = self.postprocess(stub, x, result)
|
|
129
|
+
self.visited[xid] = processed
|
|
130
|
+
return processed
|
|
131
|
+
|
|
132
|
+
if isinstance(x, list):
|
|
133
|
+
result = []
|
|
134
|
+
for i, sv in enumerate(x):
|
|
135
|
+
with self.list(i):
|
|
136
|
+
result.append(self(sv))
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
if isinstance(x, dict):
|
|
140
|
+
result = {}
|
|
141
|
+
for key, value in x.items():
|
|
142
|
+
assert isinstance(key, (str, float, int))
|
|
143
|
+
with self.map(key):
|
|
144
|
+
result[key] = self(value)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
if isinstance(x, (float, int, str, Path, Enum)):
|
|
148
|
+
return x
|
|
149
|
+
|
|
150
|
+
raise NotImplementedError(f"Cannot handle a value of type {type(x)}")
|
experimaestro/core/objects.pyi
CHANGED
|
@@ -41,40 +41,8 @@ from typing_extensions import Self
|
|
|
41
41
|
|
|
42
42
|
TConfig = TypeVar("TConfig", bound="Config")
|
|
43
43
|
|
|
44
|
-
class Identifier:
|
|
45
|
-
main: Incomplete
|
|
46
|
-
sub: Incomplete
|
|
47
|
-
def __init__(self, main: bytes, sub: Optional[bytes] = ...) -> None: ...
|
|
48
|
-
def all(self): ...
|
|
49
|
-
def state_dict(self): ...
|
|
50
|
-
@staticmethod
|
|
51
|
-
def from_state_dict(data: Union[Dict[str, str], str]): ...
|
|
52
|
-
|
|
53
|
-
def is_ignored(value): ...
|
|
54
|
-
def remove_meta(value): ...
|
|
55
|
-
|
|
56
44
|
class ObjectStore: ...
|
|
57
45
|
|
|
58
|
-
class HashComputer:
|
|
59
|
-
OBJECT_ID: bytes
|
|
60
|
-
INT_ID: bytes
|
|
61
|
-
FLOAT_ID: bytes
|
|
62
|
-
STR_ID: bytes
|
|
63
|
-
PATH_ID: bytes
|
|
64
|
-
NAME_ID: bytes
|
|
65
|
-
NONE_ID: bytes
|
|
66
|
-
LIST_ID: bytes
|
|
67
|
-
TASK_ID: bytes
|
|
68
|
-
DICT_ID: bytes
|
|
69
|
-
ENUM_ID: bytes
|
|
70
|
-
def __init__(self) -> None: ...
|
|
71
|
-
def identifier(self) -> Identifier: ...
|
|
72
|
-
def update(self, value, myself: bool = ...): ...
|
|
73
|
-
|
|
74
|
-
def updatedependencies(
|
|
75
|
-
dependencies, value: Config, path: List[str], taskids: Set[int]
|
|
76
|
-
): ...
|
|
77
|
-
|
|
78
46
|
class TaggedValue:
|
|
79
47
|
value: Incomplete
|
|
80
48
|
def __init__(self, value) -> None: ...
|
|
@@ -122,7 +90,7 @@ class ConfigInformation:
|
|
|
122
90
|
job: Job
|
|
123
91
|
dependencies: Incomplete
|
|
124
92
|
watched_outputs: List[WatchedOutput]
|
|
125
|
-
def __init__(self, pyobject:
|
|
93
|
+
def __init__(self, pyobject: ConfigMixin) -> None: ...
|
|
126
94
|
def set_meta(self, value: Optional[bool]): ...
|
|
127
95
|
@property
|
|
128
96
|
def meta(self): ...
|
|
@@ -162,7 +130,7 @@ class ConfigInformation:
|
|
|
162
130
|
definitions: List[Dict],
|
|
163
131
|
as_instance: bool = ...,
|
|
164
132
|
save_directory: Optional[Path] = ...,
|
|
165
|
-
) ->
|
|
133
|
+
) -> ConfigMixin: ...
|
|
166
134
|
@overload
|
|
167
135
|
@staticmethod
|
|
168
136
|
def fromParameters(
|
|
@@ -183,7 +151,7 @@ class ConfigInformation:
|
|
|
183
151
|
def clone(v): ...
|
|
184
152
|
def cache(fn, name: str): ...
|
|
185
153
|
|
|
186
|
-
class
|
|
154
|
+
class ConfigMixin:
|
|
187
155
|
__xpmtype__: ObjectType
|
|
188
156
|
__xpm__: Incomplete
|
|
189
157
|
def __init__(self, **kwargs) -> None: ...
|
|
@@ -217,8 +185,8 @@ class Config:
|
|
|
217
185
|
__use_xpmobject__: ClassVar[bool]
|
|
218
186
|
|
|
219
187
|
XPMValue: Type[Self]
|
|
220
|
-
XPMConfig: Union[Type[Self], Type[
|
|
221
|
-
C: Union[Type[Self], Type[
|
|
188
|
+
XPMConfig: Union[Type[Self], Type[ConfigMixin[Self]]]
|
|
189
|
+
C: Union[Type[Self], Type[ConfigMixin[Self]]]
|
|
222
190
|
|
|
223
191
|
@classmethod
|
|
224
192
|
def __getxpmtype__(cls) -> ObjectType: ...
|
|
@@ -252,6 +220,6 @@ class Task(LightweightTask):
|
|
|
252
220
|
def copyconfig(config_or_output: TConfig, **kwargs) -> TConfig: ...
|
|
253
221
|
def setmeta(config: TConfig, flag: bool) -> TConfig: ...
|
|
254
222
|
|
|
255
|
-
class
|
|
223
|
+
class ConfigMixin(Generic[T]):
|
|
256
224
|
def __validate__(self):
|
|
257
225
|
pass
|
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
|
|
@@ -130,10 +130,13 @@ class Type:
|
|
|
130
130
|
if union_t := typingutils.get_union(key):
|
|
131
131
|
return UnionType([Type.fromType(t) for t in union_t])
|
|
132
132
|
|
|
133
|
-
# Takes care of generics
|
|
133
|
+
# Takes care of generics, like List[int], not List
|
|
134
134
|
if get_origin(key):
|
|
135
135
|
return GenericType(key)
|
|
136
136
|
|
|
137
|
+
if isinstance(key, TypeVar):
|
|
138
|
+
return TypeVarType(key)
|
|
139
|
+
|
|
137
140
|
raise Exception("No type found for %s", key)
|
|
138
141
|
|
|
139
142
|
|
|
@@ -214,7 +217,7 @@ class ObjectType(Type):
|
|
|
214
217
|
identifier: Union[str, Identifier] = None,
|
|
215
218
|
):
|
|
216
219
|
"""Creates a type"""
|
|
217
|
-
from .objects import Config,
|
|
220
|
+
from .objects import Config, ConfigMixin
|
|
218
221
|
|
|
219
222
|
# Task related attributes
|
|
220
223
|
self.taskcommandfactory = None
|
|
@@ -268,7 +271,7 @@ class ObjectType(Type):
|
|
|
268
271
|
s.__getxpmtype__().configtype
|
|
269
272
|
for s in tp.__bases__
|
|
270
273
|
if issubclass(s, Config) and (s is not Config)
|
|
271
|
-
) or (
|
|
274
|
+
) or (ConfigMixin,)
|
|
272
275
|
|
|
273
276
|
*tp_qual, tp_name = self.basetype.__qualname__.split(".")
|
|
274
277
|
self.configtype = type(
|
|
@@ -597,6 +600,23 @@ class AnyType(Type):
|
|
|
597
600
|
return value
|
|
598
601
|
|
|
599
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
|
+
|
|
600
620
|
Any = AnyType()
|
|
601
621
|
|
|
602
622
|
|
|
@@ -698,6 +718,10 @@ class GenericType(Type):
|
|
|
698
718
|
def __repr__(self):
|
|
699
719
|
return repr(self.type)
|
|
700
720
|
|
|
721
|
+
def identifier(self):
|
|
722
|
+
"""Returns the identifier of the type"""
|
|
723
|
+
return Identifier(f"{self.origin}.{self.type}")
|
|
724
|
+
|
|
701
725
|
def validate(self, value):
|
|
702
726
|
# Now, let's check generics...
|
|
703
727
|
mros = typingutils.generic_mro(type(value))
|
experimaestro/notifications.py
CHANGED
|
@@ -109,6 +109,7 @@ class Reporter(threading.Thread):
|
|
|
109
109
|
return any(level.modified(self) for level in self.levels)
|
|
110
110
|
|
|
111
111
|
def check_urls(self):
|
|
112
|
+
"""Check whether we have new schedulers to notify"""
|
|
112
113
|
mtime = os.path.getmtime(self.path)
|
|
113
114
|
if mtime > self.lastcheck:
|
|
114
115
|
for f in self.path.iterdir():
|
experimaestro/scheduler/base.py
CHANGED
|
File without changes
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Tests for the use of generics in configurations"""
|
|
2
|
+
|
|
3
|
+
from typing import Generic, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from experimaestro import Config, Param
|
|
7
|
+
from experimaestro.core.arguments import Argument
|
|
8
|
+
from experimaestro.core.types import TypeVarType
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SimpleConfig(Config):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimpleConfigChild(SimpleConfig):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SimpleGenericConfig(Config, Generic[T]):
|
|
22
|
+
x: Param[T]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SimpleGenericConfigChild(SimpleGenericConfig, Generic[T]):
|
|
26
|
+
"""A child class of SimpleGenericConfig that also uses generics"""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_core_generics_typevar():
|
|
32
|
+
a = SimpleGenericConfig.C(x=1)
|
|
33
|
+
|
|
34
|
+
x_arg = a.__xpmtype__.arguments["x"]
|
|
35
|
+
|
|
36
|
+
# Check correct interpretation of typevar
|
|
37
|
+
assert type(x_arg) is Argument
|
|
38
|
+
assert isinstance(x_arg.type, TypeVarType)
|
|
39
|
+
assert x_arg.type.typevar == T
|
|
40
|
+
|
|
41
|
+
assert isinstance(a.x, int)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_core_generics_simple():
|
|
45
|
+
a = SimpleGenericConfig.C(x=2)
|
|
46
|
+
|
|
47
|
+
# OK
|
|
48
|
+
a.x = 3
|
|
49
|
+
|
|
50
|
+
# Fails: changing generics is not allowed
|
|
51
|
+
with pytest.raises(TypeError):
|
|
52
|
+
a.x = "a string"
|
|
53
|
+
|
|
54
|
+
# typevar bindings are local to the instance,
|
|
55
|
+
# so we can create a new instance with a different type
|
|
56
|
+
SimpleGenericConfig.C(x="a string")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DoubleGenericConfig(Config, Generic[T]):
|
|
60
|
+
x: Param[T]
|
|
61
|
+
y: Param[T]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_core_generics_double():
|
|
65
|
+
# OK
|
|
66
|
+
DoubleGenericConfig.C(x=1, y=1)
|
|
67
|
+
|
|
68
|
+
# Fails
|
|
69
|
+
with pytest.raises(TypeError):
|
|
70
|
+
DoubleGenericConfig.C(x=1, y="a")
|
|
71
|
+
|
|
72
|
+
a = DoubleGenericConfig.C(x=1, y=1)
|
|
73
|
+
a.y = 2
|
|
74
|
+
with pytest.raises(TypeError):
|
|
75
|
+
a.x = "b"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_core_generics_double_rebind():
|
|
79
|
+
a = DoubleGenericConfig.C(x=1, y=1)
|
|
80
|
+
# Rebinding to a different type should not work
|
|
81
|
+
with pytest.raises(TypeError):
|
|
82
|
+
a.x, a.y = "some", "string"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_core_generics_double_plus():
|
|
86
|
+
# Testing with inheritance
|
|
87
|
+
# We allow subclasses of the typevar binding
|
|
88
|
+
# We also allow generalizing up the typevar binding
|
|
89
|
+
# This means that we can use a super class of the typevar binding
|
|
90
|
+
|
|
91
|
+
# Works
|
|
92
|
+
a = DoubleGenericConfig.C(x=SimpleConfigChild.C())
|
|
93
|
+
a.y = SimpleConfig.C()
|
|
94
|
+
|
|
95
|
+
# Works also
|
|
96
|
+
b = DoubleGenericConfig.C(x=SimpleConfig.C())
|
|
97
|
+
b.y = SimpleConfigChild.C()
|
|
98
|
+
|
|
99
|
+
a.x = SimpleConfigChild.C()
|
|
100
|
+
|
|
101
|
+
with pytest.raises(TypeError):
|
|
102
|
+
a.x = "a string"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_core_generics_double_type_escalation():
|
|
106
|
+
a = DoubleGenericConfig.C(x=SimpleConfigChild.C())
|
|
107
|
+
a.y = SimpleConfigChild.C()
|
|
108
|
+
# T is now bound to SimpleConfigChild
|
|
109
|
+
|
|
110
|
+
a.y = SimpleConfig.C()
|
|
111
|
+
# T is now bound to SimpleConfig
|
|
112
|
+
|
|
113
|
+
a.y = object()
|
|
114
|
+
# T is now bound to object, which is a super class of SimpleConfigChild
|
|
115
|
+
|
|
116
|
+
# This is allowed, since we are not changing the typevar binding
|
|
117
|
+
a.x = "a string"
|
|
118
|
+
|
|
119
|
+
a.y = dict()
|
|
120
|
+
# This is allowed, since we are not changing the typevar binding
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_core_generics_double_deep_bind():
|
|
124
|
+
# Since we are deep binding the typevar T to a specific type,
|
|
125
|
+
# we should not be able to have coherent *local-only* type bindings
|
|
126
|
+
# The type bindings are transient
|
|
127
|
+
|
|
128
|
+
with pytest.raises(TypeError):
|
|
129
|
+
DoubleGenericConfig.C(
|
|
130
|
+
x=DoubleGenericConfig.C(x=1, y=2), y=DoubleGenericConfig.C(x=3, y=4)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class NestedConfig(Config, Generic[T]):
|
|
135
|
+
x: Param[DoubleGenericConfig[T]]
|
|
136
|
+
y: Param[SimpleGenericConfig[T]]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_core_generics_nested():
|
|
140
|
+
# OK
|
|
141
|
+
NestedConfig.C(x=DoubleGenericConfig.C(x=1, y=1), y=SimpleGenericConfig.C(x=2))
|
|
142
|
+
|
|
143
|
+
# Not OK
|
|
144
|
+
with pytest.raises(TypeError):
|
|
145
|
+
NestedConfig.C(
|
|
146
|
+
x=DoubleGenericConfig.C(x=1, y=1), y=SimpleGenericConfig.C(x="b")
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
with pytest.raises(TypeError):
|
|
150
|
+
a = NestedConfig.C(
|
|
151
|
+
x=DoubleGenericConfig.C(x=1, y=1), y=SimpleGenericConfig.C(x=1)
|
|
152
|
+
)
|
|
153
|
+
a.x.x = "a string"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TreeGenericConfig(Config, Generic[T]):
|
|
157
|
+
x: Param[T]
|
|
158
|
+
left: Optional["TreeGenericConfig[T]"] = None
|
|
159
|
+
right: Optional["TreeGenericConfig[T]"] = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TagTreeGenericConfig(TreeGenericConfig[T], Generic[T]):
|
|
163
|
+
"""A tagged version of TreeGenericConfig to test recursive generics"""
|
|
164
|
+
|
|
165
|
+
tag: Param[str] = "default"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_core_generics_recursive():
|
|
169
|
+
a = TreeGenericConfig.C(x=1)
|
|
170
|
+
a.left = TreeGenericConfig.C(x=2)
|
|
171
|
+
a.right = TreeGenericConfig.C(x=3)
|
|
172
|
+
|
|
173
|
+
with pytest.raises(TypeError):
|
|
174
|
+
a.left.x = "a string"
|
|
175
|
+
|
|
176
|
+
# OK to use a child class
|
|
177
|
+
a.left = TagTreeGenericConfig.C(x=4, tag="left")
|
|
178
|
+
|
|
179
|
+
with pytest.raises(TypeError):
|
|
180
|
+
a.left.x = "a string"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_core_generics_recursive_child():
|
|
184
|
+
# Testing with a child class on the generic value
|
|
185
|
+
a = TreeGenericConfig.C(x=SimpleConfig.C())
|
|
186
|
+
a.left = TreeGenericConfig.C(x=SimpleConfig.C())
|
|
187
|
+
a.right = TreeGenericConfig.C(x=SimpleConfig.C())
|
|
188
|
+
|
|
189
|
+
a.left.x = SimpleConfigChild.C()
|
|
190
|
+
|
|
191
|
+
with pytest.raises(TypeError):
|
|
192
|
+
a.left.x = "a string"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
U = TypeVar("U", bound=SimpleConfigChild)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class BoundGenericConfig(Config, Generic[U]):
|
|
199
|
+
x: Param[U]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_core_generics_bound_typevar():
|
|
203
|
+
a = BoundGenericConfig.C(x=SimpleConfigChild.C())
|
|
204
|
+
assert isinstance(a.x, SimpleConfigChild)
|
|
205
|
+
with pytest.raises(TypeError):
|
|
206
|
+
a.x = SimpleConfig.C()
|
experimaestro/tests/restart.py
CHANGED
|
@@ -27,6 +27,8 @@ TERMINATES_FUNC = [terminate]
|
|
|
27
27
|
if is_posix():
|
|
28
28
|
TERMINATES_FUNC.append(sigint)
|
|
29
29
|
|
|
30
|
+
MAX_RESTART_WAIT = 50 # 5 seconds
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
class Restart(Task):
|
|
32
34
|
touch: Meta[Path] = field(default_factory=PathGenerator("touch"))
|
|
@@ -81,7 +83,7 @@ def restart(terminate: Callable, experiment):
|
|
|
81
83
|
while not task.touch.is_file():
|
|
82
84
|
time.sleep(0.1)
|
|
83
85
|
counter += 1
|
|
84
|
-
if counter >=
|
|
86
|
+
if counter >= MAX_RESTART_WAIT:
|
|
85
87
|
terminate(xpmprocess)
|
|
86
88
|
assert False, "Timeout waiting for task to be executed"
|
|
87
89
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
from experimaestro import Param, Config
|
|
3
|
-
from experimaestro.core.objects import
|
|
3
|
+
from experimaestro.core.objects import ConfigMixin
|
|
4
4
|
from experimaestro.core.serializers import SerializationLWTask
|
|
5
5
|
|
|
6
6
|
|
|
@@ -21,10 +21,10 @@ def test_simple_instance():
|
|
|
21
21
|
b = B(a=a)
|
|
22
22
|
b = b.instance()
|
|
23
23
|
|
|
24
|
-
assert not isinstance(b,
|
|
24
|
+
assert not isinstance(b, ConfigMixin)
|
|
25
25
|
assert isinstance(b, B.__xpmtype__.objecttype)
|
|
26
26
|
|
|
27
|
-
assert not isinstance(b.a,
|
|
27
|
+
assert not isinstance(b.a, ConfigMixin)
|
|
28
28
|
assert isinstance(b.a, A1.__xpmtype__.objecttype)
|
|
29
29
|
assert isinstance(b.a, A.__xpmtype__.basetype)
|
|
30
30
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import pytest
|
|
4
5
|
from experimaestro import Config, Task, Annotated, copyconfig, default
|
|
5
6
|
from experimaestro.core.arguments import Param
|
|
6
|
-
from experimaestro.core.objects import
|
|
7
|
+
from experimaestro.core.objects import ConfigMixin
|
|
7
8
|
from experimaestro.core.types import XPMValue
|
|
8
9
|
from experimaestro.generators import pathgenerator
|
|
9
10
|
from experimaestro.scheduler.workspace import RunMode
|
|
@@ -65,9 +66,9 @@ def test_hierarchy():
|
|
|
65
66
|
assert issubclass(B, Config)
|
|
66
67
|
assert issubclass(C, Config)
|
|
67
68
|
|
|
68
|
-
assert not issubclass(OA,
|
|
69
|
-
assert not issubclass(OB,
|
|
70
|
-
assert not issubclass(OC,
|
|
69
|
+
assert not issubclass(OA, ConfigMixin)
|
|
70
|
+
assert not issubclass(OB, ConfigMixin)
|
|
71
|
+
assert not issubclass(OC, ConfigMixin)
|
|
71
72
|
|
|
72
73
|
assert issubclass(C, B)
|
|
73
74
|
|
|
@@ -91,3 +92,18 @@ def test_copyconfig(xp):
|
|
|
91
92
|
|
|
92
93
|
assert copy_b.x == b.x
|
|
93
94
|
assert "path" not in copy_b.__xpm__.values
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_direct_config_warns(caplog):
|
|
98
|
+
"""Test that using a building Config directly raises a warning"""
|
|
99
|
+
message = "Config.__new__ is deprecated"
|
|
100
|
+
|
|
101
|
+
with caplog.at_level(logging.WARNING):
|
|
102
|
+
A(x=3)
|
|
103
|
+
assert message in caplog.text
|
|
104
|
+
|
|
105
|
+
caplog.clear()
|
|
106
|
+
|
|
107
|
+
with caplog.at_level(logging.WARNING):
|
|
108
|
+
A.C(x=3)
|
|
109
|
+
assert message not in caplog.text
|
|
@@ -8,7 +8,7 @@ from experimaestro import (
|
|
|
8
8
|
from_state_dict,
|
|
9
9
|
)
|
|
10
10
|
from experimaestro.core.context import SerializationContext
|
|
11
|
-
from experimaestro.core.objects import
|
|
11
|
+
from experimaestro.core.objects import ConfigMixin
|
|
12
12
|
from experimaestro.tests.utils import TemporaryExperiment
|
|
13
13
|
|
|
14
14
|
|
|
@@ -84,11 +84,11 @@ def test_serializers_serialization():
|
|
|
84
84
|
data = state_dict(context, [obj1, obj2])
|
|
85
85
|
|
|
86
86
|
[obj1, obj2] = from_state_dict(data)
|
|
87
|
-
assert isinstance(obj1, Object1) and isinstance(obj1,
|
|
87
|
+
assert isinstance(obj1, Object1) and isinstance(obj1, ConfigMixin)
|
|
88
88
|
assert isinstance(obj2, Object2)
|
|
89
89
|
assert obj2.object is obj1
|
|
90
90
|
|
|
91
91
|
[obj1, obj2] = from_state_dict(data, as_instance=True)
|
|
92
|
-
assert isinstance(obj1, Object1) and not isinstance(obj1,
|
|
92
|
+
assert isinstance(obj1, Object1) and not isinstance(obj1, ConfigMixin)
|
|
93
93
|
assert isinstance(obj2, Object2)
|
|
94
94
|
assert obj2.object is obj1
|