pyconfigtree 0.1.0__tar.gz
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.
- pyconfigtree-0.1.0/.gitignore +11 -0
- pyconfigtree-0.1.0/.idea/.gitignore +8 -0
- pyconfigtree-0.1.0/PKG-INFO +8 -0
- pyconfigtree-0.1.0/README.md +0 -0
- pyconfigtree-0.1.0/dist/.gitignore +1 -0
- pyconfigtree-0.1.0/pyconfigtree/__init__.py +3 -0
- pyconfigtree-0.1.0/pyconfigtree/base.py +273 -0
- pyconfigtree-0.1.0/pyconfigtree/exceptions.py +34 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/__init__.py +9 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/base.py +267 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/bool_parameter.py +62 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/choice_parameter.py +92 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/float_parameter.py +27 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/int_parameter.py +27 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/list_parameter.py +143 -0
- pyconfigtree-0.1.0/pyconfigtree/parameter/string_parameter.py +27 -0
- pyconfigtree-0.1.0/pyconfigtree/py.typed +0 -0
- pyconfigtree-0.1.0/pyconfigtree/source/__init__.py +2 -0
- pyconfigtree-0.1.0/pyconfigtree/source/base.py +42 -0
- pyconfigtree-0.1.0/pyconfigtree/source/json.py +41 -0
- pyconfigtree-0.1.0/pyconfigtree/source/toml.py +43 -0
- pyconfigtree-0.1.0/pyproject.toml +78 -0
- pyconfigtree-0.1.0/tests/__init__.py +0 -0
- pyconfigtree-0.1.0/tests/node_attachment_test.py +57 -0
- pyconfigtree-0.1.0/uv.lock +413 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyconfigtree
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight yet powerful mini-framework for building hierarchical, tree-structured configs.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
7
|
+
Provides-Extra: toml
|
|
8
|
+
Requires-Dist: tomli-w>=1.0.0; extra == 'toml'
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypeVar, TypeAlias, overload
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from collections.abc import Mapping, Callable, Sequence, Awaitable, Generator
|
|
7
|
+
|
|
8
|
+
from .source import ConfigSource
|
|
9
|
+
from .exceptions import LeafNodeError, NodeLoopError, NoSourceError, NodeDuplicateError
|
|
10
|
+
from .source.base import NodeInfo, NodeType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T', bound='Node')
|
|
14
|
+
|
|
15
|
+
ON_NODE_ATTACHED_HOOK: TypeAlias = Callable[['Node', 'Node'], Awaitable[Any]]
|
|
16
|
+
ON_NODE_DETACHED_HOOK: TypeAlias = Callable[['Node', 'Node'], Awaitable[Any]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseHookTypes(Enum):
|
|
20
|
+
ON_NODE_ATTACHED = auto()
|
|
21
|
+
ON_NODE_DETACHED = auto()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Node:
|
|
25
|
+
_allow_children: bool = True
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
node_id: str,
|
|
30
|
+
name: str = '',
|
|
31
|
+
description: str = '',
|
|
32
|
+
source: ConfigSource | None = None,
|
|
33
|
+
flags: set[Any] | None = None,
|
|
34
|
+
on_node_attached_hook: ON_NODE_ATTACHED_HOOK | None = None,
|
|
35
|
+
on_node_detached_hook: ON_NODE_DETACHED_HOOK | None = None,
|
|
36
|
+
):
|
|
37
|
+
self._id: str = node_id
|
|
38
|
+
self._name = name
|
|
39
|
+
self._description = description
|
|
40
|
+
self._parent: Node | None = None
|
|
41
|
+
self._subnodes: dict[str, Node] = {}
|
|
42
|
+
self._subnodes_proxy = MappingProxyType(self._subnodes)
|
|
43
|
+
self._source = source
|
|
44
|
+
self._flags = flags or set()
|
|
45
|
+
|
|
46
|
+
self._hooks: dict[Any, Callable[..., Awaitable[Any]] | None] = {}
|
|
47
|
+
self.on_node_attached_hook = on_node_attached_hook
|
|
48
|
+
self.on_node_detached_hook = on_node_detached_hook
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def flags(self) -> frozenset[Any]:
|
|
52
|
+
return frozenset(self._flags)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def hooks(self) -> Mapping[Any, Callable[..., Awaitable[Any]] | None]:
|
|
56
|
+
return MappingProxyType(self._hooks)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def id(self) -> str:
|
|
60
|
+
return self._id
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def name(self) -> str:
|
|
64
|
+
return self._name
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def description(self) -> str:
|
|
68
|
+
return self._description
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def parent(self) -> Node | None:
|
|
72
|
+
return self._parent
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def subnodes(self) -> Mapping[str, Node]:
|
|
76
|
+
return self._subnodes_proxy
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def root(self) -> Node:
|
|
80
|
+
node = self
|
|
81
|
+
while node.parent is not None:
|
|
82
|
+
node = node.parent
|
|
83
|
+
return node
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def path(self) -> tuple[str, ...]:
|
|
87
|
+
path = [i.id for i in self.chain_to_root()]
|
|
88
|
+
path.reverse()
|
|
89
|
+
return tuple(path)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def source(self) -> ConfigSource | None:
|
|
93
|
+
return self._source
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def inherited_source(self) -> ConfigSource | None:
|
|
97
|
+
if self.source is not None:
|
|
98
|
+
return self._source
|
|
99
|
+
if self.parent is not None:
|
|
100
|
+
return self.parent.inherited_source
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def on_node_attached_hook(self) -> ON_NODE_ATTACHED_HOOK | None:
|
|
105
|
+
return self._hooks.get(BaseHookTypes.ON_NODE_ATTACHED)
|
|
106
|
+
|
|
107
|
+
@on_node_attached_hook.setter
|
|
108
|
+
def on_node_attached_hook(self, hook: ON_NODE_ATTACHED_HOOK | None) -> None:
|
|
109
|
+
self._hooks[BaseHookTypes.ON_NODE_ATTACHED] = hook
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def on_node_detached_hook(self) -> ON_NODE_DETACHED_HOOK | None:
|
|
113
|
+
return self._hooks.get(BaseHookTypes.ON_NODE_DETACHED)
|
|
114
|
+
|
|
115
|
+
@on_node_detached_hook.setter
|
|
116
|
+
def on_node_detached_hook(self, hook: ON_NODE_DETACHED_HOOK | None) -> None:
|
|
117
|
+
self._hooks[BaseHookTypes.ON_NODE_DETACHED] = hook
|
|
118
|
+
|
|
119
|
+
def _attach_node(self, node: T) -> T:
|
|
120
|
+
self.check_can_attach_node(node)
|
|
121
|
+
node._parent = self
|
|
122
|
+
self._subnodes[node.id] = node
|
|
123
|
+
return node
|
|
124
|
+
|
|
125
|
+
async def attach_node(self, node: T, run_hook: bool = True) -> T:
|
|
126
|
+
node = self._attach_node(node)
|
|
127
|
+
if run_hook:
|
|
128
|
+
await self.run_hook(BaseHookTypes.ON_NODE_ATTACHED, node, self)
|
|
129
|
+
return node
|
|
130
|
+
|
|
131
|
+
@overload
|
|
132
|
+
def _detach_node(self, node: str) -> Node: ...
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
def _detach_node(self, node: T) -> T: ...
|
|
136
|
+
|
|
137
|
+
def _detach_node(self, node: T | str) -> T | Node:
|
|
138
|
+
node_id = node if isinstance(node, str) else node.id
|
|
139
|
+
if node_id not in self.subnodes:
|
|
140
|
+
raise KeyError(f'Node {self.path} has no subnode with id {node_id}.')
|
|
141
|
+
|
|
142
|
+
detached_node = self._subnodes.pop(node_id)
|
|
143
|
+
detached_node._parent = None
|
|
144
|
+
return detached_node
|
|
145
|
+
|
|
146
|
+
@overload
|
|
147
|
+
async def detach_node(self, node: str, run_hook: bool = True) -> Node: ...
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
async def detach_node(self, node: T, run_hook: bool = True) -> T: ...
|
|
151
|
+
|
|
152
|
+
async def detach_node(self, node: T | str, run_hook: bool = True) -> T | Node:
|
|
153
|
+
detached_node = self._detach_node(node)
|
|
154
|
+
if run_hook:
|
|
155
|
+
await self.run_hook(BaseHookTypes.ON_NODE_DETACHED, detached_node, self)
|
|
156
|
+
return detached_node
|
|
157
|
+
|
|
158
|
+
def get_node_info(self, same_source_only: bool = True) -> NodeInfo:
|
|
159
|
+
return NodeInfo(
|
|
160
|
+
id=self.id,
|
|
161
|
+
name=self.name,
|
|
162
|
+
description=self.description,
|
|
163
|
+
type=NodeType.CONTAINER,
|
|
164
|
+
subnodes={
|
|
165
|
+
k: i.get_node_info(same_source_only=same_source_only)
|
|
166
|
+
for k, i in self.subnodes.items()
|
|
167
|
+
if (same_source_only and self.inherited_source == i.inherited_source)
|
|
168
|
+
or not same_source_only
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def check_can_be_attached(self) -> None:
|
|
173
|
+
if self._parent is not None:
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
f'Node {self.path} already has a parent and cannot be attached to another node.',
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def check_can_attach_node(self, node: Node) -> None:
|
|
179
|
+
if not self._allow_children:
|
|
180
|
+
raise LeafNodeError(f'Node of type {type(self)} cannot contain subnodes.')
|
|
181
|
+
|
|
182
|
+
node.check_can_be_attached()
|
|
183
|
+
|
|
184
|
+
if node is self:
|
|
185
|
+
raise NodeLoopError('Node cannot be attached to itself.')
|
|
186
|
+
|
|
187
|
+
if node.id in self.subnodes:
|
|
188
|
+
raise NodeDuplicateError(
|
|
189
|
+
f'Node {self.path} already contains a subnode with id {node.id}.',
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for i in node.chain_to_tails():
|
|
193
|
+
if i is self:
|
|
194
|
+
raise NodeLoopError('Node loop.') # todo
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
def chain_to_root(self) -> Generator[Node, None, None]:
|
|
198
|
+
node: Node | None = self
|
|
199
|
+
while node is not None:
|
|
200
|
+
yield node
|
|
201
|
+
node = node.parent
|
|
202
|
+
|
|
203
|
+
def chain_to_tails(self) -> Generator[Node, None, None]:
|
|
204
|
+
yield self
|
|
205
|
+
for i in self.subnodes.values():
|
|
206
|
+
yield from i.chain_to_tails()
|
|
207
|
+
|
|
208
|
+
def is_child_of(self, node: Node | Sequence[str], direct: bool = True) -> bool:
|
|
209
|
+
path = node.path if isinstance(node, Node) else tuple(node)
|
|
210
|
+
self_path = self.path
|
|
211
|
+
|
|
212
|
+
if direct:
|
|
213
|
+
if len(self_path) != len(path) + 1:
|
|
214
|
+
return False
|
|
215
|
+
else:
|
|
216
|
+
if len(self_path) <= len(path):
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
return self_path[: len(path)] == path
|
|
220
|
+
|
|
221
|
+
def is_parent_of(self, node: Node | Sequence[str], direct: bool = True) -> bool:
|
|
222
|
+
path = node.path if isinstance(node, Node) else node
|
|
223
|
+
self_path = self.path
|
|
224
|
+
|
|
225
|
+
if direct:
|
|
226
|
+
if len(self_path) != len(path) - 1:
|
|
227
|
+
return False
|
|
228
|
+
else:
|
|
229
|
+
if len(self_path) >= len(path):
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
return path[: len(self_path)] == self_path
|
|
233
|
+
|
|
234
|
+
async def save(self, same_source_only: bool = True) -> None:
|
|
235
|
+
if self.source is None:
|
|
236
|
+
if not self.inherited_source:
|
|
237
|
+
raise NoSourceError(f'Cannot save node {self.path}: source not specified.')
|
|
238
|
+
for i in self.chain_to_root():
|
|
239
|
+
if i.source is not None:
|
|
240
|
+
return await i.save(same_source_only=same_source_only)
|
|
241
|
+
else:
|
|
242
|
+
node_info = self.get_node_info()
|
|
243
|
+
return await self.source.save(data=node_info)
|
|
244
|
+
|
|
245
|
+
async def load(self, validate: bool = True, run_hook: bool = False) -> None:
|
|
246
|
+
if self.source is None:
|
|
247
|
+
raise NoSourceError(f'Cannot load node {self.path}: source not specified.')
|
|
248
|
+
|
|
249
|
+
data = await self.source.load()
|
|
250
|
+
await self.load_from_dict(data, validate=validate, run_hook=run_hook)
|
|
251
|
+
|
|
252
|
+
async def load_from_dict(
|
|
253
|
+
self,
|
|
254
|
+
data_dict: dict[str, Any],
|
|
255
|
+
validate: bool = True,
|
|
256
|
+
run_hook: bool = False,
|
|
257
|
+
) -> None:
|
|
258
|
+
for k, data in data_dict.items():
|
|
259
|
+
if k not in self.subnodes:
|
|
260
|
+
continue
|
|
261
|
+
node = self.subnodes[k]
|
|
262
|
+
await node.load_from_dict(
|
|
263
|
+
data_dict=data_dict[k],
|
|
264
|
+
validate=validate,
|
|
265
|
+
run_hook=run_hook,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
async def run_hook(self, hook_identifier: Any, *args: Any, **kwargs: Any) -> Any:
|
|
269
|
+
hook = self.hooks.get(hook_identifier)
|
|
270
|
+
if hook is not None:
|
|
271
|
+
await hook(*args, **kwargs)
|
|
272
|
+
if self.parent is not None:
|
|
273
|
+
await self.parent.run_hook(hook_identifier, *args, **kwargs)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'PyConfigTreeError',
|
|
3
|
+
'ValidationError',
|
|
4
|
+
'SerializationError',
|
|
5
|
+
'DeserializationError',
|
|
6
|
+
'NoSourceError',
|
|
7
|
+
'NodeLoopError',
|
|
8
|
+
'NodeDuplicateError',
|
|
9
|
+
'LeafNodeError',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PyConfigTreeError(Exception): ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationError(PyConfigTreeError): ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SerializationError(PyConfigTreeError): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeserializationError(PyConfigTreeError): ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NoSourceError(PyConfigTreeError): ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NodeLoopError(PyConfigTreeError): ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NodeDuplicateError(PyConfigTreeError): ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LeafNodeError(PyConfigTreeError): ...
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .base import (
|
|
2
|
+
Parameter as Parameter,
|
|
3
|
+
MutableParameter as MutableParameter,
|
|
4
|
+
)
|
|
5
|
+
from .int_parameter import IntParameter as IntParameter
|
|
6
|
+
from .bool_parameter import BoolParameter as BoolParameter
|
|
7
|
+
from .float_parameter import FloatParameter as FloatParameter
|
|
8
|
+
from .choice_parameter import Choice as Choice
|
|
9
|
+
from .string_parameter import StringParameter as StringParameter
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'ParameterHookTypes',
|
|
3
|
+
'Serializer',
|
|
4
|
+
'Deserializer',
|
|
5
|
+
'Validator',
|
|
6
|
+
'Parameter',
|
|
7
|
+
'MutableParameter',
|
|
8
|
+
'TypedParameter',
|
|
9
|
+
'ON_PARAMETER_VALUE_CHANGED_HOOK',
|
|
10
|
+
'_MutableParameterKwargs',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from typing import Any, Type, Generic, TypeVar, Protocol, TypeAlias
|
|
15
|
+
from enum import Enum, auto
|
|
16
|
+
from asyncio import Lock
|
|
17
|
+
from collections.abc import Callable, Awaitable
|
|
18
|
+
|
|
19
|
+
from typing_extensions import Self, Unpack, Required, TypedDict, NotRequired
|
|
20
|
+
|
|
21
|
+
from pyconfigtree.exceptions import ValidationError, DeserializationError
|
|
22
|
+
|
|
23
|
+
from ..base import Node
|
|
24
|
+
from ..source.base import ALLOWED_TYPES, NodeInfo, NodeType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
ON_PARAMETER_VALUE_CHANGED_HOOK: TypeAlias = Callable[['MutableParameter[Any]'], Awaitable[Any]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ParameterHookTypes(Enum):
|
|
31
|
+
PARAMETER_VALUE_CHANGED = auto()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_VALUE_contra = TypeVar('_VALUE_contra', contravariant=True)
|
|
35
|
+
_VALUE_co = TypeVar('_VALUE_co', covariant=True)
|
|
36
|
+
_NODE = TypeVar('_NODE', contravariant=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Serializer(Protocol[_NODE, _VALUE_contra]):
|
|
40
|
+
def __call__(self, node: _NODE, value: _VALUE_contra) -> ALLOWED_TYPES: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Deserializer(Protocol[_NODE, _VALUE_co]):
|
|
44
|
+
def __call__(self, node: _NODE, value: ALLOWED_TYPES) -> _VALUE_co: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Validator(Protocol[_NODE, _VALUE_contra]):
|
|
48
|
+
async def __call__(self, node: _NODE, value: _VALUE_contra) -> None: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
T = TypeVar('T')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Parameter(Node, Generic[T]):
|
|
55
|
+
_allow_children = False
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
node_id: str,
|
|
60
|
+
value: T,
|
|
61
|
+
name: str = '',
|
|
62
|
+
description: str = '',
|
|
63
|
+
) -> None:
|
|
64
|
+
super().__init__(node_id=node_id, name=name, description=description)
|
|
65
|
+
self._value = value
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def value(self) -> T:
|
|
69
|
+
return self._value
|
|
70
|
+
|
|
71
|
+
async def load_from_dict(
|
|
72
|
+
self,
|
|
73
|
+
data_dict: dict[str, Any],
|
|
74
|
+
validate: bool = True,
|
|
75
|
+
run_hook: bool = False,
|
|
76
|
+
) -> None:
|
|
77
|
+
# Parameter is immutable and its value cannot be set.
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_VALUE_TYPE = TypeVar('_VALUE_TYPE') # Parameter value type
|
|
82
|
+
_PARAM_CLASS = TypeVar('_PARAM_CLASS') # Parameter class
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _CommonMutableParameterKwargs(TypedDict, Generic[_PARAM_CLASS, _VALUE_TYPE]):
|
|
86
|
+
name: NotRequired[str]
|
|
87
|
+
description: NotRequired[str]
|
|
88
|
+
value: NotRequired[_VALUE_TYPE | None]
|
|
89
|
+
default_value: NotRequired[_VALUE_TYPE | None]
|
|
90
|
+
default_factory: NotRequired[Callable[[], _VALUE_TYPE] | None]
|
|
91
|
+
validator: NotRequired[Validator[_PARAM_CLASS, _VALUE_TYPE] | None]
|
|
92
|
+
on_value_changed_hook: NotRequired[ON_PARAMETER_VALUE_CHANGED_HOOK | None]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _MutableParameterKwargs(
|
|
96
|
+
_CommonMutableParameterKwargs[_PARAM_CLASS, _VALUE_TYPE], Generic[_PARAM_CLASS, _VALUE_TYPE]
|
|
97
|
+
):
|
|
98
|
+
serializer: Required[Serializer[_PARAM_CLASS, _VALUE_TYPE]]
|
|
99
|
+
deserializer: Required[Deserializer[_PARAM_CLASS, _VALUE_TYPE]]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class _TypedParameterKwargs(
|
|
103
|
+
_CommonMutableParameterKwargs[_PARAM_CLASS, _VALUE_TYPE], Generic[_PARAM_CLASS, _VALUE_TYPE]
|
|
104
|
+
):
|
|
105
|
+
serializer: NotRequired[Serializer[_PARAM_CLASS, _VALUE_TYPE]]
|
|
106
|
+
deserializer: NotRequired[Deserializer[_PARAM_CLASS, _VALUE_TYPE]]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class MutableParameter(Parameter[T], Generic[T]):
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
node_id: str,
|
|
113
|
+
*,
|
|
114
|
+
name: str = '',
|
|
115
|
+
description: str = '',
|
|
116
|
+
value: T | None = None,
|
|
117
|
+
default_value: T | None = None,
|
|
118
|
+
default_factory: Callable[[], T] | None = None,
|
|
119
|
+
validator: Validator[Self, T] | None = None,
|
|
120
|
+
serializer: Serializer[Self, T],
|
|
121
|
+
deserializer: Deserializer[Self, T],
|
|
122
|
+
on_value_changed_hook: ON_PARAMETER_VALUE_CHANGED_HOOK | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
if value is None and default_value is None:
|
|
125
|
+
raise ValueError('Either `default_value` or `default_factory` must be specified.')
|
|
126
|
+
if value is not None and default_factory is not None:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
'Either `default_value` or `default_factory` must be specified, '
|
|
129
|
+
'but not both of them.',
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self._default_factory = default_factory
|
|
133
|
+
self._default_value = default_value
|
|
134
|
+
self._changing_lock = Lock()
|
|
135
|
+
self._validator = validator
|
|
136
|
+
self._serializer = serializer
|
|
137
|
+
self._deserializer = deserializer
|
|
138
|
+
|
|
139
|
+
super().__init__(
|
|
140
|
+
node_id=node_id,
|
|
141
|
+
value=value if value is not None else self.default_value,
|
|
142
|
+
name=name,
|
|
143
|
+
description=description,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.on_value_changed_hook = on_value_changed_hook
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def default_value(self) -> T:
|
|
150
|
+
if self._default_factory is not None:
|
|
151
|
+
return self._default_factory()
|
|
152
|
+
return self._default_value
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def serializer(self) -> Serializer[Self, T]:
|
|
156
|
+
return self._serializer
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def deserializer(self) -> Deserializer[Self, T]:
|
|
160
|
+
return self._deserializer
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def validator(self) -> Validator[Self, T] | None:
|
|
164
|
+
return self._validator
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def on_value_changed_hook(self) -> ON_PARAMETER_VALUE_CHANGED_HOOK | None:
|
|
168
|
+
return self.hooks.get(ParameterHookTypes.PARAMETER_VALUE_CHANGED)
|
|
169
|
+
|
|
170
|
+
@on_value_changed_hook.setter
|
|
171
|
+
def on_value_changed_hook(self, hook: ON_PARAMETER_VALUE_CHANGED_HOOK | None) -> None:
|
|
172
|
+
self._hooks[ParameterHookTypes.PARAMETER_VALUE_CHANGED] = hook
|
|
173
|
+
|
|
174
|
+
def get_node_info(self, same_source_only: bool = True) -> NodeInfo:
|
|
175
|
+
return NodeInfo(
|
|
176
|
+
id=self.id,
|
|
177
|
+
name=self.name,
|
|
178
|
+
description=self.description,
|
|
179
|
+
type=NodeType.LEAF,
|
|
180
|
+
value=self.serialize(),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def load_from_dict(
|
|
184
|
+
self,
|
|
185
|
+
data_dict: Any,
|
|
186
|
+
validate: bool = True,
|
|
187
|
+
run_hook: bool = False,
|
|
188
|
+
) -> None:
|
|
189
|
+
await self.set_value(
|
|
190
|
+
data_dict,
|
|
191
|
+
save=False,
|
|
192
|
+
run_hook=run_hook,
|
|
193
|
+
validate=validate,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def set_value(
|
|
197
|
+
self,
|
|
198
|
+
value: Any,
|
|
199
|
+
*,
|
|
200
|
+
deserialize: bool = True,
|
|
201
|
+
validate: bool = True,
|
|
202
|
+
run_hook: bool = True,
|
|
203
|
+
save: bool = True,
|
|
204
|
+
) -> None:
|
|
205
|
+
async with self._changing_lock:
|
|
206
|
+
if deserialize:
|
|
207
|
+
value = self.deserialize(value)
|
|
208
|
+
if validate:
|
|
209
|
+
await self.validate(value)
|
|
210
|
+
|
|
211
|
+
self._value = value
|
|
212
|
+
if save:
|
|
213
|
+
await self.save()
|
|
214
|
+
|
|
215
|
+
if run_hook:
|
|
216
|
+
await self.run_hook(ParameterHookTypes.PARAMETER_VALUE_CHANGED, self)
|
|
217
|
+
|
|
218
|
+
def serialize(self) -> ALLOWED_TYPES:
|
|
219
|
+
return self.serializer(self, self.value)
|
|
220
|
+
|
|
221
|
+
def deserialize(self, value: Any) -> T:
|
|
222
|
+
return self.deserializer(self, value)
|
|
223
|
+
|
|
224
|
+
async def validate(self, value: T) -> None:
|
|
225
|
+
if self.validator is not None:
|
|
226
|
+
await self.validator(self, value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
TT = TypeVar('TT')
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TypedParameter(MutableParameter[TT], Generic[TT]):
|
|
233
|
+
_DEFAULT_SERIALIZER: Serializer[Self, TT]
|
|
234
|
+
_DEFAULT_DESERIALIZER: Deserializer[Self, TT]
|
|
235
|
+
_VALUE_TYPE: Type[TT]
|
|
236
|
+
|
|
237
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
238
|
+
if cls is TypedParameter:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
for i in [
|
|
242
|
+
'_DEFAULT_SERIALIZER',
|
|
243
|
+
'_DEFAULT_DESERIALIZER',
|
|
244
|
+
'_VALUE_TYPE',
|
|
245
|
+
]:
|
|
246
|
+
if i not in cls.__dict__:
|
|
247
|
+
raise TypeError(f'`{cls.__name__}` must define `{i}`.')
|
|
248
|
+
|
|
249
|
+
def __init__(self, node_id: str, **kwargs: Unpack[_TypedParameterKwargs[Self, TT]]) -> None:
|
|
250
|
+
super().__init__(node_id=node_id, **kwargs)
|
|
251
|
+
|
|
252
|
+
def deserialize(self, value: Any) -> TT:
|
|
253
|
+
res = super().deserialize(value)
|
|
254
|
+
if not isinstance(res, self._VALUE_TYPE):
|
|
255
|
+
raise DeserializationError(
|
|
256
|
+
f'Deserialized value of `{self.__class__.__name__}` must be an instance of '
|
|
257
|
+
f'`{self._VALUE_TYPE.__name__}`, not `{type(res)}`.',
|
|
258
|
+
)
|
|
259
|
+
return res
|
|
260
|
+
|
|
261
|
+
async def validate(self, value: Any) -> None:
|
|
262
|
+
if not isinstance(value, self._VALUE_TYPE):
|
|
263
|
+
raise ValidationError(
|
|
264
|
+
f'Value of `{self.__class__.__name__}` must be an instance of '
|
|
265
|
+
f'`{self._VALUE_TYPE.__name__}`, not `{type(value)}`.',
|
|
266
|
+
)
|
|
267
|
+
return await super().validate(value)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'bool_serializer',
|
|
6
|
+
'bool_deserializer',
|
|
7
|
+
'BoolParameter',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .base import TypedParameter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def bool_serializer(node: 'BoolParameter', value: bool) -> bool:
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bool_deserializer(node: 'BoolParameter', value: Any) -> bool:
|
|
21
|
+
return bool(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BoolParameter(TypedParameter[bool]):
|
|
25
|
+
_DEFAULT_SERIALIZER = staticmethod(bool_serializer)
|
|
26
|
+
_DEFAULT_DESERIALIZER = staticmethod(bool_deserializer)
|
|
27
|
+
_VALUE_TYPE = bool
|
|
28
|
+
|
|
29
|
+
async def on(self, save: bool = True, run_hook: bool = True, validate: bool = True) -> None:
|
|
30
|
+
await self.set_value(
|
|
31
|
+
True,
|
|
32
|
+
deserialize=False,
|
|
33
|
+
validate=validate,
|
|
34
|
+
run_hook=run_hook,
|
|
35
|
+
save=save,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def off(self, save: bool = True, run_hook: bool = True, validate: bool = True) -> None:
|
|
39
|
+
await self.set_value(
|
|
40
|
+
False,
|
|
41
|
+
deserialize=False,
|
|
42
|
+
validate=validate,
|
|
43
|
+
run_hook=run_hook,
|
|
44
|
+
save=save,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def toggle(
|
|
48
|
+
self, save: bool = True, run_hook: bool = True, validate: bool = True
|
|
49
|
+
) -> None:
|
|
50
|
+
await self.set_value(
|
|
51
|
+
not self.value,
|
|
52
|
+
deserialize=False,
|
|
53
|
+
validate=validate,
|
|
54
|
+
run_hook=run_hook,
|
|
55
|
+
save=save,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def next_value(
|
|
59
|
+
self, save: bool = True, run_hook: bool = True, validate: bool = True
|
|
60
|
+
) -> bool:
|
|
61
|
+
await self.toggle(save=save, run_hook=run_hook, validate=validate)
|
|
62
|
+
return self.value
|