Coverage for src/configuraptor/cls.py: 75%
93 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 20:12 +0100
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-09 20:12 +0100
1"""
2Logic for the TypedConfig inheritable class.
3"""
5import copy
6import os
7import typing
8from collections.abc import Mapping, MutableMapping
9from typing import Any, Iterator
11from typing_extensions import Never, Self
13from . import Alias
14from .abs import AbstractTypedConfig
15from .alias import has_aliases
16from .beautify import beautify as apply_beautify
17from .core import check_and_convert_type
18from .errors import ConfigErrorExtraKey, ConfigErrorImmutable
19from .helpers import all_annotations, is_optional
20from .loaders.loaders_shared import _convert_key
22C = typing.TypeVar("C", bound=Any)
24NO_ANNOTATION = typing.NewType("NO_ANNOTATION", object) # SentinelObject
27class TypedConfig(AbstractTypedConfig):
28 """
29 Can be used instead of load_into.
30 """
32 def __init_subclass__(cls, beautify: bool = True, **_: typing.Any) -> None:
33 """
34 When inheriting from TypedConfig, automatically beautify the class.
36 To disable this behavior:
37 class MyConfig(TypedConfig, beautify=False):
38 ...
39 """
40 if beautify:
41 apply_beautify(cls)
43 def _update(
44 self,
45 _strict: bool = True,
46 _allow_none: bool = False,
47 _skip_none: bool = False,
48 _overwrite: bool = True,
49 _ignore_extra: bool = False,
50 _lower_keys: bool = False,
51 _normalize_keys: bool = True,
52 _convert_types: bool = False,
53 _update_aliases: bool = True,
54 **values: Any,
55 ) -> Self:
56 """
57 Underscore version can be used if .update is overwritten with another value in the config.
58 """
59 annotations = all_annotations(self.__class__)
61 for key, value in values.items():
62 if _lower_keys:
63 key = key.lower()
65 if _normalize_keys:
66 # replace - with _
67 key = _convert_key(key)
69 annotation = annotations.get(key, NO_ANNOTATION)
71 if value is None and ((not is_optional(annotation) and not _allow_none) or _skip_none):
72 continue
74 existing_value = self.__dict__.get(key)
75 if existing_value is not None and not _overwrite:
76 # fill mode, don't overwrite
77 continue
79 if _strict and annotation is NO_ANNOTATION:
80 if _ignore_extra:
81 continue
82 else:
83 raise ConfigErrorExtraKey(cls=self.__class__, key=key, value=value)
85 # check_and_convert_type
86 if _strict and not (value is None and _allow_none):
87 value = check_and_convert_type(value, annotation, convert_types=_convert_types, key=key)
89 self.__dict__[key] = value
90 # setattr(self, key, value)
92 if _update_aliases:
93 cls = self.__class__
94 prop = cls.__dict__.get(key)
95 if isinstance(prop, Alias):
96 self.__dict__[prop.to] = value
97 else:
98 for alias in has_aliases(cls, key):
99 self.__dict__[alias] = value
101 return self
103 def update(
104 self,
105 _strict: bool = True,
106 _allow_none: bool = False,
107 _skip_none: bool = False,
108 _overwrite: bool = True,
109 _ignore_extra: bool = False,
110 _lower_keys: bool = False,
111 _normalize_keys: bool = True,
112 _convert_types: bool = False,
113 _update_aliases: bool = True,
114 **values: Any,
115 ) -> Self:
116 """
117 Update values on this config.
119 Args:
120 _strict: allow wrong types?
121 _allow_none: allow None or skip those entries for required items?
122 _skip_none: skip none also for optional items?
123 _overwrite: also update not-None values?
124 _ignore_extra: skip additional keys that aren't in the object.
125 _lower_keys: set the keys to lowercase (useful for env)
126 _normalize_keys: change - to _
127 _convert_types: try to convert variables to the right type if they aren't yet
128 _update_aliases: also update related fields?
130 **values: key: value pairs in the right types to update.
131 """
132 return self._update(
133 _strict=_strict,
134 _allow_none=_allow_none,
135 _skip_none=_skip_none,
136 _overwrite=_overwrite,
137 _ignore_extra=_ignore_extra,
138 _lower_keys=_lower_keys,
139 _normalize_keys=_normalize_keys,
140 _convert_types=_convert_types,
141 _update_aliases=_update_aliases,
142 **values,
143 )
145 def __or__(self, other: dict[str, Any]) -> Self:
146 """
147 Allows config |= {}.
149 Where {} is a dict of new data and optionally settings (starting with _)
151 Returns an updated clone of the original object, so this works too:
152 new_config = config | {...}
153 """
154 to_update = self._clone()
155 return to_update._update(**other)
157 def update_from_env(self) -> Self:
158 """
159 Update (in place) using the current environment variables, lowered etc.
161 Ignores extra env vars.
162 """
163 return self._update(_ignore_extra=True, _lower_keys=True, _convert_types=True, **os.environ)
165 def _fill(self, _strict: bool = True, **values: typing.Any) -> Self:
166 """
167 Alias for update without overwrite.
169 Underscore version can be used if .fill is overwritten with another value in the config.
170 """
171 return self._update(_strict, _allow_none=False, _overwrite=False, **values)
173 def fill(self, _strict: bool = True, **values: typing.Any) -> Self:
174 """
175 Alias for update without overwrite.
176 """
177 return self._update(_strict, _allow_none=False, _overwrite=False, **values)
179 @classmethod
180 def _all_annotations(cls) -> dict[str, type]:
181 """
182 Shortcut to get all annotations.
183 """
184 return all_annotations(cls)
186 def _format(self, string: str) -> str:
187 """
188 Format the config data into a string template.
190 Replacement for string.format(**config), which is only possible for MutableMappings.
191 MutableMapping does not work well with our Singleton Metaclass.
192 """
193 return string.format(**self.__dict__)
195 def __setattr__(self, key: str, value: typing.Any) -> None:
196 """
197 Updates should have the right type.
199 If you want a non-strict option, use _update(strict=False).
200 """
201 if key.startswith("_"):
202 return super().__setattr__(key, value)
203 self._update(**{key: value})
205 def _clone(self) -> Self:
206 return copy.deepcopy(self)
209K = typing.TypeVar("K", bound=str)
210V = typing.TypeVar("V", bound=Any)
213class TypedMappingAbstract(TypedConfig, Mapping[K, V]):
214 """
215 Note: this can't be used as a singleton!
217 Don't use directly, choose either TypedMapping (immutable) or TypedMutableMapping (mutable).
218 """
220 def __getitem__(self, key: K) -> V:
221 """
222 Dict-notation to get attribute.
224 Example:
225 my_config[key]
226 """
227 return typing.cast(V, self.__dict__[key])
229 def __len__(self) -> int:
230 """
231 Required for Mapping.
232 """
233 return len(self.__dict__)
235 def __iter__(self) -> Iterator[K]:
236 """
237 Required for Mapping.
238 """
239 # keys is actually a `dict_keys` but mypy doesn't need to know that
240 keys = typing.cast(list[K], self.__dict__.keys())
241 return iter(keys)
244class TypedMapping(TypedMappingAbstract[K, V]):
245 """
246 Note: this can't be used as a singleton!
247 """
249 def _update(self, *_: Any, **__: Any) -> Never:
250 raise ConfigErrorImmutable(self.__class__)
253class TypedMutableMapping(TypedMappingAbstract[K, V], MutableMapping[K, V]):
254 """
255 Note: this can't be used as a singleton!
256 """
258 def __setitem__(self, key: str, value: V) -> None:
259 """
260 Dict notation to set attribute.
262 Example:
263 my_config[key] = value
264 """
265 self.update(**{key: value})
267 def __delitem__(self, key: K) -> None:
268 """
269 Dict notation to delete attribute.
271 Example:
272 del my_config[key]
273 """
274 del self.__dict__[key]
276 def update(self, *args: Any, **kwargs: V) -> Self: # type: ignore
277 """
278 Ensure TypedConfig.update is used en not MutableMapping.update.
279 """
280 return TypedConfig._update(self, *args, **kwargs)
283T = typing.TypeVar("T", bound=TypedConfig)
286# also expose as separate function:
287def update(self: T, _strict: bool = True, _allow_none: bool = False, **values: Any) -> T:
288 """
289 Update values on a config.
291 Args:
292 self: config instance to update
293 _strict: allow wrong types?
294 _allow_none: allow None or skip those entries?
295 **values: key: value pairs in the right types to update.
296 """
297 return TypedConfig._update(self, _strict, _allow_none, **values)