Coverage for configlayer/utils.py: 99%
531 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"""Config layer support classes and functions"""
2from types import MappingProxyType
3from typing import TypeVar, Iterable, Any, Callable, Sized, Union, get_origin, Sequence
4from itertools import chain
5from functools import partial
6from dataclasses import dataclass
7from collections import ChainMap
8from collections.abc import Mapping
10from .types import state_t, holder_t, mb_holder_t, ClsObj, ItemError, ValidWrong
11from .exceptions import InternalError, InputError, InitError, CheckValueError, CheckTypeError
14_T = TypeVar('_T')
16_TEMPL_INPUT = '{} is{} needed, but{} provided'
17_TEMPL_ABSENT = 'Absent {}: '
18_TEMPL_EXTRA = 'Extra {}: '
20_IE_CE__T = ('check_extra()', 'template')
21_IE_CA__T = ('check_absent()', 'template')
22_IE_CI_ET = ('check_items()', 'extra_template')
23_IE_CI_AT = ('check_items()', 'absent_template')
24_IE_CL__T = ('check_lengths()', 'template')
26_CE = 'not more than'
27_CA = 'not less than'
28_CI = 'equal to'
30_GN_ATTRS = ('name', '__name__')
31_GN_ORDER = ('doc_obj', 'code_obj', 'attrs_obj', 'doc_cls', 'attrs_cls',
32 'str_obj', 'str_cls', 'repr_cls', 'repr_obj')
33_GN_INTERNAL = ('cls', 'obj', 'full', 'order', 'none', 'unknown', 'kwargs')
35_CLS_OBJ = ('cls', 'obj')
36_UNIQUE = object()
37_FORCE_SET = object.__setattr__
39_LOCKER_IGNORED = ('_locker_state', '_locker_enter_state')
42# Internal
45def _field_func_default(k, v): # noqa
46 return not callable(v)
49def _types(obj_t: mb_holder_t[type], func_name='', param_name='obj_t') -> holder_t[type]:
50 if isinstance(obj_t, type):
51 return obj_t, # holder_t
52 if origin := get_origin(obj_t):
53 if origin is Union: 53 ↛ 55line 53 didn't jump to line 55, because the condition on line 53 was never true
54 # bug mypy: Union origin is always has __args__
55 return obj_t.__args__ # type: ignore[attr-defined]
56 obj_t = origin
58 errors = []
59 holder, obj_types = as_holder_stated(obj_t)
60 mapping, items = as_dict_stated(obj_types)
61 if obj_types:
62 if not (errors := [ItemError(k, v) for k, v in items.items() if not isinstance(v, type)]):
63 return obj_types
64 received = ', '.join(fmt_obj_errors(obj_t, errors, bool(obj_types), holder, mapping))
65 raise InputError(param_name, must_be="type or types", received=received, func_name=func_name)
68# Bool checks
71def is_dunder(name: str) -> bool:
72 """Returns True only if a '__dunder__' name provided (double underscored from both sides)
73 :arg name: Target name for check
74 :return: bool"""
75 return len(name) > 4 and name[:2] == name[-2:] == '__' and name[2] != '_' and name[-3] != '_'
78def is_internal(name: str) -> bool:
79 """Returns True only if name is '_internal', excepting '__dunder__' name ('magic' methods)
80 :arg name: Target name for check
81 :return: bool"""
82 return name.startswith('_') and not is_dunder(name)
85def is_hidden(name: str) -> bool:
86 """Returns True if name is hidden - '_internal' or '__dunder__' name
87 :arg name: Target name for check
88 :return: bool"""
89 return name.startswith('_')
92def is_exception(obj, exc_t: type[Exception] = Exception) -> bool:
93 """Returns True only if obj is instance or subclass of provided exception type
94 :arg obj: Target object for check
95 :arg exc_t: Exception type for instance or subclass check
96 :return: bool"""
97 return isinstance(obj, exc_t) or issubclass(get_cls_obj(obj).cls or object, exc_t)
100def is_holder(obj, exclude: mb_holder_t[type] = ()) -> state_t:
101 """Returns True only if obj can hold objects of any possible type
102 :arg obj: Target object for check
103 :arg exclude: Type or types that must not be detected as holders
104 :return: bool | None (if :arg obj: is None)"""
105 exclude = _types(exclude, 'is_holder()', 'exclude') if exclude else () # no RecursionError
106 if obj is None:
107 return None
108 if isinstance(obj, (str, bytes, bytearray) + tuple(exclude)):
109 return False
110 try:
111 iter(obj) # Check iteration possibility through __iter__ or __getitem__ methods
112 except TypeError:
113 return False
114 return True
117# Type casting
120def as_holder(obj: mb_holder_t[_T], default=(), exclude: mb_holder_t[type] = ()) -> holder_t[_T]:
121 """Returns obj if it is holder type, or make holder from obj, or pass default if obj is None
122 :arg obj: Target object or objects for cast
123 :arg default: Any object that will be returned if :arg obj: is None
124 :arg exclude: Type or types that must not be detected as holders
125 :return: :arg obj: | (:arg obj:,) | :arg default:"""
126 # bug mypy: obj is always returned as holder_t, except default, that can be anything
127 return obj if (state := is_holder(obj, exclude)) else default if state is None else (obj,) # type: ignore[return-value]
130def as_holder_stated(obj: mb_holder_t[_T], default=(), exclude: mb_holder_t[type] = ()
131 ) -> tuple[bool, holder_t[_T]]:
132 """Returns obj if it is holder type, or make holder from obj, or pass default if obj is None.
133 Return is_holder() state before result
134 :arg obj: Target object or objects for cast
135 :arg default: Any object that will be returned if :arg obj: is None
136 :arg exclude: Type or types that must not be detected as holders
137 :return: bool, :arg obj: | (:arg obj:,) | :arg default:"""
138 # bug mypy: obj is always returned as holder_t, except default, that can be anything
139 return (st := is_holder(obj, exclude)), obj if st else default if st is None else (obj,) # type: ignore[return-value]
142def as_dict(objs: holder_t, keys: holder_t | Callable = enumerate, strict=False) -> dict:
143 """Returns dict from objs if it is Mapping type or process by keys func or zip keys with objs
144 :arg objs: Target objects for cast
145 :arg keys: Used only if :arg objs: is not Mapping, as func(:arg objs:) or as dict keys
146 :arg strict: Used only if :arg keys: used, and it is not Callable, as zip keyword
147 :return: dict(:arg objs: | :arg keys:(:arg objs:) | zip(:arg keys:, :arg objs:,
148 strict=:arg strict:))"""
149 s = isinstance(objs, Mapping)
150 return dict(objs if s else keys(objs) if callable(keys) else zip(keys, objs, strict=strict))
153def as_dict_stated(objs: holder_t, keys: holder_t | Callable = enumerate, strict=False
154 ) -> tuple[bool, dict]:
155 """Returns dict from objs if it is Mapping type or process by keys func or zip keys with objs
156 Return isinstance(objs, Mapping) state before result
157 :arg objs: Target objects for cast
158 :arg keys: Used only if :arg objs: is not Mapping, as func(:arg objs:) or as dict keys
159 :arg strict: Used only if :arg keys: used, and it is not Callable, as zip keyword
160 :return: bool, dict(:arg objs: | :arg keys:(:arg objs:) | zip(:arg keys:,
161 :arg objs:, strict=:arg strict:))"""
162 s = isinstance(objs, Mapping)
163 return s, dict(objs if s else keys(objs) if callable(keys) else zip(keys, objs, strict=strict))
166# Common
169def safe(func: Callable, /, *args, _res_: Any = _UNIQUE, _exc_: Any = _UNIQUE, **kwargs):
170 """Safe function execution, return result or exception object, if it raised.
171 Optional result and exception handlers or fillers could be provided (repr, 'some str', etc.)
172 :arg func: Target function for safe call
173 :arg args: Positional arguments for :arg func:
174 :arg _res_: Used on :arg func: result if Callable provided, or returned instead of result
175 :arg _exc_: Used on exception if Callable provided, or returned instead of exception
176 :arg kwargs: Keyword arguments for :arg func:
177 :return: Handled or unhandled :arg func: result or exception"""
178 try:
179 result = func(*args, **kwargs)
180 return result if _res_ is _UNIQUE else _res_(result) if callable(_res_) else _res_
181 except Exception as exc:
182 return exc if _exc_ is _UNIQUE else _exc_(exc) if callable(_exc_) else _exc_
185def with_type(obj, **kwargs) -> str:
186 """Adds type name to object representation with additional information, if needed
187 :arg obj: Target for type and optional details
188 :arg kwargs: Keyword arguments for optional details about :arg obj:
189 :return: Formatted string with :arg obj: name or repr, type, and optional details"""
190 obj_t = type(obj).__name__
191 obj = obj.__name__ if isinstance(obj, type) else repr(obj)
192 if not kwargs:
193 return f'{obj} ({obj_t})'
194 kwargs = {k: repr(v) for k, v in kwargs.items()}
195 kwargs = kwargs | {'type': obj_t} if 'type' in kwargs else {'type': obj_t} | kwargs
196 return f"{obj} ({', '.join(fmt_dict(kwargs, value_func=str))})"
199# Formatters
202def fmt_dict(obj: Mapping, key_func=str, sep='=', value_func=repr) -> tuple[str, ...]:
203 """Format dict data to tuple of str per item by customizable formatting
204 :arg obj: Target mapping for formatting
205 :arg key_func: Function for keys handling
206 :arg sep: String separator between key and value
207 :arg value_func: Function for values handling
208 :return: Formatted strings tuple of each element"""
209 return tuple(f'{key_func(k)}{sep}{value_func(v)}' for k, v in obj.items())
212def fmt_obj_errors(obj, errors: holder_t[ItemError], exists: bool, holder: bool, mapping: bool,
213 result=False, result_name='result', key_handler=repr, kv_sep='='
214 ) -> tuple[str, ...]:
215 """Format obj errors (ItemError objects) if they exist, or add obj details only
216 :arg obj: Target object for formatting, if no :arg errors: provided
217 :arg errors: Target errors list for formatting
218 :arg exists: [Formatter switch] Errors list handling
219 :arg holder: [Formatter switch] is_holder(:arg obj:) result
220 :arg mapping: [Formatter switch] isinstance(:arg obj:, Mapping) result
221 :arg result: Add error(s) result to formatted info
222 :arg result_name: :arg result: key name for not holder or mapping formatters
223 :arg key_handler: Key formatting function for mapping formatter
224 :arg kv_sep: Key-value separator for mapping formatter
225 :return: Formatted strings tuple from :arg errors: or :arg obj:"""
226 if exists and errors:
227 rn = result_name
228 if not holder:
229 error = next(x for x in errors) 229 ↛ exitline 229 didn't finish the generator expression on line 229
230 return with_type(error.value, **({rn: error.result} if result else {})),
231 if mapping:
232 return tuple(f'{key_handler(k)}{kv_sep}{with_type(v, **({rn: r} if result else {}))}'
233 for k, v, r in errors)
234 return tuple(f'{with_type(v)} at pos {i}' + (f' ({r})' if result else '')
235 for i, v, r in errors)
236 return with_type(obj),
239def fmt_name(obj: Sized, item_name='item', default=_UNIQUE) -> str:
240 """Format name depending only on the obj size - singular or plural
241 :arg obj: Target sized object for name formatting
242 :arg item_name: Base item name (without 's' postfix)
243 :arg default: Override item name if no objects provided (by default :arg item_name: + 's')
244 :return: Formatted name"""
245 if cnt := len(obj):
246 return f'{item_name}{"s" if item_name and cnt > 1 else ""}'
247 elif default == _UNIQUE:
248 return f'{item_name}s'
249 else:
250 return str(default)
253def fmt_exc(input_exc: tuple, msg='', default_exc_t: type[Exception] = Exception, *args, **kwargs
254 ) -> InputError | Exception:
255 """Format InputError (if input_exc provided) or default exception
256 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
257 :arg msg: Exception message
258 :arg default_exc_t: Exception type, used if :arg input_exc: is empty
259 :arg args: InputError or default exception positional arguments
260 :arg kwargs: InputError keyword arguments used as InputError result in default exception
261 :return: Formatted InputError or :arg default_exc_t: exception"""
262 if input_exc:
263 name, *params, in_kw = (input_exc if isinstance(input_exc[-1], dict) else (*input_exc, {}))
264 return InputError(*params, msg=msg, func_name=name, args=args, **in_kw | kwargs | in_kw)
266 if kwargs:
267 msg = InputError(msg=msg, **kwargs).args[0]
268 return default_exc_t(msg, *args) if msg else default_exc_t(*args)
271# Split
274def split(items: Iterable, condition: Callable = bool, func: Callable | None = None, *, # noqa
275 unpack=False, safely=False, modify=False, as_dicts=False,
276 cond_key: bool | None = None, cond_value: bool | None = None, cond_invert=False
277 ) -> ValidWrong:
278 """Split items to valid and wrong iterables by condition
279 Each item can be unpacked, safely called and/or modified by provided func
280 Key (or index) and/or value can be provided as condition parameter(s) (default: value only)
281 If provided only cond_key or only cond_value - another parameter will be in inverted state
282 If unpack enabled, but item is not iterable - unpack will be skipped!
283 :arg items: Target objects for split
284 :arg condition: Split function, bool (default) means that True - Valid, False - Wrong
285 :arg func: Modify function that called with each item as positional argument
286 :arg unpack: Unpack item for :arg func: call with multiple positional arguments
287 :arg safely: Safe call :arg func:, at exception - item placed in Wrong
288 :arg modify: :arg func: call results used in ValidWrong, instead of :arg items:
289 :arg as_dicts: Fill ValidWrong by dicts, even if not dict provided (indexes used as keys)
290 :arg cond_key: Add item key (or index) to condition positional arguments
291 :arg cond_value: Add item value to condition positional arguments (default: True)
292 :arg cond_invert: Invert :arg condition: bool result to swap Valid and Wrong
293 :return: ValidWrong instance with filled valid, wrong and errors attributes
294 :raise InputError: When input parameters has wrong data or used not correct"""
295 if not cond_key and cond_value is None:
296 cond_value = True
297 elif not cond_value and cond_key is None:
298 cond_key = True
299 elif not cond_key and not cond_value:
300 raise fmt_exc(('split()', 'cond_key', 'cond_value'),
301 must_be='at least one True or not filled',
302 received=f'{cond_key = }, {cond_value = }')
304 valid, wrong, errors = {}, {}, []
305 mapping, named = as_dict_stated(items)
306 for k, raw_item in named.items():
307 args = as_holder(raw_item, default=(None,)) if unpack else (raw_item,)
308 item = raw_item if func is None else safe(func, *args) if safely else func(*args)
309 cond_args = [x for x, state in ((k, cond_key), (item, cond_value)) if state]
310 result = item if modify else raw_item
311 if safely and is_exception(item) or bool(item := condition(*cond_args)) == cond_invert:
312 wrong[k] = result
313 errors.append(ItemError(k, raw_item, item))
314 else:
315 valid[k] = result
317 # Return result items depending on the source items type
318 if as_dicts or mapping:
319 return ValidWrong(valid, wrong, errors)
320 return ValidWrong(tuple(valid.values()), tuple(wrong.values()), errors)
323# Joins
326def join(*items, sep=', ', typecast=True, skip_false=True) -> str:
327 """Advanced str.join() method with str typecast and skip false-values possibilities
328 :arg items: Target items to join
329 :arg sep: Items separator
330 :arg typecast: Cast each item from :arg items: to str
331 :arg skip_false: Skip each item from :arg items: if it False at bool check
332 :return: Joined string"""
333 if skip_false:
334 items = tuple(filter(bool, items))
335 if typecast:
336 items = tuple(map(str, items))
337 return sep.join(items)
340def sentence(*items, sep=' ', typecast=True, skip_false=True) -> str:
341 """Make a single sentence from provided items (capitalize first letter in result)
342 :arg items: Target items to join (mostly words or phrases)
343 :arg sep: Items separator
344 :arg typecast: Cast each item from :arg args: to str
345 :arg skip_false: Skip each item from :arg args: if it False at bool check
346 :return: Joined sentence"""
347 phrase = join(*items, sep=sep, typecast=typecast, skip_false=skip_false)
348 return f'{phrase[0].upper()}{phrase[1:]}' if phrase else ''
351def sentences(*items, sep='. ', typecast=True, skip_false=True) -> str:
352 """Make a multiple sentences from provided items (capitalize first letter in each item)
353 :arg items: Target items to join (mostly finished sentences)
354 :arg sep: Items separator
355 :arg typecast: Cast each item from :arg args: to str
356 :arg skip_false: Skip each item from :arg args: if it False at bool check
357 :return: Joined sentences"""
358 phrases = tuple(sentence(x, typecast=typecast, skip_false=skip_false) for x in items)
359 return join(*phrases, sep=sep, typecast=typecast, skip_false=skip_false)
362# Data checks
365def check_input(provided: Any | None, needed: Any | None, item_name='Item', template=_TEMPL_INPUT,
366 input_exc=()) -> bool:
367 """Check provided input correctness. Error if provided XOR needed (None == False, Any == True)
368 :arg provided: Target object to check
369 :arg needed: Target object to check with
370 :arg item_name: Object name for error message
371 :arg template: String with 3 placeholders: :arg item_name:, 'not' provided, 'not' needed
372 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
373 :return: False if provided is None else True"""
374 if (is_provided := (provided is not None)) != (needed is not None):
375 args = (' not', '') if is_provided else ('', ' not')
376 msg = sentence(template.format(item_name, *args).lstrip())
377 raise fmt_exc(input_exc, msg, CheckValueError)
378 return is_provided
381def _fmt_template(template, items, item_name, item_func, sep, input_exc=()) -> str:
382 msg = sep.join(map(item_func, items))
383 match template.count('{'):
384 case 0: return template + msg
385 case 1: return template.format(fmt_name(items, item_name)) + msg
386 case 2: return template.format(fmt_name(items, item_name), msg)
387 case _: raise fmt_exc(input_exc, must_be='not more than 2 data places',
388 received=repr(template))
391def _items_out(msg1, msg2, provided, as_text, input_exc, sep='. ', **kwargs) -> holder_t | str:
392 if msg := sentences(msg1, msg2, sep=sep, typecast=False) if msg2 else msg1:
393 exc = fmt_exc(input_exc, msg, CheckValueError, **kwargs)
394 if as_text:
395 return str(exc)
396 raise exc
397 return msg if as_text else provided
400def _add_info(msg, provided, expected, item_func):
401 return {'must_be': f"{msg} expected ({', '.join(map(item_func, expected))})",
402 'received': f": {', '.join(map(item_func, provided))}"}
405def check_extra(provided: holder_t, expected: holder_t, item_name='item', item_func=repr, *,
406 item_sep=', ', as_text=False, template=_TEMPL_EXTRA, input_exc=(), **kwargs
407 ) -> holder_t | str:
408 """Check for not exists items provided (if all provided is expected)
409 :arg provided: Target objects to check
410 :arg expected: Target objects to check with
411 :arg item_name: Object name for error message
412 :arg item_func: Function to show item information for error message
413 :arg item_sep: Items separator for error message
414 :arg as_text: Not raise exception, return formatted str
415 :arg template: String with 0-2 placeholders: formatted item_name, wrong items message
416 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
417 :arg kwargs: InputError additional keyword arguments
418 :return: :arg provided: | str (if :arg as_text:)"""
419 if msg := [x for x in provided if x not in expected] or '':
420 msg = _fmt_template(template, msg, item_name, item_func, item_sep, _IE_CE__T)
421 kwargs = _add_info(_CE, provided, expected, item_func) | kwargs
422 return _items_out(msg, '', provided, as_text, input_exc, **kwargs)
425def check_absent(provided: holder_t, expected: holder_t, item_name='item', item_func=repr, *,
426 item_sep=', ', as_text=False, template=_TEMPL_ABSENT, input_exc=(), **kwargs
427 ) -> holder_t | str:
428 """Check for not enough items provided (if all expected is provided)
429 :arg provided: Target objects to check
430 :arg expected: Target objects to check with
431 :arg item_name: Object name for error message
432 :arg item_func: Function to show item information for error message
433 :arg item_sep: Items separator for error message
434 :arg as_text: Not raise exception, return formatted str
435 :arg template: String with 0-2 placeholders: formatted item_name, wrong items message
436 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
437 :arg kwargs: InputError additional keyword arguments
438 :return: :arg provided: | str (if :arg as_text:)"""
439 if msg := [x for x in expected if x not in provided] or '':
440 msg = _fmt_template(template, msg, item_name, item_func, item_sep, _IE_CA__T)
441 kwargs = _add_info(_CA, provided, expected, item_func) | kwargs
442 return _items_out(msg, '', provided, as_text, input_exc, **kwargs)
445def check_items(provided: holder_t, expected: holder_t, item_name='item', item_func=repr, *,
446 item_sep=', ', check_sep='. ', as_text=False, input_exc=(),
447 extra=True, extra_template=_TEMPL_EXTRA,
448 absent=True, absent_template=_TEMPL_ABSENT, **kwargs) -> holder_t | str:
449 """Check for not enough and not exists items provided with customizable output
450 Both checks can be disabled by absent and extra keywords and each can have custom header
451 :arg provided: Target objects to check
452 :arg expected: Target objects to check with
453 :arg item_name: Object name for error message
454 :arg item_func: Function to show item information for error message
455 :arg item_sep: Items separator for error message
456 :arg check_sep: Checks separator for error message
457 :arg as_text: Not raise exception, return formatted str
458 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
459 :arg extra: Extra check state
460 :arg extra_template: String with 0-2 placeholders: formatted item_name, wrong items message
461 :arg absent: Absent check state
462 :arg absent_template: String with 0-2 placeholders: formatted item_name, wrong items message
463 :arg kwargs: InputError additional keyword arguments
464 :return: :arg provided: | str (if :arg as_text:)"""
465 msg1, msg2 = ('', '')
466 if extra and (items := [x for x in provided if x not in expected]):
467 msg1 = _fmt_template(extra_template, items, item_name, item_func, item_sep, _IE_CI_ET)
468 if absent and (items := [x for x in expected if x not in provided]):
469 msg2 = _fmt_template(absent_template, items, item_name, item_func, item_sep, _IE_CI_AT)
470 if msg1 or msg2:
471 msg_part = _CI if extra and absent else _CE if extra else _CA
472 kwargs = _add_info(msg_part, provided, expected, item_func) | kwargs
473 return _items_out(msg1, msg2, provided, as_text, input_exc, check_sep, **kwargs)
476def check_lengths(provided: Sequence, expected: Sequence, item_name='value', item_func=repr, *,
477 item_sep=', ', as_text=False, input_exc=(),
478 extra=True, extra_template=_TEMPL_EXTRA,
479 absent=True, absent_template=_TEMPL_ABSENT, **kwargs):
480 """Check for length equality of provided and expected with customizable output
481 Both checks can be disabled by absent and extra keywords and each can have custom header
482 :arg provided: Target objects sequence to check
483 :arg expected: Target objects sequence to check with
484 :arg item_name: Object name for error message
485 :arg item_func: Function to show item information for error message
486 :arg item_sep: Items separator for error message
487 :arg as_text: Not raise exception, return formatted str
488 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
489 :arg extra: Extra check state
490 :arg extra_template: String with 0-2 placeholders: formatted item_name, wrong items message
491 :arg absent: Absent check state
492 :arg absent_template: String with 0-2 placeholders: formatted item_name, wrong items message
493 :arg kwargs: InputError additional keyword arguments
494 :return: :arg provided: | str (if :arg as_text:)"""
495 msg = ''
496 if (pl := len(provided)) != (nl := len(expected)):
497 state, template, obj, i = ((extra, extra_template, provided, nl) if pl > nl else
498 (absent, absent_template, expected, pl))
499 if state:
500 msg = _fmt_template(template, obj[i:], item_name, item_func, item_sep, _IE_CL__T)
501 kwargs = {'must_be': f'{nl} {fmt_name(expected, item_name, f"{item_name}s")} long',
502 'received': str(pl)} | kwargs
503 return _items_out(msg, '', provided, as_text, input_exc, **kwargs)
506def check_type(obj: object, obj_t: mb_holder_t[type], typecast=False, name='', obj_t_check=True,
507 input_exc=(), raw=False, *raw_args):
508 """Check object type(s), with optional typecast if other type provided, dicts NOT supported
509 :arg obj: Target object to check
510 :arg obj_t: Target object type or types to check with
511 :arg typecast: :arg obj: type casting to :arg obj_t: (if wrong type)
512 :arg name: :arg obj: name ('object', 'item', etc.) for error message
513 :arg obj_t_check: Check :arg obj_t: that types provided
514 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
515 :arg raw: Return not raised exception at error, with not formatted parts in args
516 :arg raw_args: Additional args before not formatted parts in args (if :arg raw:)
517 :return: :arg obj: | typecast-ed :arg obj: (if :arg typecast:) | CheckTypeError
518 :raise InputError: If wrong arguments provided
519 :raise InputError: If check failed and filled :arg input_exc: provided (upper-level error)
520 :raise CheckTypeError: If check failed and :arg input_exc: not (or empty) provided"""
521 # Check simple
522 # bug mypy: valid type or types for isinstance
523 if isinstance(obj, obj_types := _types(obj_t, 'check_type()') if obj_t_check else obj_t): # type: ignore[arg-type]
524 return obj
526 # Check with typecast if enabled
527 error = ''
528 obj_types = as_holder(obj_types)
529 if typecast:
530 for obj_type in obj_types:
531 if isinstance(result := safe(obj_type, obj), obj_type):
532 return result
534 error += f', typecast to {obj_type.__name__}: {result!r}'
535 if not isinstance(result, Exception):
536 error += f' ({type(obj).__name__})'
538 # Format error
539 name = sentence(f'{name} ') if name else ''
540 must_be = f'{" or ".join(x.__name__ for x in obj_types)} {fmt_name(tuple(obj_types), "type")}'
541 msg = f"{name}{with_type(obj)} must be {must_be}{error}"
542 if raw:
543 return CheckTypeError(*raw_args, obj, obj_t, msg, name, must_be, error)
544 raise fmt_exc(input_exc, msg, CheckTypeError)
547def _build_ct_ie_kw(no_info):
548 return {
549 'input_exc': ('check_types()', 'obj', {'must_be': '', 'received': ''} if no_info else {}),
550 'extra_template': "Extra {} (without type): ",
551 'absent_template': "Absent {} (type provided): "}
554_CT_IE_MAPS = _build_ct_ie_kw(True)
555_CT_IE_PAIRS = _build_ct_ie_kw(False)
558def check_types(obj: mb_holder_t[object], obj_t: mb_holder_t[type], typecast=False, item_name='',
559 *, one_obj=False, pairs=False, strict=True, obj_t_check=True, input_exc=()):
560 """Check object(s) type(s), with optional typecast if other type provided, dicts supported
561 :arg obj: Target object or objects to check
562 :arg obj_t: Target object type or types to check with
563 :arg typecast: :arg obj: type casting to :arg obj_t: (if wrong type)
564 :arg item_name: Common item name ('object', 'item', etc.) for error message
565 :arg one_obj: Check :arg obj: as single object, even if any holder type provided
566 :arg pairs: Zip :arg obj: and :arg obj_t: for pairs check (must be strict lengths)
567 :arg strict: At :arg pairs: or both mappings detect :arg obj: absent keys/values
568 :arg obj_t_check: Check :arg obj_t: that types provided
569 :arg input_exc: InputError args tuple: (func_name: str, *items: str, kwargs: dict)
570 :return: :arg obj: | typecast-ed :arg obj: (if :arg typecast:) | CheckTypeError
571 :raise InputError: If wrong arguments provided
572 :raise InputError: If check failed and filled :arg input_exc: provided (upper-level error)
573 :raise CheckTypeError: If check failed and :arg input_exc: not (or empty) provided"""
574 # Check mutually exclusive input parameters
575 if one_obj and pairs:
576 raise fmt_exc(('check_types()', 'one_obj', 'pairs'),
577 must_be='not more than one True', received='both')
579 # Check input obj
580 obj_is_holder, objects = (False, (obj,)) if one_obj else as_holder_stated(obj)
581 if not objects or any(x is None for x in objects):
582 raise fmt_exc(('check_types()', 'obj'), must_be='one or more objects without None',
583 received=f"{obj = !r}{'' if obj == objects else f' ({objects = !r})'}")
585 # Check single object
586 if not obj_is_holder:
587 if obj_t_check: 587 ↛ 589line 587 didn't jump to line 589, because the condition on line 587 was never false
588 _types(obj_t, 'check_types()')
589 return check_type(obj, obj_t, typecast, item_name, False, input_exc)
591 # Prepare common part for datasets
592 obj_types = _types(obj_t, 'check_types()') if obj_t_check else as_holder(obj_t)
593 obj_t_mapping = isinstance(obj_t, Mapping)
594 obj_mapping, named = as_dict_stated(objects)
595 maps = obj_mapping and obj_t_mapping
597 # Add args to check_type: typecast, name, obj_t_check, input_exc, raw
598 add_args = (typecast, item_name, False, (), True)
600 # Build dataset
601 if maps: # Value-type pairs by its names if both are mappings
602 check_items(named, obj_types, absent=strict, item_name='key', **_CT_IE_MAPS)
603 # bug mypy: obj_types is Mapping in that case, so it has get method
604 dataset = [(v, obj_types.get(k), *add_args, k) for k, v in named.items()] # type: ignore[attr-defined]
606 elif pairs: # Value-type pairs by lengths if enabled
607 # bug mypy: obj_types is Mapping in that case, so it has values method
608 obj_t_vals = obj_types.values() if obj_t_mapping else obj_types # type: ignore[attr-defined]
609 check_lengths(tuple(objects), obj_t_vals, absent=strict, item_name='value', **_CT_IE_PAIRS)
610 dataset = [(v, t, *add_args, ki) for (ki, v), t in zip(named.items(), obj_t_vals)]
612 else: # Common type(s) for all values
613 dataset = [(v, obj_types, *add_args, ki) for ki, v in named.items()]
615 # Check multiple objects
616 exceptions, valid = split(dataset, is_exception, check_type, unpack=True, modify=True)
618 # Raise formatted error message if errors exists
619 if exceptions:
620 msg = f'Several {item_name or "object"}s'
621 errors = [ItemError(*x.args[:2]) for x in exceptions]
622 raw = exceptions[0].args
624 # Format error(s)
625 if len(exceptions) == 1: # Single error - as single
626 info = fmt_obj_errors(obj, errors, True, True, obj_mapping)
627 msg = f'{raw[-3]}{info[0]} must be {raw[-2]}{raw[-1]}'
629 elif maps or pairs: # Several errors, separate type - as newline
630 info = fmt_obj_errors(obj, errors, True, True, obj_mapping, False, '', str, ': ')
631 msg += '\n\t'.join((' has a wrong type:',
632 *[f'{raw[-3]}{x} must be {e.args[-2]}{e.args[-1]}'
633 for x, e in zip(info, exceptions, strict=True)]))
635 else: # Several errors, common type(s) - as inline
636 info = fmt_obj_errors(obj, errors, True, True, obj_mapping, False, '', repr, '=')
637 result = ", ".join(f"{x}{e.args[-1]}" for x, e in zip(info, exceptions, strict=True))
638 msg += f' are not {raw[-2]}: {result}'
640 raise fmt_exc(input_exc, msg, CheckTypeError)
642 # Return input (or typecast-ed) value
643 if not typecast:
644 return obj
645 # note mypy: typecast is user selectable, so if an error occurs - it is considered as scheduled
646 return type(obj)(zip((x[-1] for x in dataset), valid, strict=True) if obj_mapping else valid) # type: ignore[call-arg]
649# Decorators
652def decorate_methods(decorator: Callable, exclude: mb_holder_t[str] | Callable = is_hidden):
653 """Class decorator to apply provided decorator to non-excluded methods
654 :arg decorator: Target decorator for methods
655 :arg exclude: Excluded from decoration class methods (default: hidden methods are excluded)
656 :return: Class with decorated methods"""
657 if callable(exclude):
658 excluded = exclude
659 else:
660 exclude = as_holder(exclude)
662 def excluded(x):
663 return x in exclude
665 def decorate(cls):
666 for name, attr in cls.__dict__.items():
667 if not excluded(name) and callable(attr):
668 setattr(cls, name, decorator(attr))
669 return cls
671 # Decorator is called only (usage without calling is useless and wrong)
672 return decorate
675def init_reraise(entity_name: str, *get_name_args, **get_name_kwargs):
676 """Decorator for __init__ method (reraise basic error info at exception with InitError)
677 :arg entity_name: Class objects common name, for error message
678 :arg get_name_args: Class object GetName args, for error message
679 :arg get_name_kwargs: Class object GetName kwargs, for error message
680 :return: Decorated __init__ method
681 :raise InitError: If error in __init__ method occurred"""
682 def init_decorator(__init__):
683 def init_wrapper(self, *args, **kwargs):
684 try:
685 __init__(self, *args, **kwargs)
686 except Exception as e:
687 target_name = GetName(self, *get_name_args, **get_name_kwargs)
688 view = ", ".join((*map(repr, args), *fmt_dict(kwargs)))
689 msg = f'Cannot init {entity_name} {target_name!r} (self.__init__({view}))'
690 raise InitError(msg) from e
691 return init_wrapper
693 # Decorator is not called - entity_name is class
694 if not isinstance(entity_name, str) and not get_name_args and not get_name_kwargs:
695 cls, entity_name = entity_name, 'class'
696 return init_decorator(cls)
698 # Decorator is called
699 return init_decorator
702def set_slots_defaults(field_names: mb_holder_t[str] = (),
703 field_func: Callable = _field_func_default,
704 fields_t: type | None = None):
705 """@dataclass(slots=True) alternative, that allows set slots with provided default values
706 :arg field_names: Strict field name or names (if needed)
707 :arg field_func: Condition for getting fields from class attributes
708 :arg fields_t: Common fields type
709 :return: Decorated class with slots defaults
710 :raise InputError: If wrong arguments provided"""
711 def decorator(cls: type):
712 name = cls.__name__
713 # bug mypy: mapping proxy is support '|' operand with dict
714 base = type.__dict__ | {'__weakref__': None} # type: ignore[operator]
715 differ = {k: v for k, v in cls.__dict__.items() if k not in base or base[k] != v}
716 condition = lambda k, v: k in field_names or k not in base and field_func(k, v) # noqa
717 fields, attrs = split(differ, cond_key=True, cond_value=True, condition=condition)
719 types = {k: fields_t for k in fields} if fields_t else getattr(cls, '__annotations__', {})
720 type_kw = {'__slots__': tuple(fields)} | ({'__annotations__': types} if types else {})
722 def __repr__(self):
723 return f'{name}({", ".join(f"{k}={getattr(self, k, None)}" for k in fields)})'
725 def __eq__(self, other):
726 return repr(self) == repr(other)
728 def __init__(self, *args, **kwargs):
729 # Input checks
730 check_lengths(args, fields, absent=False, input_exc=(f'{name}()', '*args'))
731 if kwargs:
732 check_extra(kwargs, fields, input_exc=(f'{name}()', '**kwargs'))
734 if args and kwargs:
735 unfilled = tuple(fields)[len(args):]
736 if left := [x for x in kwargs if x not in unfilled]:
737 raise fmt_exc((f'{name}()', '*args', '**kwargs'),
738 f'Already provided item in args: {", ".join(left)}')
740 if fields_t:
741 if args:
742 check_types(args, fields_t, input_exc=(f'{name}()', '*args'))
743 if kwargs:
744 check_types(kwargs, fields_t, input_exc=(f'{name}()', '**kwargs'))
746 # Fill fields by defaults | args | kwargs
747 if args:
748 kwargs = dict(zip(fields, args)) | kwargs
749 [_FORCE_SET(self, k, v) for k, v in (fields | kwargs).items()]
751 # Call post init as in dataclass
752 if post_init := attrs.get('__post_init__'):
753 post_init(self)
755 new_cls = type(cls.__name__, (), attrs | type_kw)
756 # note mypy: skip static checks
757 new_cls.__repr__ = __repr__ # type: ignore[method-assign, assignment]
758 new_cls.__init__ = __init__ # type: ignore[misc]
759 new_cls.__eq__ = __eq__ # type: ignore[method-assign, assignment]
760 return new_cls
762 # Decorator is not called
763 if isinstance(field_names, type) and field_func == _field_func_default and fields_t is None:
764 _cls, field_names = field_names, ()
765 return decorator(_cls)
767 # Decorator is called with or without parameters
768 if field_names:
769 check_types(field_names, str, input_exc=('@set_slots_defaults()', 'field_names'))
770 field_names = as_holder(field_names)
771 if field_func != _field_func_default:
772 # bug mypy: Special forms is not supported yet (https://github.com/python/mypy/issues/9773)
773 check_type(field_func, Callable, input_exc=('@set_slots_defaults()', 'field_func')) # type: ignore[arg-type]
774 if fields_t:
775 check_type(fields_t, type, input_exc=('@set_slots_defaults()', 'fields_t'))
776 return decorator
779# Getters
782def get_cls_obj(obj: object | type) -> ClsObj:
783 """Get class and object from target class or object, as NamedTuple with cls and obj attrs
784 :arg obj: Target object for getting
785 :return: NamedTuple with cls and obj attrs"""
786 if obj is None:
787 return ClsObj(None, None)
788 elif isinstance(obj, type):
789 return ClsObj(obj, None)
790 return ClsObj(type(obj), obj)
793def get_cls_attr(obj: object | type, attr: str):
794 """Get class attribute from target class or object
795 :arg obj: Target object for getting
796 :arg attr: Class attribute name
797 :return: Class attribute value | None (if not exists)"""
798 return None if (cls := get_cls_obj(obj).cls) is None else getattr(cls, attr)
801def get_attrs(obj: object | type, skip_parent: int = 0, skip_child: int = 0, *,
802 internal=False, dunder=False, ignored: Iterable[type] = (type, object),
803 merge: Iterable[str] = ('__annotations__', '__slots__')) -> dict[str, Any]:
804 """Get attributes by exploring method resolution order, with optional parent coerce
805 By default merges __annotations__ dict and __slots__ tuple
806 :arg obj: Target object for getting
807 :arg skip_parent: Classes count starting from parent to skip
808 :arg skip_child: Classes count starting from child to skip
809 :arg internal: Include internal attrs (starting from '_', except __dunder__)
810 :arg dunder: Include __dunder__ attrs
811 :arg ignored: Ignored classes (default: type and object)
812 :arg merge: Merge provided methods values from several classes
813 :return: Dict with attributes
814 :raise InputError: If wrong arguments provided"""
815 if obj is None:
816 raise fmt_exc(('get_attrs()', 'obj'), must_be='not None', received='None')
817 if ignored != (type, object) and ignored:
818 check_types(ignored, type, input_exc=('get_attrs()', 'ignored'))
820 # Prepare MRO
821 obj_t = obj if (is_cls := isinstance(obj, type)) else type(obj)
822 mro = [x for x in type.mro(obj_t) if x not in ignored]
824 # Check for skip correctness
825 if (parents := len(mro)) <= (skip_total := skip_parent + skip_child):
826 if not parents:
827 raise fmt_exc(('get_attrs()', 'obj'),
828 must_be='not ignored type', received=with_type(obj),
829 ignored_types=f': {", ".join(x.__name__ for x in ignored)}')
830 raise fmt_exc(('get_attrs()', 'skip_parent', 'skip_child'),
831 must_be=f'less than {parents} skipped in total',
832 skipped=f': {skip_total} ({skip_parent = }, {skip_child = })')
834 # Build filtered MRO from coerced MRO and from object (if not class provided) in first order
835 mro_flt = [x.__dict__ for x in mro[skip_child:len(mro) - skip_parent]]
836 if not is_cls:
837 # bug mypy: Union[Any, Dict[Any, Any]]?..
838 mro_flt.insert(0, getattr(obj, '__dict__', # type: ignore[arg-type]
839 {k: v for k in obj.__dir__()
840 if (v := getattr(obj, k, _UNIQUE)) is not _UNIQUE}))
841 # bug mypy: Why MutableMapping?..
842 coerced: ChainMap[str, Any] = ChainMap(*mro_flt) # type: ignore[arg-type]
844 # Filter hidden if needed
845 if dunder and internal:
846 result = dict(coerced)
847 else:
848 result = {k: v for k, v in coerced.items()
849 if (dunder or not is_dunder(k)) and (internal or not is_internal(k))}
851 # Merge provided dicts or iterables
852 for k in merge:
853 if k in result and (attrs := [y for x in coerced.maps if is_holder(y := x.get(k))]):
854 if isinstance(attrs[-1], Mapping):
855 # bug mypy: Why MutableMapping?..
856 result[k] = dict(ChainMap(*attrs)) # type: ignore[arg-type]
857 else:
858 # bug mypy: Optional? Type[None]?..
859 result[k] = type(attrs[-1])(dict.fromkeys(chain(*attrs[::-1]))) # type: ignore[arg-type, misc]
860 return result
863class _GNMethod(str):
864 # bug mypy: MappingProxyType as default dict arg
865 def __new__(cls, first_or_cls, obj=_UNIQUE, attrs: dict = MappingProxyType({})): # type: ignore[assignment]
866 inst = str.__new__(cls, obj or first_or_cls if (pair := obj != _UNIQUE) else first_or_cls)
867 inst.__dict__ |= attrs | (dict(zip(_CLS_OBJ, (first_or_cls, obj))) if pair else {})
868 return inst
871class _GNClassMethod:
872 def __init__(self, decorated):
873 self.func = decorated
874 self.args = decorated.__code__.co_argcount - 1
876 def __call__(self, target, *args):
877 cls_args, obj_args = args, args
878 if (received := len(args)) and received != self.args:
879 if received != self.args * 2: 879 ↛ 886line 879 didn't jump to line 886, because the condition on line 879 was never false
880 args_names = self.func.__code__.co_varnames[1:self.args + 1]
881 must_be = (f'{self.args} (same for both) or {self.args * 2} (separately) '
882 'provided for cls and obj') if self.args else 'no arguments'
883 raise fmt_exc((f'GetName.{self.func.__code__.co_name}()', *args_names),
884 force_header=True, must_be=must_be,
885 received=f'{received}: {", ".join(map(repr, args))}')
886 cls_args, obj_args = args[:self.args], args[self.args:]
888 cls, obj = get_cls_obj(target)
889 return _GNMethod('' if cls is None else self.func(cls, *cls_args),
890 '' if obj is None else self.func(obj, *obj_args))
892 def __repr__(self):
893 return f'GetName.{self.func.__code__.co_name}'
896def _flt_std_name(target: object | type, name: str) -> str:
897 return '' if get_cls_attr(target, '__qualname__') in name else name
900@decorate_methods(_GNClassMethod)
901class _GNMethods:
902 def doc(self: Any) -> str:
903 """Get first line in __doc__ attr as name"""
904 return doc.split('\n')[0] if (doc := getattr(self, '__doc__', None)) else ''
906 def code(self: Any) -> str:
907 """Get __code__.co_name attr as name"""
908 return getattr(co, 'co_name', '') if (co := getattr(self, '__code__', None)) else ''
910 def attrs(self: Any, attrs: Iterable[str] = _GN_ATTRS) -> str:
911 """Get first from default or provided attrs as name"""
912 if attrs:
913 if attrs != _GN_ATTRS:
914 check_types(attrs, str)
916 for attr in attrs:
917 if result := getattr(self, attr, ''):
918 if result := safe(str, result, _exc_=''): 918 ↛ 916line 918 didn't jump to line 916, because the condition on line 918 was never false
919 return result
920 return ''
922 def repr(self: Any) -> str:
923 """Get __str__ attr as name"""
924 return safe(self.__repr__, _res_=partial(_flt_std_name, self), _exc_='')
926 def str(self: Any) -> str:
927 """Get __repr__ attr as name"""
928 return safe(self.__str__, _res_=partial(_flt_std_name, self), _exc_='')
931_GN_METHODS = get_attrs(_GNMethods)
932_GN_METHODS_EMPTY = {k: _GNMethod('', '') for k in _GN_METHODS}
933_GN_POSSIBLE_PARAMS = {'possible_parameters': f': {", ".join(_GN_ORDER)}'}
936@dataclass(slots=True)
937class _GNMethodHolder:
938 value: str
939 state: bool
940 args: tuple
941 method: _GNClassMethod
944class GetName(str, _GNMethods):
945 """Get first available name from provided target using multiple configurable ways
946 By default - only first of cls and obj names will be received (full=False)
947 Order of get methods can be changed, set of all get methods names is not required
948 Any get method can be enabled/disabled by bool ('{method}', '{method}_cls', '{method}_obj')
949 Attrs get method ('attrs', 'attrs_cls', 'attrs_obj') can have names tuple instead of bool
950 If None obj provided - 'none' arg value will be returned, as GetName (if str type)
951 All get methods failed - 'unknown' arg value will be returned, as GetName (if str type)"""
952 cls: _GNMethod
953 obj: _GNMethod
955 def __new__(cls, obj, doc=False, code=False, attrs: bool | tuple[holder_t[str]] = True,
956 str=True, repr=True, *, full=False, order=_GN_ORDER, none='', unknown='', # noqa
957 **kwargs):
958 """
959 :arg obj: Target object for getting
960 :arg doc: Enable cls and obj get method, first line in __doc__ attr as name
961 :arg code: Enable cls and obj get method, __code__.co_name attr as name
962 :arg attrs: Enable cls and obj get method, first from default or provided attrs as name
963 :arg str: Enable cls and obj get method, __str__ attr as name
964 :arg repr: Enable cls and obj get method, __repr__ attr as name
965 :arg full: Get all enabled methods and fill (default: only first in cls and obj)
966 :arg order: Get methods order, cls or obj required ('{method}_cls', '{method}_obj')
967 :arg none: Return value if :arg obj: is None, typecast-ed to GetName (if str type)
968 :arg unknown: Return value if all methods failed, typecast-ed to GetName (if str type)
969 :arg kwargs: Enable cls or obj get method separately ('{method}_cls', '{method}_obj')"""
971 # Filter only methods states/args from internally used arguments
972 left = {k: v for k, v in locals().items() if k not in _GN_INTERNAL}
973 holders: dict[str, _GNMethodHolder] = {}
974 for k, method in _GN_METHODS.items():
975 value = cls._get_method_holder(k, method, left.pop(k))
976 value2 = _GNMethodHolder(value.value, value.state, value.args, value.method) # copy
977 holders |= {f'{k}_cls': value, f'{k}_obj': value2}
978 if left: 978 ↛ 979line 978 didn't jump to line 979, because the condition on line 978 was never true
979 raise InternalError(f'Found not used args in GetName with {obj=!r}: {left}')
981 # Rewrite separately (cls/obj) provided methods states/args
982 if kwargs:
983 if wrongs := {k: v for k, v in kwargs.items() if k not in holders.keys()}:
984 # bug mypy: unpacked keyword arguments is not a positional argument..
985 raise fmt_exc(('GetName()', *wrongs.keys()), **_GN_POSSIBLE_PARAMS) # type: ignore[arg-type]
986 for k, v in kwargs.items(): 986 ↛ 990line 986 didn't jump to line 990, because the loop on line 986 didn't complete
987 holders[k] = cls._get_method_holder(k, holders[k].method, v)
989 # Check order
990 if order != _GN_ORDER:
991 check_extra(order, _GN_ORDER, item_name='method', template='Not exists {}: ',
992 input_exc=('GetName()', 'order', {'must_be': ''} | _GN_POSSIBLE_PARAMS))
994 # Return none if None target provided
995 if obj is None:
996 return cls._try_type_cast(none, full)
998 # Get names (all or only first in cls and obj) by enabled methods in provided order
999 first, first_cls_obj, left_set = '', dict.fromkeys(_CLS_OBJ, ''), set(_CLS_OBJ)
1000 targets = get_cls_obj(obj)
1001 if targets.obj is None:
1002 left_set.discard('obj')
1003 for method_name in order:
1004 if (cls_or_obj := method_name.rsplit('_', 1)[1]) in left_set:
1005 if (holder := holders[method_name]).state:
1006 if name := holder.method.func(getattr(targets, cls_or_obj), *holder.args):
1007 holder.value = name
1008 if not first:
1009 first = name
1010 if not first_cls_obj[cls_or_obj]:
1011 first_cls_obj[cls_or_obj] = name
1012 if not full:
1013 left_set.discard(cls_or_obj)
1014 if not left_set:
1015 break
1017 # Return unknown if no name found
1018 if not first:
1019 return cls._try_type_cast(unknown, full)
1021 # Fast names receive way (only first in cls and obj methods), without GetName instance fill
1022 if not full:
1023 methods = {k: '' for k in _GN_METHODS}
1024 # note mypy: that's planned
1025 return _GNMethod.__new__(cls, first, attrs=first_cls_obj | methods) # type: ignore[arg-type]
1027 # GetName instance fill by first cls and obj methods and each method separately
1028 methods = {k: _GNMethod(holders[f'{k}_cls'].value, holders[f'{k}_obj'].value)
1029 for k in _GN_METHODS}
1030 # note mypy: that's planned
1031 return _GNMethod.__new__(cls, first, attrs=first_cls_obj | methods) # type: ignore[arg-type]
1033 @staticmethod
1034 def _get_method_holder(k, method, value) -> _GNMethodHolder:
1035 check_type(value, (bool, tuple) if method.args else bool, input_exc=('GetName()', k))
1036 return _GNMethodHolder('', bool(value), value if isinstance(value, tuple) else (), method)
1038 @classmethod
1039 def _try_type_cast(cls, value, full):
1040 if isinstance(value, str):
1041 attrs = _GN_METHODS_EMPTY if full else {}
1042 return _GNMethod.__new__(cls, value, attrs=attrs | dict.fromkeys(_CLS_OBJ, '')) # noqa
1043 return value
1046# Uncategorized
1049class UID:
1050 """Unique ID instances used as filler for detecting by comparison"""
1051 __slots__ = ('name',)
1052 name: str
1053 exists: list[str] = []
1055 def __init__(self, name: str):
1056 """
1057 :arg name: Unique name for ID"""
1058 if (name := str(name)) in self.exists:
1059 raise ValueError(f'{name!r} unique ID cannot be created, it already exists')
1060 self.exists.append(name)
1061 self.name = name
1063 def __repr__(self):
1064 return f'UID({self.name!r})'
1066 def __str__(self):
1067 return f'{self.name} ID'
1070class Locker:
1071 """Lock child class from attributes manipulations with internal unlock possibility
1072 Can be unlocked during active context manager, as it finished - class will be also locked"""
1073 __slots__ = ('_locker_state', '_locker_enter_state', '_locker_set_attr', '_locker_del_attr',
1074 '_locker_ignored', '_locker_name')
1075 _locker_state: bool
1076 _locker_enter_state: bool
1077 _locker_set_attr: bool
1078 _locker_del_attr: bool
1079 _locker_ignored: tuple
1080 _locker_name: str
1082 def __new__(cls, *args, **kwargs):
1083 obj = object.__new__(cls)
1084 [_FORCE_SET(obj, *x) for x in zip(Locker.__slots__, (False, False, True, True, ()))]
1085 return obj
1087 def __init__(self, *ignored_attrs: str, set_attr=True, del_attr=True, name: str | None = None):
1088 """
1089 :arg ignored_attrs: Attributes names that will not be locked at all
1090 :arg set_attr: Allow __set__ unlocked attributes
1091 :arg del_attr: Allow __del__ unlocked attributes
1092 :arg name: Set locker name for error message"""
1093 self._locker_ignored += ignored_attrs
1094 name = f'{GetName(self)!r} object' if name is None else str(name)
1095 _FORCE_SET(self, '_locker_name', name)
1096 _FORCE_SET(self, '_locker_set_attr', set_attr)
1097 _FORCE_SET(self, '_locker_del_attr', del_attr)
1098 self._locker_state = True
1100 @staticmethod
1101 def _get_exc_msg(is_set, allowed):
1102 if allowed:
1103 return is_set, 'is locked for changes'
1104 else:
1105 return is_set, f'does not support {"setting" if is_set else "deleting"} attributes'
1107 def _make_exc(self, is_set, msg, key, value=None):
1108 op, value = ('set', f', {value!r}') if is_set else ('del', '')
1109 return TypeError(f'{self._locker_name} {msg} (self.__{op}attr__({key!r}{value}))')
1111 def is_unlocked(self, key):
1112 """Get True if provided attribute is unlocked"""
1113 return not self._locker_state or key in self._locker_ignored
1115 def __setattr__(self, key, value):
1116 if key in _LOCKER_IGNORED or self._locker_set_attr and self.is_unlocked(key):
1117 return _FORCE_SET(self, key, value)
1118 raise self._make_exc(*self._get_exc_msg(True, self._locker_set_attr), key, value)
1120 def __delattr__(self, key):
1121 if not hasattr(self, key): # msg as at set any new attrs in slotted class
1122 raise AttributeError(f'{self._locker_name} has no attribute {key!r}')
1123 if key in Locker.__slots__:
1124 raise self._make_exc(False, 'Locker attrs deletion is forbidden', key)
1125 if self._locker_del_attr and self.is_unlocked(key):
1126 return object.__delattr__(self, key)
1127 raise self._make_exc(*self._get_exc_msg(False, self._locker_del_attr), key)
1129 def __enter__(self):
1130 self._locker_enter_state, self._locker_state = self._locker_state, False
1131 return self
1133 def __exit__(self, exc_type, exc_val, exc_tb):
1134 if self._locker_enter_state:
1135 self._locker_state = True