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.
@@ -0,0 +1,11 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .*
11
+ !.gitignore
@@ -0,0 +1,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -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,3 @@
1
+ from .base import Node as Node
2
+ from .source import *
3
+ from .parameter import *
@@ -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