pyconfigtree 0.1.0__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.
- pyconfigtree/__init__.py +3 -0
- pyconfigtree/base.py +273 -0
- pyconfigtree/exceptions.py +34 -0
- pyconfigtree/parameter/__init__.py +9 -0
- pyconfigtree/parameter/base.py +267 -0
- pyconfigtree/parameter/bool_parameter.py +62 -0
- pyconfigtree/parameter/choice_parameter.py +92 -0
- pyconfigtree/parameter/float_parameter.py +27 -0
- pyconfigtree/parameter/int_parameter.py +27 -0
- pyconfigtree/parameter/list_parameter.py +143 -0
- pyconfigtree/parameter/string_parameter.py +27 -0
- pyconfigtree/py.typed +0 -0
- pyconfigtree/source/__init__.py +2 -0
- pyconfigtree/source/base.py +42 -0
- pyconfigtree/source/json.py +41 -0
- pyconfigtree/source/toml.py +43 -0
- pyconfigtree-0.1.0.dist-info/METADATA +8 -0
- pyconfigtree-0.1.0.dist-info/RECORD +19 -0
- pyconfigtree-0.1.0.dist-info/WHEEL +4 -0
pyconfigtree/__init__.py
ADDED
pyconfigtree/base.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any, Generic, TypeVar
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Self, Unpack
|
|
7
|
+
|
|
8
|
+
from .base import TypedParameter, _MutableParameterKwargs
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = TypeVar('T')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(kw_only=True, frozen=True)
|
|
15
|
+
class Choice(Generic[T]):
|
|
16
|
+
id: str
|
|
17
|
+
name: str = ''
|
|
18
|
+
description: str = ''
|
|
19
|
+
value: T
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if not self.id:
|
|
23
|
+
raise ValueError('Choice ID cannot be empty.')
|
|
24
|
+
|
|
25
|
+
if not isinstance(id, str):
|
|
26
|
+
raise TypeError('Choice ID must be a string.')
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return self.id
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def choice_serializer(node: 'ChoiceParameter[Any]', value: Choice[Any]) -> str:
|
|
33
|
+
return value.id
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def choice_deserializer(node: 'ChoiceParameter[Any]', value: Any) -> Choice[Any]:
|
|
37
|
+
value = str(value)
|
|
38
|
+
return node.choices.get(value, node.choices[node.fallback_choice_id])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ChoiceParameter(TypedParameter[Choice[T]], Generic[T]):
|
|
42
|
+
_DEFAULT_SERIALIZER = choice_serializer
|
|
43
|
+
_DEFAULT_DESERIALIZER = choice_deserializer
|
|
44
|
+
_VALUE_TYPE = Choice
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
node_id: str,
|
|
49
|
+
*,
|
|
50
|
+
choices: Sequence[Choice[T]],
|
|
51
|
+
fallback_choice_id: str,
|
|
52
|
+
**kwargs: Unpack[_MutableParameterKwargs[Self, Choice[T]]],
|
|
53
|
+
) -> None:
|
|
54
|
+
if not choices:
|
|
55
|
+
raise ValueError('At least 1 choice must be provided.')
|
|
56
|
+
|
|
57
|
+
self._choices: dict[str, Choice[T]] = {}
|
|
58
|
+
for i in choices:
|
|
59
|
+
if i.id in self._choices:
|
|
60
|
+
raise ValueError('Duplicate choice ID.') # todo
|
|
61
|
+
self._choices[i.id] = i
|
|
62
|
+
|
|
63
|
+
if fallback_choice_id not in self._choices:
|
|
64
|
+
raise ValueError('Fallback choice ID does not exists.')
|
|
65
|
+
self._fallback_choice_id = fallback_choice_id
|
|
66
|
+
|
|
67
|
+
super().__init__(node_id=node_id, **kwargs)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def choices(self) -> Mapping[str, Choice[T]]:
|
|
71
|
+
return MappingProxyType(self._choices)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def fallback_choice_id(self) -> str:
|
|
75
|
+
return self._fallback_choice_id
|
|
76
|
+
|
|
77
|
+
async def set_value(
|
|
78
|
+
self,
|
|
79
|
+
value: Choice[T] | str,
|
|
80
|
+
*,
|
|
81
|
+
deserialize: bool = True,
|
|
82
|
+
validate: bool = True,
|
|
83
|
+
run_hook: bool = True,
|
|
84
|
+
save: bool = True,
|
|
85
|
+
) -> None:
|
|
86
|
+
choice = self.choices.get(value) if isinstance(value, str) else value
|
|
87
|
+
if choice not in self.choices.values():
|
|
88
|
+
raise ValueError('Invalid choice.')
|
|
89
|
+
|
|
90
|
+
await super().set_value(
|
|
91
|
+
choice, deserialize=deserialize, validate=validate, run_hook=run_hook, save=save
|
|
92
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'float_serializer',
|
|
6
|
+
'float_deserializer',
|
|
7
|
+
'FloatParameter',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .base import TypedParameter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def float_serializer(node: FloatParameter, value: float) -> float:
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def float_deserializer(node: FloatParameter, value: Any) -> float:
|
|
21
|
+
return float(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FloatParameter(TypedParameter[float]):
|
|
25
|
+
_DEFAULT_SERIALIZER = staticmethod(float_serializer)
|
|
26
|
+
_DEFAULT_DESERIALIZER = staticmethod(float_deserializer)
|
|
27
|
+
_VALUE_TYPE = float
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'int_serializer',
|
|
6
|
+
'int_deserializer',
|
|
7
|
+
'IntParameter',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .base import TypedParameter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def int_serializer(node: IntParameter, value: int) -> int:
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def int_deserializer(node: IntParameter, value: Any) -> int:
|
|
21
|
+
return int(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IntParameter(TypedParameter[int]):
|
|
25
|
+
_DEFAULT_SERIALIZER = staticmethod(int_serializer)
|
|
26
|
+
_DEFAULT_DESERIALIZER = staticmethod(int_deserializer)
|
|
27
|
+
_VALUE_TYPE = int
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
from json import JSONDecodeError
|
|
6
|
+
from collections.abc import Callable, Iterable, Awaitable
|
|
7
|
+
|
|
8
|
+
from typing_extensions import Self, Unpack
|
|
9
|
+
|
|
10
|
+
from pyconfigtree.exceptions import DeserializationError
|
|
11
|
+
|
|
12
|
+
from .base import ALLOWED_TYPES, ParameterHookTypes, _TypedParameterKwargs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
'list_serializer',
|
|
17
|
+
'list_deserializer',
|
|
18
|
+
'ListParameter',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from pyconfigtree.exceptions import SerializationError
|
|
25
|
+
|
|
26
|
+
from .base import TypedParameter
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def list_serializer(value: list[Any]) -> list[ALLOWED_TYPES]:
|
|
30
|
+
result: list[ALLOWED_TYPES] = []
|
|
31
|
+
for i in value:
|
|
32
|
+
if isinstance(i, (int, str, float, bool)):
|
|
33
|
+
result.append(i)
|
|
34
|
+
elif isinstance(i, list):
|
|
35
|
+
result.append(list_serializer(i))
|
|
36
|
+
else:
|
|
37
|
+
raise SerializationError(f'Unable to serialize list item {i!r}.')
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def list_deserializer(value: Any) -> list[Any]:
|
|
42
|
+
if isinstance(value, str):
|
|
43
|
+
try:
|
|
44
|
+
value = json.loads(value)
|
|
45
|
+
except JSONDecodeError:
|
|
46
|
+
raise DeserializationError(f'Unable to convert string {value!r} to list.')
|
|
47
|
+
|
|
48
|
+
if isinstance(value, Iterable):
|
|
49
|
+
return [str(i) if not isinstance(i, (int, str, float, bool)) else i for i in value]
|
|
50
|
+
|
|
51
|
+
raise DeserializationError(f'Unable to convert {value!r} to list of strings.')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
T = TypeVar('T')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ListParameter(TypedParameter[list[T]], Generic[T]):
|
|
58
|
+
_DEFAULT_SERIALIZER = staticmethod(list_serializer)
|
|
59
|
+
_DEFAULT_DESERIALIZER = staticmethod(list_deserializer)
|
|
60
|
+
_VALUE_TYPE = list
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
node_id: str,
|
|
65
|
+
*,
|
|
66
|
+
item_deserializer: Callable[[Any], T] | None = None,
|
|
67
|
+
add_item_validator: Callable[[T, Self], Awaitable[None]] | None = None,
|
|
68
|
+
remove_item_validator: Callable[[T, Self], Awaitable[None]] | None = None,
|
|
69
|
+
**kwargs: Unpack[_TypedParameterKwargs[Self, list[T]]],
|
|
70
|
+
) -> None:
|
|
71
|
+
super().__init__(node_id=node_id, **kwargs)
|
|
72
|
+
self.item_deserializer = item_deserializer
|
|
73
|
+
self.add_item_validator = add_item_validator
|
|
74
|
+
self.remove_item_validator = remove_item_validator
|
|
75
|
+
|
|
76
|
+
async def add_item(
|
|
77
|
+
self,
|
|
78
|
+
item: T,
|
|
79
|
+
deserialize: bool = True,
|
|
80
|
+
validate: bool = True,
|
|
81
|
+
run_hook: bool = True,
|
|
82
|
+
save: bool = True,
|
|
83
|
+
) -> None:
|
|
84
|
+
async with self._changing_lock:
|
|
85
|
+
if deserialize:
|
|
86
|
+
item = await self.deserialize_item(item)
|
|
87
|
+
if validate:
|
|
88
|
+
await self.add_item_validate(item)
|
|
89
|
+
|
|
90
|
+
self._value.append(item)
|
|
91
|
+
if save:
|
|
92
|
+
await self.save()
|
|
93
|
+
|
|
94
|
+
if run_hook:
|
|
95
|
+
await self.run_hook(ParameterHookTypes.PARAMETER_VALUE_CHANGED, self)
|
|
96
|
+
|
|
97
|
+
async def pop_item(
|
|
98
|
+
self, index: int, validate: bool = True, run_hook: bool = True, save: bool = True
|
|
99
|
+
) -> T | None:
|
|
100
|
+
if index < 0 or index >= len(self.value):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
async with self._changing_lock:
|
|
104
|
+
if validate:
|
|
105
|
+
item = self._value[index]
|
|
106
|
+
await self.remove_item_validate(item)
|
|
107
|
+
|
|
108
|
+
result = self._value.pop(index)
|
|
109
|
+
if save:
|
|
110
|
+
await self.save()
|
|
111
|
+
|
|
112
|
+
if run_hook:
|
|
113
|
+
await self.run_hook(ParameterHookTypes.PARAMETER_VALUE_CHANGED, self)
|
|
114
|
+
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
async def remove_item(
|
|
118
|
+
self, item: T, validate: bool = True, run_hook: bool = True, save: bool = True
|
|
119
|
+
) -> None:
|
|
120
|
+
if item not in self._value:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
async with self._changing_lock:
|
|
124
|
+
if validate:
|
|
125
|
+
await self.remove_item_validate(item)
|
|
126
|
+
|
|
127
|
+
self._value.remove(item)
|
|
128
|
+
if save:
|
|
129
|
+
await self.save()
|
|
130
|
+
|
|
131
|
+
if run_hook:
|
|
132
|
+
await self.run_hook(ParameterHookTypes.PARAMETER_VALUE_CHANGED, self)
|
|
133
|
+
|
|
134
|
+
async def add_item_validate(self, item: T) -> None:
|
|
135
|
+
if self.add_item_validator is not None:
|
|
136
|
+
await self.add_item_validator(item, self)
|
|
137
|
+
|
|
138
|
+
async def remove_item_validate(self, item: T) -> None:
|
|
139
|
+
if self.remove_item_validator is not None:
|
|
140
|
+
await self.remove_item_validator(item, self)
|
|
141
|
+
|
|
142
|
+
def deserialize_item(self, item: Any) -> T:
|
|
143
|
+
return item if self.item_deserializer is None else self.item_deserializer(item)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
'str_serializer',
|
|
6
|
+
'str_deserializer',
|
|
7
|
+
'StringParameter',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .base import TypedParameter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def str_serializer(node: StringParameter, value: str) -> str:
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def str_deserializer(node: StringParameter, value: Any) -> str:
|
|
21
|
+
return str(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StringParameter(TypedParameter[str]):
|
|
25
|
+
_DEFAULT_SERIALIZER = staticmethod(str_serializer)
|
|
26
|
+
_DEFAULT_DESERIALIZER = staticmethod(str_deserializer)
|
|
27
|
+
_VALUE_TYPE = str
|
pyconfigtree/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, Union, TypeAlias
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
from dataclasses import field, dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SIMPLE_TYPES: TypeAlias = int | float | bool | str
|
|
8
|
+
CONTAINER_TYPES: TypeAlias = (
|
|
9
|
+
dict[str, Union[SIMPLE_TYPES, 'CONTAINER_TYPES']]
|
|
10
|
+
| list[Union[SIMPLE_TYPES, 'CONTAINER_TYPES']]
|
|
11
|
+
)
|
|
12
|
+
ALLOWED_TYPES: TypeAlias = SIMPLE_TYPES | CONTAINER_TYPES
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigSource(ABC):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def load(self) -> dict[str, Any]: ...
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def save(self, data: 'NodeInfo') -> None: ...
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def __eq__(self, other: Any) -> bool: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NodeType(Enum):
|
|
27
|
+
CONTAINER = auto()
|
|
28
|
+
LEAF = auto()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class NodeInfo:
|
|
33
|
+
id: str
|
|
34
|
+
name: str
|
|
35
|
+
description: str
|
|
36
|
+
type: NodeType
|
|
37
|
+
subnodes: dict[str, 'NodeInfo'] = field(default_factory=dict)
|
|
38
|
+
value: ALLOWED_TYPES | None = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if self.type is NodeType.CONTAINER and self.value is not None:
|
|
42
|
+
raise ValueError(f'Node of type {self.type} cannot contain value.')
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .base import NodeInfo, NodeType, ConfigSource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JSONSource(ConfigSource):
|
|
9
|
+
def __init__(self, path: str | Path, encoding: str = 'utf-8') -> None:
|
|
10
|
+
self._path = Path(path)
|
|
11
|
+
self._encoding = encoding
|
|
12
|
+
|
|
13
|
+
async def load(self) -> dict[str, Any]:
|
|
14
|
+
with open(self._path, 'r', encoding=self.encoding) as f:
|
|
15
|
+
return json.load(f) # type: ignore
|
|
16
|
+
|
|
17
|
+
async def save(self, data: NodeInfo) -> None:
|
|
18
|
+
dicted = self.node_info_to_dict(data)
|
|
19
|
+
with open(self._path, 'w', encoding=self.encoding) as f:
|
|
20
|
+
json.dump(dicted, f, indent=4)
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
def node_info_to_dict(self, node: NodeInfo) -> Any:
|
|
24
|
+
if node.type is NodeType.LEAF:
|
|
25
|
+
return node.value
|
|
26
|
+
return {k: self.node_info_to_dict(v) for k, v in node.subnodes.items()}
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def path(self) -> Path:
|
|
30
|
+
return self._path
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def encoding(self) -> str:
|
|
34
|
+
return self._encoding
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other: Any) -> bool:
|
|
37
|
+
if isinstance(other, JSONSource):
|
|
38
|
+
return self.path == other.path
|
|
39
|
+
if isinstance(other, (str, Path)):
|
|
40
|
+
return self.path == Path(other)
|
|
41
|
+
return False
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import tomli_w
|
|
5
|
+
import tomllib # type: ignore
|
|
6
|
+
|
|
7
|
+
from .base import NodeInfo, NodeType, ConfigSource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TOMLSource(ConfigSource):
|
|
11
|
+
def __init__(self, path: str | Path, encoding: str = 'utf-8') -> None:
|
|
12
|
+
self._path = Path(path)
|
|
13
|
+
self._encoding = encoding
|
|
14
|
+
|
|
15
|
+
async def load(self) -> dict[str, Any]:
|
|
16
|
+
with open(self._path, 'rb') as f:
|
|
17
|
+
return tomllib.load(f) # type: ignore
|
|
18
|
+
|
|
19
|
+
async def save(self, data: NodeInfo) -> None:
|
|
20
|
+
dicted = self.node_info_to_dict(data)
|
|
21
|
+
with open(self._path, 'w', encoding='utf-8') as f:
|
|
22
|
+
f.write(tomli_w.dumps(dicted, multiline_strings=True, indent=4))
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def node_info_to_dict(self, node: NodeInfo) -> Any:
|
|
26
|
+
if node.type is NodeType.LEAF:
|
|
27
|
+
return node.value
|
|
28
|
+
return {k: self.node_info_to_dict(v) for k, v in node.subnodes.items()}
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def path(self) -> Path:
|
|
32
|
+
return self._path
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def encoding(self) -> str:
|
|
36
|
+
return self._encoding
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other: Any) -> bool:
|
|
39
|
+
if isinstance(other, TOMLSource):
|
|
40
|
+
return self.path == other.path
|
|
41
|
+
if isinstance(other, (str, Path)):
|
|
42
|
+
return self.path == Path(other)
|
|
43
|
+
return False
|
|
@@ -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'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
pyconfigtree/__init__.py,sha256=TFRM-hfFZvku9kXrzzHJs8cUXDn6kB2SbodICyhKv48,78
|
|
2
|
+
pyconfigtree/base.py,sha256=1gQG7V803Omw-iAXPuq7IiwQZ0A7qwCBTILx2wUvtIU,8955
|
|
3
|
+
pyconfigtree/exceptions.py,sha256=6WrZE6y9_TsgpTb2DPMC-jEv9r9O0z9GmxUUlkPPhXQ,588
|
|
4
|
+
pyconfigtree/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
pyconfigtree/parameter/__init__.py,sha256=kHEB8nrbMjsQh-zFk5c11Js0Ko3KZeLw44HEK-Hk5Pc,381
|
|
6
|
+
pyconfigtree/parameter/base.py,sha256=lHVjdApfr1kiU8WkcLIg3epecVqWkFjSXziSrntl2R8,8237
|
|
7
|
+
pyconfigtree/parameter/bool_parameter.py,sha256=t9piL_6w_FnXG46KoYFRkOxbwTwzUHYWpjou7GwyyIk,1605
|
|
8
|
+
pyconfigtree/parameter/choice_parameter.py,sha256=YFlawqSRjmjAkpXGEVvxq66dEXsFeilX3v1O39hx0Hg,2644
|
|
9
|
+
pyconfigtree/parameter/float_parameter.py,sha256=WRx-rY2426U9IFoliSOJ9TZ-Regeg9lCglqHgbDYkvM,550
|
|
10
|
+
pyconfigtree/parameter/int_parameter.py,sha256=6UQgFLF6Wcali3TcS60LYaoaN2pvrYMtFWIphU46S34,518
|
|
11
|
+
pyconfigtree/parameter/list_parameter.py,sha256=kZJA0CNq6j2UK92QxkxJQ5iR6sAsgJsAQfOcsCoIAN8,4365
|
|
12
|
+
pyconfigtree/parameter/string_parameter.py,sha256=5uR_RfXm3O3hl-voocM6Pha97TyxuQ96FhzCbyoxHiM,530
|
|
13
|
+
pyconfigtree/source/__init__.py,sha256=7O2UoHfcjlQZKeQkJzxXdh73ovkj2et_yQoqqvndqTc,90
|
|
14
|
+
pyconfigtree/source/base.py,sha256=cSelegHJUkTAGnq1zxLCbkq0OPih4d-HrQ95J6aot0o,1088
|
|
15
|
+
pyconfigtree/source/json.py,sha256=gkOgDid4M7rClD6NhORedtRN79KS0RSPWNHUin9JTl4,1252
|
|
16
|
+
pyconfigtree/source/toml.py,sha256=WlgMvXc8pJfBUNcneA7609ryJ1BVTiT_mIp-cz5MgRI,1295
|
|
17
|
+
pyconfigtree-0.1.0.dist-info/METADATA,sha256=AqmWUG8tXPRW6GC8ugzFqpHJNUrU7gxCSeRQbxpmM-w,292
|
|
18
|
+
pyconfigtree-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
19
|
+
pyconfigtree-0.1.0.dist-info/RECORD,,
|