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

53 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-21 10:22 +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, **values: Any) -> None: 

40 """ 

41 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 if _strict and key not in annotations: 

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

51 

52 if _strict and not check_type(value, annotations[key]) and not (value is None and _allow_none): 

53 raise ConfigErrorInvalidType(expected_type=annotations[key], key=key, value=value) 

54 

55 self.__dict__[key] = value 

56 # setattr(self, key, value) 

57 

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

59 """ 

60 Update values on this config. 

61 

62 Args: 

63 _strict: allow wrong types? 

64 _allow_none: allow None or skip those entries? 

65 **values: key: value pairs in the right types to update. 

66 """ 

67 return self._update(_strict, _allow_none, **values) 

68 

69 @classmethod 

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

71 """ 

72 Shortcut to get all annotations. 

73 """ 

74 return all_annotations(cls) 

75 

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

77 """ 

78 Format the config data into a string template. 

79 

80 Replacement for string.format(**config), which is only possible for MutableMappings. 

81 MutableMapping does not work well with our Singleton Metaclass. 

82 """ 

83 return string.format(**self.__dict__) 

84 

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

86 """ 

87 Updates should have the right type. 

88 

89 If you want a non-strict option, use _update(strict=False). 

90 """ 

91 if key.startswith("_"): 

92 return super().__setattr__(key, value) 

93 self._update(**{key: value}) 

94 

95 

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

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

98 

99 

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

101 """ 

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

103 

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

105 """ 

106 

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

108 """ 

109 Dict-notation to get attribute. 

110 

111 Example: 

112 my_config[key] 

113 """ 

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

115 

116 def __len__(self) -> int: 

117 """ 

118 Required for Mapping. 

119 """ 

120 return len(self.__dict__) 

121 

122 def __iter__(self) -> Iterator[K]: 

123 """ 

124 Required for Mapping. 

125 """ 

126 # keys is actually a `dict_keys` but mypy doesn't need to know that 

127 keys = typing.cast(list[K], self.__dict__.keys()) 

128 return iter(keys) 

129 

130 

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

132 """ 

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

134 """ 

135 

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

137 raise ConfigErrorImmutable(self.__class__) 

138 

139 

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

141 """ 

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

143 """ 

144 

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

146 """ 

147 Dict notation to set attribute. 

148 

149 Example: 

150 my_config[key] = value 

151 """ 

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

153 

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

155 """ 

156 Dict notation to delete attribute. 

157 

158 Example: 

159 del my_config[key] 

160 """ 

161 del self.__dict__[key] 

162 

163 def update(self, *args: Any, **kwargs: V) -> None: # type: ignore 

164 """ 

165 Ensure TypedConfig.update is used en not MutableMapping.update. 

166 """ 

167 return TypedConfig._update(self, *args, **kwargs) 

168 

169 

170# also expose as separate function: 

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

172 """ 

173 Update values on a config. 

174 

175 Args: 

176 self: config instance to update 

177 _strict: allow wrong types? 

178 _allow_none: allow None or skip those entries? 

179 **values: key: value pairs in the right types to update. 

180 """ 

181 return TypedConfig._update(self, _strict, _allow_none, **values)