Coverage for src/configuraptor/cls.py: 100%

1"""

2 Logic for the TypedConfig inheritable class.

3"""

4

5 import typing

6 from collections.abc import Mapping, MutableMapping

7 from typing import Any, Iterator

8

9 from .core import T_data, all_annotations, check_type, load_into

10 from .errors import ConfigErrorExtraKey, ConfigErrorImmutable, ConfigErrorInvalidType

11

12 C = typing.TypeVar("C", bound=Any)

13

14

15 class TypedConfig:

16 """

17 Can be used instead of load_into.

18 """

19

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.

24

25 SomeClass.load(data, ...) = load_into(SomeClass, data, ...).

26 """

27 return load_into(cls, data, key=key, init=init, strict=strict)

28

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__)

34

35 for key, value in values.items():

36 if value is None and not _allow_none:

37 continue

38

39 if _strict and key not in annotations:

40 raise ConfigErrorExtraKey(cls=self.__class__, key=key, value=value)

41

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)

44

45 self.__dict__[key] = value

46 # setattr(self, key, value)

47

48 def update(self, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:

49 """

50 Update values on this config.

51

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)

58

59 @classmethod

60 def _all_annotations(cls) -> dict[str, type]:

61 """

62 Shortcut to get all annotations.

63 """

64 return all_annotations(cls)

65

66 def _format(self, string: str) -> str:

67 """

68 Format the config data into a string template.

69

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__)

74

75 def __setattr__(self, key: str, value: typing.Any) -> None:

76 """

77 Updates should have the right type.

78

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})

84

85

86 K = typing.TypeVar("K", bound=str)

87 V = typing.TypeVar("V", bound=Any)

88

89

90 class TypedMappingAbstract(TypedConfig, Mapping[K, V]):

91 """

92 Note: this can't be used as a singleton!

93

94 Don't use directly, choose either TypedMapping (immutable) or TypedMutableMapping (mutable).

95 """

96

97 def __getitem__(self, key: K) -> V:

98 """

99 Dict-notation to get attribute.

100

101 Example:

102 my_config[key]

103 """

104 return typing.cast(V, self.__dict__[key])

105

106 def __len__(self) -> int:

107 """

108 Required for Mapping.

109 """

110 return len(self.__dict__)

111

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)

119

120

121 class TypedMapping(TypedMappingAbstract[K, V]):

122 """

123 Note: this can't be used as a singleton!

124 """

125

126 def _update(self, *_: Any, **__: Any) -> None:

127 raise ConfigErrorImmutable(self.__class__)

128

129

130 class TypedMutableMapping(TypedMappingAbstract[K, V], MutableMapping[K, V]):

131 """

132 Note: this can't be used as a singleton!

133 """

134

135 def __setitem__(self, key: str, value: V) -> None:

136 """

137 Dict notation to set attribute.

138

139 Example:

140 my_config[key] = value

141 """

142 self.update(**{key: value})

143

144 def __delitem__(self, key: K) -> None:

145 """

146 Dict notation to delete attribute.

147

148 Example:

149 del my_config[key]

150 """

151 del self.__dict__[key]

152

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)

158

159

160# also expose as separate function:

161 def update(self: Any, _strict: bool = True, _allow_none: bool = False, **values: Any) -> None:

162 """

163 Update values on a config.

164

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)