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

1""" 

2Logic for the TypedConfig inheritable class. 

3""" 

4 

5import typing 

6from collections.abc import Mapping, MutableMapping 

7from typing import Any, Iterator 

8 

9from .abs import AbstractTypedConfig 

10from .errors import ConfigErrorExtraKey, ConfigErrorImmutable, ConfigErrorInvalidType 

11from .helpers import all_annotations, check_type 

12 

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

14 

15 

16class TypedConfig(AbstractTypedConfig): 

17 """ 

18 Can be used instead of load_into. 

19 """ 

20 

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

26 

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

28 if value is None and not _allow_none: 

29 continue 

30 

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 

35 

36 if _strict and key not in annotations: 

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

38 

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) 

41 

42 self.__dict__[key] = value 

43 # setattr(self, key, value) 

44 

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

46 """ 

47 Update values on this config. 

48 

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) 

56 

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

58 """ 

59 Alias for update without overwrite. 

60 

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) 

64 

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) 

70 

71 @classmethod 

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

73 """ 

74 Shortcut to get all annotations. 

75 """ 

76 return all_annotations(cls) 

77 

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

79 """ 

80 Format the config data into a string template. 

81 

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

86 

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

88 """ 

89 Updates should have the right type. 

90 

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

96 

97 

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

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

100 

101 

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

103 """ 

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

105 

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

107 """ 

108 

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

110 """ 

111 Dict-notation to get attribute. 

112 

113 Example: 

114 my_config[key] 

115 """ 

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

117 

118 def __len__(self) -> int: 

119 """ 

120 Required for Mapping. 

121 """ 

122 return len(self.__dict__) 

123 

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) 

131 

132 

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

134 """ 

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

136 """ 

137 

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

139 raise ConfigErrorImmutable(self.__class__) 

140 

141 

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

143 """ 

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

145 """ 

146 

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

148 """ 

149 Dict notation to set attribute. 

150 

151 Example: 

152 my_config[key] = value 

153 """ 

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

155 

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

157 """ 

158 Dict notation to delete attribute. 

159 

160 Example: 

161 del my_config[key] 

162 """ 

163 del self.__dict__[key] 

164 

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) 

170 

171 

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. 

176 

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)