codekingpro/portable-devtools
114k
1# SPDX-License-Identifier: MIT2 3 4import copy5 6from ._compat import get_generic_base7from ._make import _OBJ_SETATTR, NOTHING, fields8from .exceptions import AttrsAttributeNotFoundError9 10 11_ATOMIC_TYPES = frozenset(12 {13 type(None),14 bool,15 int,16 float,17 str,18 complex,19 bytes,20 type(...),21 type,22 range,23 property,24 }25)26 27 28def asdict(29 inst,30 recurse=True,31 filter=None,32 dict_factory=dict,33 retain_collection_types=False,34 value_serializer=None,35):36 """37 Return the *attrs* attribute values of *inst* as a dict.38 39 Optionally recurse into other *attrs*-decorated classes.40 41 Args:42 inst: Instance of an *attrs*-decorated class.43 44 recurse (bool): Recurse into classes that are also *attrs*-decorated.45 46 filter (~typing.Callable):47 A callable whose return code determines whether an attribute or48 element is included (`True`) or dropped (`False`). Is called with49 the `attrs.Attribute` as the first argument and the value as the50 second argument.51 52 dict_factory (~typing.Callable):53 A callable to produce dictionaries from. For example, to produce54 ordered dictionaries instead of normal Python dictionaries, pass in55 ``collections.OrderedDict``.56 57 retain_collection_types (bool):58 Do not convert to `list` when encountering an attribute whose type59 is `tuple` or `set`. Only meaningful if *recurse* is `True`.60 61 value_serializer (typing.Callable | None):62 A hook that is called for every attribute or dict key/value. It63 receives the current instance, field and value and must return the64 (updated) value. The hook is run *after* the optional *filter* has65 been applied.66 67 Returns:68 Return type of *dict_factory*.69 70 Raises:71 attrs.exceptions.NotAnAttrsClassError:72 If *cls* is not an *attrs* class.73 74 .. versionadded:: 16.0.0 *dict_factory*75 .. versionadded:: 16.1.0 *retain_collection_types*76 .. versionadded:: 20.3.0 *value_serializer*77 .. versionadded:: 21.3.078 If a dict has a collection for a key, it is serialized as a tuple.79 """80 attrs = fields(inst.__class__)81 rv = dict_factory()82 for a in attrs:83 v = getattr(inst, a.name)84 if filter is not None and not filter(a, v):85 continue86 87 if value_serializer is not None:88 v = value_serializer(inst, a, v)89 90 if recurse is True:91 value_type = type(v)92 if value_type in _ATOMIC_TYPES:93 rv[a.name] = v94 elif has(value_type):95 rv[a.name] = asdict(96 v,97 recurse=True,98 filter=filter,99 dict_factory=dict_factory,100 retain_collection_types=retain_collection_types,101 value_serializer=value_serializer,102 )103 elif issubclass(value_type, (tuple, list, set, frozenset)):104 cf = value_type if retain_collection_types is True else list105 items = [106 _asdict_anything(107 i,108 is_key=False,109 filter=filter,110 dict_factory=dict_factory,111 retain_collection_types=retain_collection_types,112 value_serializer=value_serializer,113 )114 for i in v115 ]116 try:117 rv[a.name] = cf(items)118 except TypeError:119 if not issubclass(cf, tuple):120 raise121 # Workaround for TypeError: cf.__new__() missing 1 required122 # positional argument (which appears, for a namedturle)123 rv[a.name] = cf(*items)124 elif issubclass(value_type, dict):125 df = dict_factory126 rv[a.name] = df(127 (128 _asdict_anything(129 kk,130 is_key=True,131 filter=filter,132 dict_factory=df,133 retain_collection_types=retain_collection_types,134 value_serializer=value_serializer,135 ),136 _asdict_anything(137 vv,138 is_key=False,139 filter=filter,140 dict_factory=df,141 retain_collection_types=retain_collection_types,142 value_serializer=value_serializer,143 ),144 )145 for kk, vv in v.items()146 )147 else:148 rv[a.name] = v149 else:150 rv[a.name] = v151 return rv152 153 154def _asdict_anything(155 val,156 is_key,157 filter,158 dict_factory,159 retain_collection_types,160 value_serializer,161):162 """163 ``asdict`` only works on attrs instances, this works on anything.164 """165 val_type = type(val)166 if val_type in _ATOMIC_TYPES:167 rv = val168 if value_serializer is not None:169 rv = value_serializer(None, None, rv)170 elif getattr(val_type, "__attrs_attrs__", None) is not None:171 # Attrs class.172 rv = asdict(173 val,174 recurse=True,175 filter=filter,176 dict_factory=dict_factory,177 retain_collection_types=retain_collection_types,178 value_serializer=value_serializer,179 )180 elif issubclass(val_type, (tuple, list, set, frozenset)):181 if retain_collection_types is True:182 cf = val.__class__183 elif is_key:184 cf = tuple185 else:186 cf = list187 188 rv = cf(189 [190 _asdict_anything(191 i,192 is_key=False,193 filter=filter,194 dict_factory=dict_factory,195 retain_collection_types=retain_collection_types,196 value_serializer=value_serializer,197 )198 for i in val199 ]200 )201 elif issubclass(val_type, dict):202 df = dict_factory203 rv = df(204 (205 _asdict_anything(206 kk,207 is_key=True,208 filter=filter,209 dict_factory=df,210 retain_collection_types=retain_collection_types,211 value_serializer=value_serializer,212 ),213 _asdict_anything(214 vv,215 is_key=False,216 filter=filter,217 dict_factory=df,218 retain_collection_types=retain_collection_types,219 value_serializer=value_serializer,220 ),221 )222 for kk, vv in val.items()223 )224 else:225 rv = val226 if value_serializer is not None:227 rv = value_serializer(None, None, rv)228 229 return rv230 231 232def astuple(233 inst,234 recurse=True,235 filter=None,236 tuple_factory=tuple,237 retain_collection_types=False,238):239 """240 Return the *attrs* attribute values of *inst* as a tuple.241 242 Optionally recurse into other *attrs*-decorated classes.243 244 Args:245 inst: Instance of an *attrs*-decorated class.246 247 recurse (bool):248 Recurse into classes that are also *attrs*-decorated.249 250 filter (~typing.Callable):251 A callable whose return code determines whether an attribute or252 element is included (`True`) or dropped (`False`). Is called with253 the `attrs.Attribute` as the first argument and the value as the254 second argument.255 256 tuple_factory (~typing.Callable):257 A callable to produce tuples from. For example, to produce lists258 instead of tuples.259 260 retain_collection_types (bool):261 Do not convert to `list` or `dict` when encountering an attribute262 which type is `tuple`, `dict` or `set`. Only meaningful if263 *recurse* is `True`.264 265 Returns:266 Return type of *tuple_factory*267 268 Raises:269 attrs.exceptions.NotAnAttrsClassError:270 If *cls* is not an *attrs* class.271 272 .. versionadded:: 16.2.0273 """274 attrs = fields(inst.__class__)275 rv = []276 retain = retain_collection_types # Very long. :/277 for a in attrs:278 v = getattr(inst, a.name)279 if filter is not None and not filter(a, v):280 continue281 value_type = type(v)282 if recurse is True:283 if value_type in _ATOMIC_TYPES:284 rv.append(v)285 elif has(value_type):286 rv.append(287 astuple(288 v,289 recurse=True,290 filter=filter,291 tuple_factory=tuple_factory,292 retain_collection_types=retain,293 )294 )295 elif issubclass(value_type, (tuple, list, set, frozenset)):296 cf = v.__class__ if retain is True else list297 items = [298 (299 astuple(300 j,301 recurse=True,302 filter=filter,303 tuple_factory=tuple_factory,304 retain_collection_types=retain,305 )306 if has(j.__class__)307 else j308 )309 for j in v310 ]311 try:312 rv.append(cf(items))313 except TypeError:314 if not issubclass(cf, tuple):315 raise316 # Workaround for TypeError: cf.__new__() missing 1 required317 # positional argument (which appears, for a namedturle)318 rv.append(cf(*items))319 elif issubclass(value_type, dict):320 df = value_type if retain is True else dict321 rv.append(322 df(323 (324 (325 astuple(326 kk,327 tuple_factory=tuple_factory,328 retain_collection_types=retain,329 )330 if has(kk.__class__)331 else kk332 ),333 (334 astuple(335 vv,336 tuple_factory=tuple_factory,337 retain_collection_types=retain,338 )339 if has(vv.__class__)340 else vv341 ),342 )343 for kk, vv in v.items()344 )345 )346 else:347 rv.append(v)348 else:349 rv.append(v)350 351 return rv if tuple_factory is list else tuple_factory(rv)352 353 354def has(cls):355 """356 Check whether *cls* is a class with *attrs* attributes.357 358 Args:359 cls (type): Class to introspect.360 361 Raises:362 TypeError: If *cls* is not a class.363 364 Returns:365 bool:366 """367 attrs = getattr(cls, "__attrs_attrs__", None)368 if attrs is not None:369 return True370 371 # No attrs, maybe it's a specialized generic (A[str])?372 generic_base = get_generic_base(cls)373 if generic_base is not None:374 generic_attrs = getattr(generic_base, "__attrs_attrs__", None)375 if generic_attrs is not None:376 # Stick it on here for speed next time.377 cls.__attrs_attrs__ = generic_attrs378 return generic_attrs is not None379 return False380 381 382def assoc(inst, **changes):383 """384 Copy *inst* and apply *changes*.385 386 This is different from `evolve` that applies the changes to the arguments387 that create the new instance.388 389 `evolve`'s behavior is preferable, but there are `edge cases`_ where it390 doesn't work. Therefore `assoc` is deprecated, but will not be removed.391 392 .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251393 394 Args:395 inst: Instance of a class with *attrs* attributes.396 397 changes: Keyword changes in the new copy.398 399 Returns:400 A copy of inst with *changes* incorporated.401 402 Raises:403 attrs.exceptions.AttrsAttributeNotFoundError:404 If *attr_name* couldn't be found on *cls*.405 406 attrs.exceptions.NotAnAttrsClassError:407 If *cls* is not an *attrs* class.408 409 .. deprecated:: 17.1.0410 Use `attrs.evolve` instead if you can. This function will not be411 removed du to the slightly different approach compared to412 `attrs.evolve`, though.413 """414 new = copy.copy(inst)415 attrs = fields(inst.__class__)416 for k, v in changes.items():417 a = getattr(attrs, k, NOTHING)418 if a is NOTHING:419 msg = f"{k} is not an attrs attribute on {new.__class__}."420 raise AttrsAttributeNotFoundError(msg)421 _OBJ_SETATTR(new, k, v)422 return new423 424 425def resolve_types(426 cls, globalns=None, localns=None, attribs=None, include_extras=True427):428 """429 Resolve any strings and forward annotations in type annotations.430 431 This is only required if you need concrete types in :class:`Attribute`'s432 *type* field. In other words, you don't need to resolve your types if you433 only use them for static type checking.434 435 With no arguments, names will be looked up in the module in which the class436 was created. If this is not what you want, for example, if the name only437 exists inside a method, you may pass *globalns* or *localns* to specify438 other dictionaries in which to look up these names. See the docs of439 `typing.get_type_hints` for more details.440 441 Args:442 cls (type): Class to resolve.443 444 globalns (dict | None): Dictionary containing global variables.445 446 localns (dict | None): Dictionary containing local variables.447 448 attribs (list | None):449 List of attribs for the given class. This is necessary when calling450 from inside a ``field_transformer`` since *cls* is not an *attrs*451 class yet.452 453 include_extras (bool):454 Resolve more accurately, if possible. Pass ``include_extras`` to455 ``typing.get_hints``, if supported by the typing module. On456 supported Python versions (3.9+), this resolves the types more457 accurately.458 459 Raises:460 TypeError: If *cls* is not a class.461 462 attrs.exceptions.NotAnAttrsClassError:463 If *cls* is not an *attrs* class and you didn't pass any attribs.464 465 NameError: If types cannot be resolved because of missing variables.466 467 Returns:468 *cls* so you can use this function also as a class decorator. Please469 note that you have to apply it **after** `attrs.define`. That means the470 decorator has to come in the line **before** `attrs.define`.471 472 .. versionadded:: 20.1.0473 .. versionadded:: 21.1.0 *attribs*474 .. versionadded:: 23.1.0 *include_extras*475 """476 # Since calling get_type_hints is expensive we cache whether we've477 # done it already.478 if getattr(cls, "__attrs_types_resolved__", None) != cls:479 import typing480 481 kwargs = {482 "globalns": globalns,483 "localns": localns,484 "include_extras": include_extras,485 }486 487 hints = typing.get_type_hints(cls, **kwargs)488 for field in fields(cls) if attribs is None else attribs:489 if field.name in hints:490 # Since fields have been frozen we must work around it.491 _OBJ_SETATTR(field, "type", hints[field.name])492 # We store the class we resolved so that subclasses know they haven't493 # been resolved.494 cls.__attrs_types_resolved__ = cls495 496 # Return the class so you can use it as a decorator too.497 return cls498 