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

51 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-22 13:51 +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 self._update(**{key: value}) 

82 

83 

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

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

86 

87 

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

89 """ 

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

91 

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

93 """ 

94 

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

96 """ 

97 Dict-notation to get attribute. 

98 

99 Example: 

100 my_config[key] 

101 """ 

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

103 

104 def __len__(self) -> int: 

105 """ 

106 Required for Mapping. 

107 """ 

108 return len(self.__dict__) 

109 

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

111 """ 

112 Required for Mapping. 

113 """ 

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

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

116 return iter(keys) 

117 

118 

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

120 """ 

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

122 """ 

123 

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

125 raise ConfigErrorImmutable(self.__class__) 

126 

127 

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

129 """ 

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

131 """ 

132 

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

134 """ 

135 Dict notation to set attribute. 

136 

137 Example: 

138 my_config[key] = value 

139 """ 

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

141 

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

143 """ 

144 Dict notation to delete attribute. 

145 

146 Example: 

147 del my_config[key] 

148 """ 

149 del self.__dict__[key] 

150 

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

152 """ 

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

154 """ 

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

156 

157 

158# also expose as separate function: 

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

160 """ 

161 Update values on a config. 

162 

163 Args: 

164 self: config instance to update 

165 _strict: allow wrong types? 

166 _allow_none: allow None or skip those entries? 

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

168 """ 

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