Coverage for configlayer/utils.py: 99%

531 statements  

« 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 

9 

10from .types import state_t, holder_t, mb_holder_t, ClsObj, ItemError, ValidWrong 

11from .exceptions import InternalError, InputError, InitError, CheckValueError, CheckTypeError 

12 

13 

14_T = TypeVar('_T') 

15 

16_TEMPL_INPUT = '{} is{} needed, but{} provided' 

17_TEMPL_ABSENT = 'Absent {}: ' 

18_TEMPL_EXTRA = 'Extra {}: ' 

19 

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') 

25 

26_CE = 'not more than' 

27_CA = 'not less than' 

28_CI = 'equal to' 

29 

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') 

34 

35_CLS_OBJ = ('cls', 'obj') 

36_UNIQUE = object() 

37_FORCE_SET = object.__setattr__ 

38 

39_LOCKER_IGNORED = ('_locker_state', '_locker_enter_state') 

40 

41 

42# Internal 

43 

44 

45def _field_func_default(k, v): # noqa 

46 return not callable(v) 

47 

48 

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 

57 

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) 

66 

67 

68# Bool checks 

69 

70 

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] != '_' 

76 

77 

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) 

83 

84 

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('_') 

90 

91 

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) 

98 

99 

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 

115 

116 

117# Type casting 

118 

119 

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] 

128 

129 

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] 

140 

141 

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

151 

152 

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

164 

165 

166# Common 

167 

168 

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_ 

183 

184 

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))})" 

197 

198 

199# Formatters 

200 

201 

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

210 

211 

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), 

237 

238 

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) 

251 

252 

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) 

265 

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) 

269 

270 

271# Split 

272 

273 

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 = }') 

303 

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 

316 

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) 

321 

322 

323# Joins 

324 

325 

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) 

338 

339 

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

349 

350 

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) 

360 

361 

362# Data checks 

363 

364 

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 

379 

380 

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

389 

390 

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 

398 

399 

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))}"} 

403 

404 

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) 

423 

424 

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) 

443 

444 

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) 

474 

475 

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) 

504 

505 

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 

525 

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 

533 

534 error += f', typecast to {obj_type.__name__}: {result!r}' 

535 if not isinstance(result, Exception): 

536 error += f' ({type(obj).__name__})' 

537 

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) 

545 

546 

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): "} 

552 

553 

554_CT_IE_MAPS = _build_ct_ie_kw(True) 

555_CT_IE_PAIRS = _build_ct_ie_kw(False) 

556 

557 

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') 

578 

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})'}") 

584 

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) 

590 

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 

596 

597 # Add args to check_type: typecast, name, obj_t_check, input_exc, raw 

598 add_args = (typecast, item_name, False, (), True) 

599 

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] 

605 

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)] 

611 

612 else: # Common type(s) for all values 

613 dataset = [(v, obj_types, *add_args, ki) for ki, v in named.items()] 

614 

615 # Check multiple objects 

616 exceptions, valid = split(dataset, is_exception, check_type, unpack=True, modify=True) 

617 

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 

623 

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]}' 

628 

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)])) 

634 

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}' 

639 

640 raise fmt_exc(input_exc, msg, CheckTypeError) 

641 

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] 

647 

648 

649# Decorators 

650 

651 

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) 

661 

662 def excluded(x): 

663 return x in exclude 

664 

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 

670 

671 # Decorator is called only (usage without calling is useless and wrong) 

672 return decorate 

673 

674 

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 

692 

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) 

697 

698 # Decorator is called 

699 return init_decorator 

700 

701 

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) 

718 

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 {}) 

721 

722 def __repr__(self): 

723 return f'{name}({", ".join(f"{k}={getattr(self, k, None)}" for k in fields)})' 

724 

725 def __eq__(self, other): 

726 return repr(self) == repr(other) 

727 

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')) 

733 

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)}') 

739 

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')) 

745 

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

750 

751 # Call post init as in dataclass 

752 if post_init := attrs.get('__post_init__'): 

753 post_init(self) 

754 

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 

761 

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) 

766 

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 

777 

778 

779# Getters 

780 

781 

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) 

791 

792 

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) 

799 

800 

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')) 

819 

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] 

823 

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 = })') 

833 

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] 

843 

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))} 

850 

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 

861 

862 

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 

869 

870 

871class _GNClassMethod: 

872 def __init__(self, decorated): 

873 self.func = decorated 

874 self.args = decorated.__code__.co_argcount - 1 

875 

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:] 

887 

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

891 

892 def __repr__(self): 

893 return f'GetName.{self.func.__code__.co_name}' 

894 

895 

896def _flt_std_name(target: object | type, name: str) -> str: 

897 return '' if get_cls_attr(target, '__qualname__') in name else name 

898 

899 

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

905 

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

909 

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) 

915 

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

921 

922 def repr(self: Any) -> str: 

923 """Get __str__ attr as name""" 

924 return safe(self.__repr__, _res_=partial(_flt_std_name, self), _exc_='') 

925 

926 def str(self: Any) -> str: 

927 """Get __repr__ attr as name""" 

928 return safe(self.__str__, _res_=partial(_flt_std_name, self), _exc_='') 

929 

930 

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)}'} 

934 

935 

936@dataclass(slots=True) 

937class _GNMethodHolder: 

938 value: str 

939 state: bool 

940 args: tuple 

941 method: _GNClassMethod 

942 

943 

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 

954 

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')""" 

970 

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}') 

980 

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) 

988 

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

993 

994 # Return none if None target provided 

995 if obj is None: 

996 return cls._try_type_cast(none, full) 

997 

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 

1016 

1017 # Return unknown if no name found 

1018 if not first: 

1019 return cls._try_type_cast(unknown, full) 

1020 

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] 

1026 

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] 

1032 

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) 

1037 

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 

1044 

1045 

1046# Uncategorized 

1047 

1048 

1049class UID: 

1050 """Unique ID instances used as filler for detecting by comparison""" 

1051 __slots__ = ('name',) 

1052 name: str 

1053 exists: list[str] = [] 

1054 

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 

1062 

1063 def __repr__(self): 

1064 return f'UID({self.name!r})' 

1065 

1066 def __str__(self): 

1067 return f'{self.name} ID' 

1068 

1069 

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 

1081 

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 

1086 

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 

1099 

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' 

1106 

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}))') 

1110 

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 

1114 

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) 

1119 

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) 

1128 

1129 def __enter__(self): 

1130 self._locker_enter_state, self._locker_state = self._locker_state, False 

1131 return self 

1132 

1133 def __exit__(self, exc_type, exc_val, exc_tb): 

1134 if self._locker_enter_state: 

1135 self._locker_state = True