Coverage for configlayer/_config.py: 99%

119 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-08 11:36 +0000

1"""Internal config layer support structure""" 

2from types import MappingProxyType 

3from typing import Any, Callable 

4from functools import partial 

5 

6from ._profiles import Profiles 

7from ._io import IO 

8from ._file import File 

9 

10from .types import fields_t, on_set_t, Field 

11from .utils import Locker, check_extra, check_types, set_slots_defaults, fmt_exc 

12from .exceptions import OptionsCheckError, InputError 

13 

14 

15_get_raw = object.__getattribute__ 

16 

17 

18@set_slots_defaults(fields_t=bool) 

19class Options: 

20 """Config options defaults 

21 There is 2 ways to change it: 

22 1. Fill instance and provide it at Config init (options keyword) 

23 2. Change in inited config directly (<config_data>.cfg.options.<option_name>)""" 

24 typecheck = True # Check field data for type at each field set 

25 typecast = True # Try to cast type if type check enabled and failed 

26 revert_fails = False # Field value revert if on_set get some error 

27 

28 def __post_init__(self): 

29 if msg := self._check(): 

30 raise OptionsCheckError(msg) 

31 

32 def __setattr__(self, key, value, revert=False): 

33 if not isinstance(value, bool): 

34 raise OptionsCheckError('Options accepting only booleans') 

35 if value == (prev_value := getattr(self, key, None)): 35 ↛ 36line 35 didn't jump to line 36, because the condition on line 35 was never true

36 return 

37 

38 object.__setattr__(self, key, value) 

39 if msg := self._check(): 

40 if revert: 

41 return f'Some options changed in wrong way, check failed: {msg}' 

42 msg = f'Set option {key!r} to {value!r} failed: {msg}' 

43 if msg2 := self.__setattr__(key, prev_value, revert=True): 

44 raise OptionsCheckError(f'{msg}. Revert to {prev_value!r} also failed: {msg2}') 

45 raise OptionsCheckError(f'{msg}. Reverted to {prev_value!r} successfully') 

46 

47 def _check(self): 

48 if self.typecast and not self.typecheck: 

49 return 'Type checking is disabled, type casting cannot be enabled' 

50 

51 

52class ConfigSupport(Locker): 

53 """Config support structure 

54 Holds a lot of functionality for config operations""" 

55 __slots__ = ('__weakref__', '_data', '_fields', '_on_set', '_name', 

56 'name', 'type_name', 'def_sect', 'options', 'version', 'profiles', 'io', 'file') 

57 _data: Any 

58 _fields: dict[str, Field] 

59 _on_set: dict[str, on_set_t] 

60 name: str 

61 type_name: str 

62 def_sect: str 

63 options: Options 

64 version: None | int 

65 profiles: None | Profiles 

66 io: None | IO 

67 file: None | File 

68 

69 def __init__(self, data, fields, _name, name, default_section, options, type_name): 

70 self._data = data 

71 self._fields = fields 

72 self._on_set = {} 

73 self._name = _name 

74 self.name = name 

75 self.type_name = type_name 

76 self.def_sect = default_section 

77 self.options = options 

78 self.version = self.profiles = self.io = self.file = None 

79 

80 # Locks structure for changes with disabling attribute deletion 

81 super().__init__(del_attr=False, name=str(self)) 

82 

83 def __repr__(self): 

84 return f'{self._name}.cfg' # noqa 

85 

86 def __str__(self): 

87 return f'{self.name!r} {self.type_name} support structure' 

88 

89 def add_on_set(self, name: str, field_name: str | None, run_if_equal: bool, 

90 func: Callable, *args, **kwargs): 

91 """Add function call on specified or any (if None provided) config field set 

92 :arg name: Handler name 

93 :arg field_name: Field name or None (if all fields handler provided) 

94 :arg run_if_equal: Run handler anyway (if field value is not changed) 

95 :arg func: Handler function 

96 :arg args: :arg func: positional arguments before: key, prev_value, value 

97 :arg kwargs: :arg func: keyword arguments 

98 :raise InputError: If wrong arguments provided""" 

99 if name in self._on_set: 

100 raise InputError('name', func_name=f'{self!r}.add_on_set()', 

101 msg=f'Cannot add {name!r} handler, it already exists') 

102 if field_name is not None and field_name not in self._fields: 

103 raise InputError('field_name', func_name=f'{self!r}.add_on_set()', 

104 msg=f'Cannot add {name!r} handler, not exists {field_name = }') 

105 self._on_set.update({name: (field_name, run_if_equal, partial(func, *args, **kwargs))}) 

106 

107 def del_on_set(self, name: str): 

108 """Delete on_set handler by its name 

109 :arg name: Handler name""" 

110 del self._on_set[name] 

111 

112 @property 

113 def get_on_set(self) -> MappingProxyType[str, on_set_t]: 

114 """Get exists on_set handlers as a dict view""" 

115 return MappingProxyType(self._on_set) 

116 

117 @property 

118 def get_fields(self) -> MappingProxyType[str, Field]: 

119 """Get fields descriptors as dict""" 

120 return MappingProxyType(self._fields) 

121 

122 @property 

123 def get_data(self) -> fields_t: 

124 """Get fields data as dict""" 

125 data = self._data 

126 return {k: getattr(data, k) for k in self._fields} 

127 

128 @property 

129 def get_changed(self) -> fields_t[bool]: 

130 """Get changed fields states as dict""" 

131 data = self._data 

132 return {k: getattr(data, k) != v.default for k, v in self._fields.items()} 

133 

134 @property 

135 def get_types(self) -> fields_t[type]: 

136 """Get fields types as dict""" 

137 return {k: v.type for k, v in self._fields.items()} 

138 

139 @property 

140 def get_defaults(self) -> fields_t: 

141 """Get fields defaults as dict""" 

142 return {k: v.default for k, v in self._fields.items()} 

143 

144 @property 

145 def get_factory_defaults(self) -> fields_t: 

146 """Get fields defaults provided at config declaration as dict""" 

147 c = type(self._data) 

148 return {k: f.default if isinstance(f := getattr(c, k), Field) else f for k in self._fields} 

149 

150 def _check_fields(self, input_exc, fields: fields_t, types=True, typecast=False): 

151 if not fields: 

152 raise fmt_exc(input_exc, 'Empty fields provided') 

153 

154 check_extra(fields, self._fields, 'field', input_exc=input_exc) 

155 

156 if types: 

157 _types = self.get_types 

158 # bug mypy: dict[str, type] is type, not str.. and it is also holder_t.. 

159 return check_types(fields, {k: _types[k] for k in fields}, typecast, # type: ignore[misc] 

160 input_exc=input_exc, obj_t_check=False) 

161 return fields 

162 

163 def _set_fields(self, fields: fields_t): 

164 data = self._data 

165 # bug mypy: return value is not used 

166 [setattr(data, k, v) for k, v in fields.items()] # type: ignore[func-returns-value] 

167 

168 def _set_defaults(self, fields: fields_t): 

169 _fields = self._fields 

170 # bug mypy: return value is not used 

171 [setattr(_fields[k], 'default', v) for k, v in fields.items()] # type: ignore[func-returns-value] 

172 

173 def set_fields(self, fields: fields_t, typecheck=True, typecast=False): 

174 """Set current fields by dict with type check and cast possibility 

175 :arg fields: Dict with data to set fields 

176 :arg typecheck: Data types check 

177 :arg typecast: Data types cast (if check failed) 

178 :raise InputError: If wrong arguments provided""" 

179 input_exc = (f'{self!r}.set_fields()', 'fields') 

180 self._set_fields(self._check_fields(input_exc, fields, typecheck, typecast)) 

181 if self.profiles: 

182 self.profiles.update() 

183 

184 def set_defaults(self, fields: fields_t, typecheck=True, typecast=False): 

185 """Set defaults by dict with type check and cast possibility 

186 :arg fields: Dict with data to set defaults 

187 :arg typecheck: Data types check 

188 :arg typecast: Data types cast (if check failed) 

189 :raise InputError: If wrong arguments provided""" 

190 input_exc = (f'{self!r}.set_defaults()', 'fields') 

191 fields = self._check_fields(input_exc, fields, typecheck, typecast) 

192 if self.profiles and self.profiles.active == self.def_sect: 

193 self._set_fields(fields) 

194 self._set_defaults(fields)