Coverage for configlayer/_config.py: 99%
119 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-08 11:36 +0000
« 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
6from ._profiles import Profiles
7from ._io import IO
8from ._file import File
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
15_get_raw = object.__getattribute__
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
28 def __post_init__(self):
29 if msg := self._check():
30 raise OptionsCheckError(msg)
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
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')
47 def _check(self):
48 if self.typecast and not self.typecheck:
49 return 'Type checking is disabled, type casting cannot be enabled'
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
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
80 # Locks structure for changes with disabling attribute deletion
81 super().__init__(del_attr=False, name=str(self))
83 def __repr__(self):
84 return f'{self._name}.cfg' # noqa
86 def __str__(self):
87 return f'{self.name!r} {self.type_name} support structure'
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))})
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]
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)
117 @property
118 def get_fields(self) -> MappingProxyType[str, Field]:
119 """Get fields descriptors as dict"""
120 return MappingProxyType(self._fields)
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}
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()}
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()}
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()}
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}
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')
154 check_extra(fields, self._fields, 'field', input_exc=input_exc)
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
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]
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]
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()
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)