Coverage for src/configuraptor/cls.py: 100%
60 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 10:50 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-21 10:50 +0200
1"""
2Logic for the TypedConfig inheritable class.
3"""
5import typing
6from collections.abc import Mapping, MutableMapping
7from typing import Any, Iterator
9from .core import T_data, all_annotations, check_type, load_into
10from .errors import ConfigErrorExtraKey, ConfigErrorImmutable, ConfigErrorInvalidType
12C = typing.TypeVar("C", bound=Any)
15class TypedConfig:
16 """
17 Can be used instead of load_into.
18 """
20 @classmethod
21 def load(
22 cls: typing.Type[C],
23 data: T_data = None,
24 key: str = None,
25 init: dict[str, Any] = None,
26 strict: bool = True,
27 lower_keys: bool = False,
28 convert_types: bool = False,
29 ) -> C:
30 """
31 Load a class' config values from the config file.
33 SomeClass.load(data, ...) = load_into(SomeClass, data, ...).
34 """
35 return load_into(
36 cls, data, key=key, init=init, strict=strict, lower_keys=lower_keys, convert_types=convert_types
37 )
39 def _update(self, _strict: bool = True, _allow_none: bool = False, _overwrite: bool = True, **values: Any) -> None:
40 """
41 Underscore version can be used if .update is overwritten with another value in the config.
42 """
43 annotations = all_annotations(self.__class__)
45 for key, value in values.items():
46 if value is None and not _allow_none:
47 continue
49 existing_value = self.__dict__.get(key)
50 if existing_value is not None and not _overwrite:
51 # fill mode, don't overwrite
52 continue
54 if _strict and key not in annotations:
55 raise ConfigErrorExtraKey(cls=self.__class__, key=key, value=value)
57 if _strict and not check_type(value, annotations[key]) and not (value is None and _allow_none):
58 raise ConfigErrorInvalidType(expected_type=annotations[key], key=key, value=value)
60 self.__dict__[key] = value
61 # setattr(self, key, value)
63 def update(self, _strict: bool = True, _allow_none: bool = False, _overwrite: bool = True, **values: Any) -> None:
64 """
65 Update values on this config.
67 Args:
68 _strict: allow wrong types?
69 _allow_none: allow None or skip those entries?
70 _overwrite: also update not-None values?
71 **values: key: value pairs in the right types to update.
72 """
73 return self._update(_strict=_strict, _allow_none=_allow_none, _overwrite=_overwrite, **values)
75 def _fill(self, _strict: bool = True, **values: typing.Any) -> None:
76 """
77 Alias for update without overwrite.
79 Underscore version can be used if .fill is overwritten with another value in the config.
80 """
81 return self._update(_strict, _allow_none=False, _overwrite=False, **values)
83 def fill(self, _strict: bool = True, **values: typing.Any) -> None:
84 """
85 Alias for update without overwrite.
86 """
87 return self._update(_strict, _allow_none=False, _overwrite=False, **values)
89 @classmethod
90 def _all_annotations(cls) -> dict[str, type]:
91 """
92 Shortcut to get all annotations.
93 """
94 return all_annotations(cls)
96 def _format(self, string: str) -> str:
97 """
98 Format the config data into a string template.
100 Replacement for string.format(**config), which is only possible for MutableMappings.
101 MutableMapping does not work well with our Singleton Metaclass.
102 """
103 return string.format(**self.__dict__)
105 def __setattr__(self, key: str, value: typing.Any) -> None:
106 """
107 Updates should have the right type.
109 If you want a non-strict option, use _update(strict=False).
110 """
111 if key.startswith("_"):
112 return super().__setattr__(key, value)
113 self._update(**{key: value})
116K = typing.TypeVar("K", bound=str)
117V = typing.TypeVar("V", bound=Any)
120class TypedMappingAbstract(TypedConfig, Mapping[K, V]):
121 """
122 Note: this can't be used as a singleton!
124 Don't use directly, choose either TypedMapping (immutable) or TypedMutableMapping (mutable).
125 """
127 def __getitem__(self, key: K) -> V:
128 """
129 Dict-notation to get attribute.
131 Example:
132 my_config[key]
133 """
134 return typing.cast(V, self.__dict__[key])
136 def __len__(self) -> int:
137 """
138 Required for Mapping.
139 """
140 return len(self.__dict__)
142 def __iter__(self) -> Iterator[K]:
143 """
144 Required for Mapping.
145 """
146 # keys is actually a `dict_keys` but mypy doesn't need to know that
147 keys = typing.cast(list[K], self.__dict__.keys())
148 return iter(keys)
151class TypedMapping(TypedMappingAbstract[K, V]):
152 """
153 Note: this can't be used as a singleton!
154 """
156 def _update(self, *_: Any, **__: Any) -> None:
157 raise ConfigErrorImmutable(self.__class__)
160class TypedMutableMapping(TypedMappingAbstract[K, V], MutableMapping[K, V]):
161 """
162 Note: this can't be used as a singleton!
163 """
165 def __setitem__(self, key: str, value: V) -> None:
166 """
167 Dict notation to set attribute.
169 Example:
170 my_config[key] = value
171 """
172 self.update(**{key: value})
174 def __delitem__(self, key: K) -> None:
175 """
176 Dict notation to delete attribute.
178 Example:
179 del my_config[key]
180 """
181 del self.__dict__[key]
183 def update(self, *args: Any, **kwargs: V) -> None: # type: ignore
184 """
185 Ensure TypedConfig.update is used en not MutableMapping.update.
186 """
187 return TypedConfig._update(self, *args, **kwargs)
190# also expose as separate function:
191def update(self: Any, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:
192 """
193 Update values on a config.
195 Args:
196 self: config instance to update
197 _strict: allow wrong types?
198 _allow_none: allow None or skip those entries?
199 **values: key: value pairs in the right types to update.
200 """
201 return TypedConfig._update(self, _strict, _allow_none, **values)