Coverage for src/configuraptor/cls.py: 100%
58 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-09-18 12:33 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-09-18 12:33 +0200
1"""
2Logic for the TypedConfig inheritable class.
3"""
5import typing
6from collections.abc import Mapping, MutableMapping
7from typing import Any, Iterator
9from .abs import AbstractTypedConfig
10from .errors import ConfigErrorExtraKey, ConfigErrorImmutable, ConfigErrorInvalidType
11from .helpers import all_annotations, check_type
13C = typing.TypeVar("C", bound=Any)
16class TypedConfig(AbstractTypedConfig):
17 """
18 Can be used instead of load_into.
19 """
21 def _update(self, _strict: bool = True, _allow_none: bool = False, _overwrite: bool = True, **values: Any) -> None:
22 """
23 Underscore version can be used if .update is overwritten with another value in the config.
24 """
25 annotations = all_annotations(self.__class__)
27 for key, value in values.items():
28 if value is None and not _allow_none:
29 continue
31 existing_value = self.__dict__.get(key)
32 if existing_value is not None and not _overwrite:
33 # fill mode, don't overwrite
34 continue
36 if _strict and key not in annotations:
37 raise ConfigErrorExtraKey(cls=self.__class__, key=key, value=value)
39 if _strict and not check_type(value, annotations[key]) and not (value is None and _allow_none):
40 raise ConfigErrorInvalidType(expected_type=annotations[key], key=key, value=value)
42 self.__dict__[key] = value
43 # setattr(self, key, value)
45 def update(self, _strict: bool = True, _allow_none: bool = False, _overwrite: bool = True, **values: Any) -> None:
46 """
47 Update values on this config.
49 Args:
50 _strict: allow wrong types?
51 _allow_none: allow None or skip those entries?
52 _overwrite: also update not-None values?
53 **values: key: value pairs in the right types to update.
54 """
55 return self._update(_strict=_strict, _allow_none=_allow_none, _overwrite=_overwrite, **values)
57 def _fill(self, _strict: bool = True, **values: typing.Any) -> None:
58 """
59 Alias for update without overwrite.
61 Underscore version can be used if .fill is overwritten with another value in the config.
62 """
63 return self._update(_strict, _allow_none=False, _overwrite=False, **values)
65 def fill(self, _strict: bool = True, **values: typing.Any) -> None:
66 """
67 Alias for update without overwrite.
68 """
69 return self._update(_strict, _allow_none=False, _overwrite=False, **values)
71 @classmethod
72 def _all_annotations(cls) -> dict[str, type]:
73 """
74 Shortcut to get all annotations.
75 """
76 return all_annotations(cls)
78 def _format(self, string: str) -> str:
79 """
80 Format the config data into a string template.
82 Replacement for string.format(**config), which is only possible for MutableMappings.
83 MutableMapping does not work well with our Singleton Metaclass.
84 """
85 return string.format(**self.__dict__)
87 def __setattr__(self, key: str, value: typing.Any) -> None:
88 """
89 Updates should have the right type.
91 If you want a non-strict option, use _update(strict=False).
92 """
93 if key.startswith("_"):
94 return super().__setattr__(key, value)
95 self._update(**{key: value})
98K = typing.TypeVar("K", bound=str)
99V = typing.TypeVar("V", bound=Any)
102class TypedMappingAbstract(TypedConfig, Mapping[K, V]):
103 """
104 Note: this can't be used as a singleton!
106 Don't use directly, choose either TypedMapping (immutable) or TypedMutableMapping (mutable).
107 """
109 def __getitem__(self, key: K) -> V:
110 """
111 Dict-notation to get attribute.
113 Example:
114 my_config[key]
115 """
116 return typing.cast(V, self.__dict__[key])
118 def __len__(self) -> int:
119 """
120 Required for Mapping.
121 """
122 return len(self.__dict__)
124 def __iter__(self) -> Iterator[K]:
125 """
126 Required for Mapping.
127 """
128 # keys is actually a `dict_keys` but mypy doesn't need to know that
129 keys = typing.cast(list[K], self.__dict__.keys())
130 return iter(keys)
133class TypedMapping(TypedMappingAbstract[K, V]):
134 """
135 Note: this can't be used as a singleton!
136 """
138 def _update(self, *_: Any, **__: Any) -> None:
139 raise ConfigErrorImmutable(self.__class__)
142class TypedMutableMapping(TypedMappingAbstract[K, V], MutableMapping[K, V]):
143 """
144 Note: this can't be used as a singleton!
145 """
147 def __setitem__(self, key: str, value: V) -> None:
148 """
149 Dict notation to set attribute.
151 Example:
152 my_config[key] = value
153 """
154 self.update(**{key: value})
156 def __delitem__(self, key: K) -> None:
157 """
158 Dict notation to delete attribute.
160 Example:
161 del my_config[key]
162 """
163 del self.__dict__[key]
165 def update(self, *args: Any, **kwargs: V) -> None: # type: ignore
166 """
167 Ensure TypedConfig.update is used en not MutableMapping.update.
168 """
169 return TypedConfig._update(self, *args, **kwargs)
172# also expose as separate function:
173def update(self: Any, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:
174 """
175 Update values on a config.
177 Args:
178 self: config instance to update
179 _strict: allow wrong types?
180 _allow_none: allow None or skip those entries?
181 **values: key: value pairs in the right types to update.
182 """
183 return TypedConfig._update(self, _strict, _allow_none, **values)