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

1""" 

2Logic for the TypedConfig inheritable class. 

3""" 

4 

5import typing 

6from collections.abc import Mapping, MutableMapping 

7from typing import Any, Iterator 

8 

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

10from .errors import ConfigErrorExtraKey, ConfigErrorImmutable, ConfigErrorInvalidType 

11 

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

13 

14 

15class TypedConfig: 

16 """ 

17 Can be used instead of load_into. 

18 """ 

19 

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. 

32 

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 ) 

38 

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

44 

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

46 if value is None and not _allow_none: 

47 continue 

48 

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 

53 

54 if _strict and key not in annotations: 

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

56 

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) 

59 

60 self.__dict__[key] = value 

61 # setattr(self, key, value) 

62 

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

64 """ 

65 Update values on this config. 

66 

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) 

74 

75 def _fill(self, _strict: bool = True, **values: typing.Any) -> None: 

76 """ 

77 Alias for update without overwrite. 

78 

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) 

82 

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) 

88 

89 @classmethod 

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

91 """ 

92 Shortcut to get all annotations. 

93 """ 

94 return all_annotations(cls) 

95 

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

97 """ 

98 Format the config data into a string template. 

99 

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

104 

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

106 """ 

107 Updates should have the right type. 

108 

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

114 

115 

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

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

118 

119 

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

121 """ 

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

123 

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

125 """ 

126 

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

128 """ 

129 Dict-notation to get attribute. 

130 

131 Example: 

132 my_config[key] 

133 """ 

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

135 

136 def __len__(self) -> int: 

137 """ 

138 Required for Mapping. 

139 """ 

140 return len(self.__dict__) 

141 

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) 

149 

150 

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

152 """ 

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

154 """ 

155 

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

157 raise ConfigErrorImmutable(self.__class__) 

158 

159 

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

161 """ 

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

163 """ 

164 

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

166 """ 

167 Dict notation to set attribute. 

168 

169 Example: 

170 my_config[key] = value 

171 """ 

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

173 

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

175 """ 

176 Dict notation to delete attribute. 

177 

178 Example: 

179 del my_config[key] 

180 """ 

181 del self.__dict__[key] 

182 

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) 

188 

189 

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. 

194 

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)