Coverage for src/configuraptor/cls.py: 100%
53 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-26 13:52 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-26 13:52 +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(cls: typing.Type[C], data: T_data, key: str = None, init: dict[str, Any] = None, strict: bool = True) -> C:
22 """
23 Load a class' config values from the config file.
25 SomeClass.load(data, ...) = load_into(SomeClass, data, ...).
26 """
27 return load_into(cls, data, key=key, init=init, strict=strict)
29 def _update(self, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:
30 """
31 Can be used if .update is overwritten with another value in the config.
32 """
33 annotations = all_annotations(self.__class__)
35 for key, value in values.items():
36 if value is None and not _allow_none:
37 continue
39 if _strict and key not in annotations:
40 raise ConfigErrorExtraKey(cls=self.__class__, key=key, value=value)
42 if _strict and not check_type(value, annotations[key]) and not (value is None and _allow_none):
43 raise ConfigErrorInvalidType(expected_type=annotations[key], key=key, value=value)
45 self.__dict__[key] = value
46 # setattr(self, key, value)
48 def update(self, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:
49 """
50 Update values on this config.
52 Args:
53 _strict: allow wrong types?
54 _allow_none: allow None or skip those entries?
55 **values: key: value pairs in the right types to update.
56 """
57 return self._update(_strict, _allow_none, **values)
59 @classmethod
60 def _all_annotations(cls) -> dict[str, type]:
61 """
62 Shortcut to get all annotations.
63 """
64 return all_annotations(cls)
66 def _format(self, string: str) -> str:
67 """
68 Format the config data into a string template.
70 Replacement for string.format(**config), which is only possible for MutableMappings.
71 MutableMapping does not work well with our Singleton Metaclass.
72 """
73 return string.format(**self.__dict__)
75 def __setattr__(self, key: str, value: typing.Any) -> None:
76 """
77 Updates should have the right type.
79 If you want a non-strict option, use _update(strict=False).
80 """
81 if key.startswith("_"):
82 return super().__setattr__(key, value)
83 self._update(**{key: value})
86K = typing.TypeVar("K", bound=str)
87V = typing.TypeVar("V", bound=Any)
90class TypedMappingAbstract(TypedConfig, Mapping[K, V]):
91 """
92 Note: this can't be used as a singleton!
94 Don't use directly, choose either TypedMapping (immutable) or TypedMutableMapping (mutable).
95 """
97 def __getitem__(self, key: K) -> V:
98 """
99 Dict-notation to get attribute.
101 Example:
102 my_config[key]
103 """
104 return typing.cast(V, self.__dict__[key])
106 def __len__(self) -> int:
107 """
108 Required for Mapping.
109 """
110 return len(self.__dict__)
112 def __iter__(self) -> Iterator[K]:
113 """
114 Required for Mapping.
115 """
116 # keys is actually a `dict_keys` but mypy doesn't need to know that
117 keys = typing.cast(list[K], self.__dict__.keys())
118 return iter(keys)
121class TypedMapping(TypedMappingAbstract[K, V]):
122 """
123 Note: this can't be used as a singleton!
124 """
126 def _update(self, *_: Any, **__: Any) -> None:
127 raise ConfigErrorImmutable(self.__class__)
130class TypedMutableMapping(TypedMappingAbstract[K, V], MutableMapping[K, V]):
131 """
132 Note: this can't be used as a singleton!
133 """
135 def __setitem__(self, key: str, value: V) -> None:
136 """
137 Dict notation to set attribute.
139 Example:
140 my_config[key] = value
141 """
142 self.update(**{key: value})
144 def __delitem__(self, key: K) -> None:
145 """
146 Dict notation to delete attribute.
148 Example:
149 del my_config[key]
150 """
151 del self.__dict__[key]
153 def update(self, *args: Any, **kwargs: V) -> None: # type: ignore
154 """
155 Ensure TypedConfig.update is used en not MutableMapping.update.
156 """
157 return TypedConfig._update(self, *args, **kwargs)
160# also expose as separate function:
161def update(self: Any, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:
162 """
163 Update values on a config.
165 Args:
166 self: config instance to update
167 _strict: allow wrong types?
168 _allow_none: allow None or skip those entries?
169 **values: key: value pairs in the right types to update.
170 """
171 return TypedConfig._update(self, _strict, _allow_none, **values)