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

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(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 

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

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

88 

89 

90class 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 

121class 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 

130class 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: 

161def 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)