codekingpro/portable-devtools
114k
1# SPDX-License-Identifier: MIT2 3from __future__ import annotations4 5import abc6import contextlib7import copy8import enum9import inspect10import itertools11import linecache12import sys13import types14import unicodedata15import weakref16 17from collections.abc import Callable, Mapping18from functools import cached_property19from typing import Any, NamedTuple, TypeVar20 21# We need to import _compat itself in addition to the _compat members to avoid22# having the thread-local in the globals here.23from . import _compat, _config, setters24from ._compat import (25 PY_3_10_PLUS,26 PY_3_11_PLUS,27 PY_3_13_PLUS,28 _AnnotationExtractor,29 _get_annotations,30 get_generic_base,31)32from .exceptions import (33 DefaultAlreadySetError,34 FrozenInstanceError,35 NotAnAttrsClassError,36 UnannotatedAttributeError,37)38 39 40# This is used at least twice, so cache it here.41_OBJ_SETATTR = object.__setattr__42_INIT_FACTORY_PAT = "__attr_factory_%s"43_CLASSVAR_PREFIXES = (44 "typing.ClassVar",45 "t.ClassVar",46 "ClassVar",47 "typing_extensions.ClassVar",48)49# we don't use a double-underscore prefix because that triggers50# name mangling when trying to create a slot for the field51# (when slots=True)52_HASH_CACHE_FIELD = "_attrs_cached_hash"53 54_EMPTY_METADATA_SINGLETON = types.MappingProxyType({})55 56# Unique object for unequivocal getattr() defaults.57_SENTINEL = object()58 59_DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate)60 61 62class _Nothing(enum.Enum):63 """64 Sentinel to indicate the lack of a value when `None` is ambiguous.65 66 If extending attrs, you can use ``typing.Literal[NOTHING]`` to show67 that a value may be ``NOTHING``.68 69 .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False.70 .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant.71 """72 73 NOTHING = enum.auto()74 75 def __repr__(self):76 return "NOTHING"77 78 def __bool__(self):79 return False80 81 82NOTHING = _Nothing.NOTHING83"""84Sentinel to indicate the lack of a value when `None` is ambiguous.85 86When using in 3rd party code, use `attrs.NothingType` for type annotations.87"""88 89 90class _CacheHashWrapper(int):91 """92 An integer subclass that pickles / copies as None93 94 This is used for non-slots classes with ``cache_hash=True``, to avoid95 serializing a potentially (even likely) invalid hash value. Since `None`96 is the default value for uncalculated hashes, whenever this is copied,97 the copy's value for the hash should automatically reset.98 99 See GH #613 for more details.100 """101 102 def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008103 return _none_constructor, _args104 105 106def attrib(107 default=NOTHING,108 validator=None,109 repr=True,110 cmp=None,111 hash=None,112 init=True,113 metadata=None,114 type=None,115 converter=None,116 factory=None,117 kw_only=None,118 eq=None,119 order=None,120 on_setattr=None,121 alias=None,122):123 """124 Create a new field / attribute on a class.125 126 Identical to `attrs.field`, except it's not keyword-only.127 128 Consider using `attrs.field` in new code (``attr.ib`` will *never* go away,129 though).130 131 .. warning::132 133 Does **nothing** unless the class is also decorated with134 `attr.s` (or similar)!135 136 137 .. versionadded:: 15.2.0 *convert*138 .. versionadded:: 16.3.0 *metadata*139 .. versionchanged:: 17.1.0 *validator* can be a ``list`` now.140 .. versionchanged:: 17.1.0141 *hash* is `None` and therefore mirrors *eq* by default.142 .. versionadded:: 17.3.0 *type*143 .. deprecated:: 17.4.0 *convert*144 .. versionadded:: 17.4.0145 *converter* as a replacement for the deprecated *convert* to achieve146 consistency with other noun-based arguments.147 .. versionadded:: 18.1.0148 ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``.149 .. versionadded:: 18.2.0 *kw_only*150 .. versionchanged:: 19.2.0 *convert* keyword argument removed.151 .. versionchanged:: 19.2.0 *repr* also accepts a custom callable.152 .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.153 .. versionadded:: 19.2.0 *eq* and *order*154 .. versionadded:: 20.1.0 *on_setattr*155 .. versionchanged:: 20.3.0 *kw_only* backported to Python 2156 .. versionchanged:: 21.1.0157 *eq*, *order*, and *cmp* also accept a custom callable158 .. versionchanged:: 21.1.0 *cmp* undeprecated159 .. versionadded:: 22.2.0 *alias*160 .. versionchanged:: 25.4.0161 *kw_only* can now be None, and its default is also changed from False to162 None.163 """164 eq, eq_key, order, order_key = _determine_attrib_eq_order(165 cmp, eq, order, True166 )167 168 if hash is not None and hash is not True and hash is not False:169 msg = "Invalid value for hash. Must be True, False, or None."170 raise TypeError(msg)171 172 if factory is not None:173 if default is not NOTHING:174 msg = (175 "The `default` and `factory` arguments are mutually exclusive."176 )177 raise ValueError(msg)178 if not callable(factory):179 msg = "The `factory` argument must be a callable."180 raise ValueError(msg)181 default = Factory(factory)182 183 if metadata is None:184 metadata = {}185 186 # Apply syntactic sugar by auto-wrapping.187 if isinstance(on_setattr, (list, tuple)):188 on_setattr = setters.pipe(*on_setattr)189 190 if validator and isinstance(validator, (list, tuple)):191 validator = and_(*validator)192 193 if converter and isinstance(converter, (list, tuple)):194 converter = pipe(*converter)195 196 return _CountingAttr(197 default=default,198 validator=validator,199 repr=repr,200 cmp=None,201 hash=hash,202 init=init,203 converter=converter,204 metadata=metadata,205 type=type,206 kw_only=kw_only,207 eq=eq,208 eq_key=eq_key,209 order=order,210 order_key=order_key,211 on_setattr=on_setattr,212 alias=alias,213 )214 215 216def _compile_and_eval(217 script: str,218 globs: dict[str, Any] | None,219 locs: Mapping[str, object] | None = None,220 filename: str = "",221) -> None:222 """223 Evaluate the script with the given global (globs) and local (locs)224 variables.225 """226 bytecode = compile(script, filename, "exec")227 eval(bytecode, globs, locs)228 229 230def _linecache_and_compile(231 script: str,232 filename: str,233 globs: dict[str, Any] | None,234 locals: Mapping[str, object] | None = None,235) -> dict[str, Any]:236 """237 Cache the script with _linecache_, compile it and return the _locals_.238 """239 240 locs = {} if locals is None else locals241 242 # In order of debuggers like PDB being able to step through the code,243 # we add a fake linecache entry.244 count = 1245 base_filename = filename246 while True:247 linecache_tuple = (248 len(script),249 None,250 script.splitlines(True),251 filename,252 )253 old_val = linecache.cache.setdefault(filename, linecache_tuple)254 if old_val == linecache_tuple:255 break256 257 filename = f"{base_filename[:-1]}-{count}>"258 count += 1259 260 _compile_and_eval(script, globs, locs, filename)261 262 return locs263 264 265def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type:266 """267 Create a tuple subclass to hold `Attribute`s for an `attrs` class.268 269 The subclass is a bare tuple with properties for names.270 271 class MyClassAttributes(tuple):272 __slots__ = ()273 x = property(itemgetter(0))274 """275 attr_class_name = f"{cls_name}Attributes"276 body = {}277 for i, attr_name in enumerate(attr_names):278 279 def getter(self, i=i):280 return self[i]281 282 body[attr_name] = property(getter)283 return type(attr_class_name, (tuple,), body)284 285 286# Tuple class for extracted attributes from a class definition.287# `base_attrs` is a subset of `attrs`.288class _Attributes(NamedTuple):289 attrs: type290 base_attrs: list[Attribute]291 base_attrs_map: dict[str, type]292 293 294def _is_class_var(annot):295 """296 Check whether *annot* is a typing.ClassVar.297 298 The string comparison hack is used to avoid evaluating all string299 annotations which would put attrs-based classes at a performance300 disadvantage compared to plain old classes.301 """302 annot = str(annot)303 304 # Annotation can be quoted.305 if annot.startswith(("'", '"')) and annot.endswith(("'", '"')):306 annot = annot[1:-1]307 308 return annot.startswith(_CLASSVAR_PREFIXES)309 310 311def _has_own_attribute(cls, attrib_name):312 """313 Check whether *cls* defines *attrib_name* (and doesn't just inherit it).314 """315 return attrib_name in cls.__dict__316 317 318def _collect_base_attrs(319 cls, taken_attr_names320) -> tuple[list[Attribute], dict[str, type]]:321 """322 Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.323 """324 base_attrs = []325 base_attr_map = {} # A dictionary of base attrs to their classes.326 327 # Traverse the MRO and collect attributes.328 for base_cls in reversed(cls.__mro__[1:-1]):329 for a in getattr(base_cls, "__attrs_attrs__", []):330 if a.inherited or a.name in taken_attr_names:331 continue332 333 a = a.evolve(inherited=True) # noqa: PLW2901334 base_attrs.append(a)335 base_attr_map[a.name] = base_cls336 337 # For each name, only keep the freshest definition i.e. the furthest at the338 # back. base_attr_map is fine because it gets overwritten with every new339 # instance.340 filtered = []341 seen = set()342 for a in reversed(base_attrs):343 if a.name in seen:344 continue345 filtered.insert(0, a)346 seen.add(a.name)347 348 return filtered, base_attr_map349 350 351def _collect_base_attrs_broken(cls, taken_attr_names):352 """353 Collect attr.ibs from base classes of *cls*, except *taken_attr_names*.354 355 N.B. *taken_attr_names* will be mutated.356 357 Adhere to the old incorrect behavior.358 359 Notably it collects from the front and considers inherited attributes which360 leads to the buggy behavior reported in #428.361 """362 base_attrs = []363 base_attr_map = {} # A dictionary of base attrs to their classes.364 365 # Traverse the MRO and collect attributes.366 for base_cls in cls.__mro__[1:-1]:367 for a in getattr(base_cls, "__attrs_attrs__", []):368 if a.name in taken_attr_names:369 continue370 371 a = a.evolve(inherited=True) # noqa: PLW2901372 taken_attr_names.add(a.name)373 base_attrs.append(a)374 base_attr_map[a.name] = base_cls375 376 return base_attrs, base_attr_map377 378 379def _transform_attrs(380 cls,381 these,382 auto_attribs,383 kw_only,384 collect_by_mro,385 field_transformer,386) -> _Attributes:387 """388 Transform all `_CountingAttr`s on a class into `Attribute`s.389 390 If *these* is passed, use that and don't look for them on the class.391 392 If *collect_by_mro* is True, collect them in the correct MRO order,393 otherwise use the old -- incorrect -- order. See #428.394 395 Return an `_Attributes`.396 """397 cd = cls.__dict__398 anns = _get_annotations(cls)399 400 if these is not None:401 ca_list = list(these.items())402 elif auto_attribs is True:403 ca_names = {404 name405 for name, attr in cd.items()406 if attr.__class__ is _CountingAttr407 }408 ca_list = []409 annot_names = set()410 for attr_name, type in anns.items():411 if _is_class_var(type):412 continue413 annot_names.add(attr_name)414 a = cd.get(attr_name, NOTHING)415 416 if a.__class__ is not _CountingAttr:417 a = attrib(a)418 ca_list.append((attr_name, a))419 420 unannotated = ca_names - annot_names421 if unannotated:422 raise UnannotatedAttributeError(423 "The following `attr.ib`s lack a type annotation: "424 + ", ".join(425 sorted(unannotated, key=lambda n: cd.get(n).counter)426 )427 + "."428 )429 else:430 ca_list = sorted(431 (432 (name, attr)433 for name, attr in cd.items()434 if attr.__class__ is _CountingAttr435 ),436 key=lambda e: e[1].counter,437 )438 439 fca = Attribute.from_counting_attr440 no = ClassProps.KeywordOnly.NO441 own_attrs = [442 fca(443 attr_name,444 ca,445 kw_only is not no,446 anns.get(attr_name),447 )448 for attr_name, ca in ca_list449 ]450 451 if collect_by_mro:452 base_attrs, base_attr_map = _collect_base_attrs(453 cls, {a.name for a in own_attrs}454 )455 else:456 base_attrs, base_attr_map = _collect_base_attrs_broken(457 cls, {a.name for a in own_attrs}458 )459 460 if kw_only is ClassProps.KeywordOnly.FORCE:461 own_attrs = [a.evolve(kw_only=True) for a in own_attrs]462 base_attrs = [a.evolve(kw_only=True) for a in base_attrs]463 464 attrs = base_attrs + own_attrs465 466 # Resolve default field alias before executing field_transformer, so that467 # the transformer receives fully populated Attribute objects with usable468 # alias values.469 for a in attrs:470 if not a.alias:471 # Evolve is very slow, so we hold our nose and do it dirty.472 _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))473 _OBJ_SETATTR.__get__(a)("alias_is_default", True)474 475 if field_transformer is not None:476 attrs = tuple(field_transformer(cls, attrs))477 478 # Check attr order after executing the field_transformer.479 # Mandatory vs non-mandatory attr order only matters when they are part of480 # the __init__ signature and when they aren't kw_only (which are moved to481 # the end and can be mandatory or non-mandatory in any order, as they will482 # be specified as keyword args anyway). Check the order of those attrs:483 had_default = False484 for a in (a for a in attrs if a.init is not False and a.kw_only is False):485 if had_default is True and a.default is NOTHING:486 msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}"487 raise ValueError(msg)488 489 if had_default is False and a.default is not NOTHING:490 had_default = True491 492 # Resolve default field alias for any new attributes that the493 # field_transformer may have added without setting an alias.494 for a in attrs:495 if not a.alias:496 _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name))497 _OBJ_SETATTR.__get__(a)("alias_is_default", True)498 499 # Create AttrsClass *after* applying the field_transformer since it may500 # add or remove attributes!501 attr_names = [a.name for a in attrs]502 AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names)503 504 return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map)505 506 507def _make_cached_property_getattr(cached_properties, original_getattr, cls):508 lines = [509 # Wrapped to get `__class__` into closure cell for super()510 # (It will be replaced with the newly constructed class after construction).511 "def wrapper(_cls):",512 " __class__ = _cls",513 " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):",514 " func = cached_properties.get(item)",515 " if func is not None:",516 " result = func(self)",517 " _setter = _cached_setattr_get(self)",518 " _setter(item, result)",519 " return result",520 ]521 if original_getattr is not None:522 lines.append(523 " return original_getattr(self, item)",524 )525 else:526 lines.extend(527 [528 " try:",529 " return super().__getattribute__(item)",530 " except AttributeError:",531 " if not hasattr(super(), '__getattr__'):",532 " raise",533 " return super().__getattr__(item)",534 " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"",535 " raise AttributeError(original_error)",536 ]537 )538 539 lines.extend(540 [541 " return __getattr__",542 "__getattr__ = wrapper(_cls)",543 ]544 )545 546 unique_filename = _generate_unique_filename(cls, "getattr")547 548 glob = {549 "cached_properties": cached_properties,550 "_cached_setattr_get": _OBJ_SETATTR.__get__,551 "original_getattr": original_getattr,552 }553 554 return _linecache_and_compile(555 "\n".join(lines), unique_filename, glob, locals={"_cls": cls}556 )["__getattr__"]557 558 559def _frozen_setattrs(self, name, value):560 """561 Attached to frozen classes as __setattr__.562 """563 if isinstance(self, BaseException) and name in (564 "__cause__",565 "__context__",566 "__traceback__",567 "__suppress_context__",568 "__notes__",569 ):570 BaseException.__setattr__(self, name, value)571 return572 573 raise FrozenInstanceError574 575 576def _frozen_delattrs(self, name):577 """578 Attached to frozen classes as __delattr__.579 """580 if isinstance(self, BaseException) and name == "__notes__":581 BaseException.__delattr__(self, name)582 return583 584 raise FrozenInstanceError585 586 587def evolve(*args, **changes):588 """589 Create a new instance, based on the first positional argument with590 *changes* applied.591 592 .. tip::593 594 On Python 3.13 and later, you can also use `copy.replace` instead.595 596 Args:597 598 inst:599 Instance of a class with *attrs* attributes. *inst* must be passed600 as a positional argument.601 602 changes:603 Keyword changes in the new copy.604 605 Returns:606 A copy of inst with *changes* incorporated.607 608 Raises:609 TypeError:610 If *attr_name* couldn't be found in the class ``__init__``.611 612 attrs.exceptions.NotAnAttrsClassError:613 If *cls* is not an *attrs* class.614 615 .. versionadded:: 17.1.0616 .. deprecated:: 23.1.0617 It is now deprecated to pass the instance using the keyword argument618 *inst*. It will raise a warning until at least April 2024, after which619 it will become an error. Always pass the instance as a positional620 argument.621 .. versionchanged:: 24.1.0622 *inst* can't be passed as a keyword argument anymore.623 """624 try:625 (inst,) = args626 except ValueError:627 msg = (628 f"evolve() takes 1 positional argument, but {len(args)} were given"629 )630 raise TypeError(msg) from None631 632 cls = inst.__class__633 attrs = fields(cls)634 for a in attrs:635 if not a.init:636 continue637 attr_name = a.name # To deal with private attributes.638 init_name = a.alias639 if init_name not in changes:640 changes[init_name] = getattr(inst, attr_name)641 642 return cls(**changes)643 644 645class _ClassBuilder:646 """647 Iteratively build *one* class.648 """649 650 __slots__ = (651 "_add_method_dunders",652 "_attr_names",653 "_attrs",654 "_base_attr_map",655 "_base_names",656 "_cache_hash",657 "_cls",658 "_cls_dict",659 "_delete_attribs",660 "_frozen",661 "_has_custom_setattr",662 "_has_post_init",663 "_has_pre_init",664 "_is_exc",665 "_on_setattr",666 "_pre_init_has_args",667 "_repr_added",668 "_script_snippets",669 "_slots",670 "_weakref_slot",671 "_wrote_own_setattr",672 )673 674 def __init__(675 self,676 cls: type,677 these,678 auto_attribs: bool,679 props: ClassProps,680 has_custom_setattr: bool,681 ):682 attrs, base_attrs, base_map = _transform_attrs(683 cls,684 these,685 auto_attribs,686 props.kw_only,687 props.collected_fields_by_mro,688 props.field_transformer,689 )690 691 self._cls = cls692 self._cls_dict = dict(cls.__dict__) if props.is_slotted else {}693 self._attrs = attrs694 self._base_names = {a.name for a in base_attrs}695 self._base_attr_map = base_map696 self._attr_names = tuple(a.name for a in attrs)697 self._slots = props.is_slotted698 self._frozen = props.is_frozen699 self._weakref_slot = props.has_weakref_slot700 self._cache_hash = (701 props.hashability is ClassProps.Hashability.HASHABLE_CACHED702 )703 self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))704 self._pre_init_has_args = False705 if self._has_pre_init:706 # Check if the pre init method has more arguments than just `self`707 # We want to pass arguments if pre init expects arguments708 pre_init_func = cls.__attrs_pre_init__709 pre_init_signature = inspect.signature(pre_init_func)710 self._pre_init_has_args = len(pre_init_signature.parameters) > 1711 self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))712 self._delete_attribs = not bool(these)713 self._is_exc = props.is_exception714 self._on_setattr = props.on_setattr_hook715 716 self._has_custom_setattr = has_custom_setattr717 self._wrote_own_setattr = False718 719 self._cls_dict["__attrs_attrs__"] = self._attrs720 self._cls_dict["__attrs_props__"] = props721 722 if props.is_frozen:723 self._cls_dict["__setattr__"] = _frozen_setattrs724 self._cls_dict["__delattr__"] = _frozen_delattrs725 726 self._wrote_own_setattr = True727 elif self._on_setattr in (728 _DEFAULT_ON_SETATTR,729 setters.validate,730 setters.convert,731 ):732 has_validator = has_converter = False733 for a in attrs:734 if a.validator is not None:735 has_validator = True736 if a.converter is not None:737 has_converter = True738 739 if has_validator and has_converter:740 break741 if (742 (743 self._on_setattr == _DEFAULT_ON_SETATTR744 and not (has_validator or has_converter)745 )746 or (self._on_setattr == setters.validate and not has_validator)747 or (self._on_setattr == setters.convert and not has_converter)748 ):749 # If class-level on_setattr is set to convert + validate, but750 # there's no field to convert or validate, pretend like there's751 # no on_setattr.752 self._on_setattr = None753 754 if props.added_pickling:755 (756 self._cls_dict["__getstate__"],757 self._cls_dict["__setstate__"],758 ) = self._make_getstate_setstate()759 760 # tuples of script, globs, hook761 self._script_snippets: list[762 tuple[str, dict, Callable[[dict, dict], Any]]763 ] = []764 self._repr_added = False765 766 # We want to only do this check once; in 99.9% of cases these767 # exist.768 if not hasattr(self._cls, "__module__") or not hasattr(769 self._cls, "__qualname__"770 ):771 self._add_method_dunders = self._add_method_dunders_safe772 else:773 self._add_method_dunders = self._add_method_dunders_unsafe774 775 def __repr__(self):776 return f"<_ClassBuilder(cls={self._cls.__name__})>"777 778 def _eval_snippets(self) -> None:779 """780 Evaluate any registered snippets in one go.781 """782 script = "\n".join([snippet[0] for snippet in self._script_snippets])783 globs = {}784 for _, snippet_globs, _ in self._script_snippets:785 globs.update(snippet_globs)786 787 locs = _linecache_and_compile(788 script,789 _generate_unique_filename(self._cls, "methods"),790 globs,791 )792 793 for _, _, hook in self._script_snippets:794 hook(self._cls_dict, locs)795 796 def build_class(self):797 """798 Finalize class based on the accumulated configuration.799 800 Builder cannot be used after calling this method.801 """802 self._eval_snippets()803 if self._slots is True:804 cls = self._create_slots_class()805 self._cls.__attrs_base_of_slotted__ = weakref.ref(cls)806 else:807 cls = self._patch_original_class()808 if PY_3_10_PLUS:809 cls = abc.update_abstractmethods(cls)810 811 # The method gets only called if it's not inherited from a base class.812 # _has_own_attribute does NOT work properly for classmethods.813 if (814 getattr(cls, "__attrs_init_subclass__", None)815 and "__attrs_init_subclass__" not in cls.__dict__816 ):817 cls.__attrs_init_subclass__()818 819 return cls820 821 def _patch_original_class(self):822 """823 Apply accumulated methods and return the class.824 """825 cls = self._cls826 base_names = self._base_names827 828 # Clean class of attribute definitions (`attr.ib()`s).829 if self._delete_attribs:830 for name in self._attr_names:831 if (832 name not in base_names833 and getattr(cls, name, _SENTINEL) is not _SENTINEL834 ):835 # An AttributeError can happen if a base class defines a836 # class variable and we want to set an attribute with the837 # same name by using only a type annotation.838 with contextlib.suppress(AttributeError):839 delattr(cls, name)840 841 # Attach our dunder methods.842 for name, value in self._cls_dict.items():843 setattr(cls, name, value)844 845 # If we've inherited an attrs __setattr__ and don't write our own,846 # reset it to object's.847 if not self._wrote_own_setattr and getattr(848 cls, "__attrs_own_setattr__", False849 ):850 cls.__attrs_own_setattr__ = False851 852 if not self._has_custom_setattr:853 cls.__setattr__ = _OBJ_SETATTR854 855 return cls856 857 def _create_slots_class(self):858 """859 Build and return a new class with a `__slots__` attribute.860 """861 cd = {862 k: v863 for k, v in self._cls_dict.items()864 if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")865 }866 867 # 3.14.0rc2+868 if hasattr(sys, "_clear_type_descriptors"):869 sys._clear_type_descriptors(self._cls)870 871 # If our class doesn't have its own implementation of __setattr__872 # (either from the user or by us), check the bases, if one of them has873 # an attrs-made __setattr__, that needs to be reset. We don't walk the874 # MRO because we only care about our immediate base classes.875 # XXX: This can be confused by subclassing a slotted attrs class with876 # XXX: a non-attrs class and subclass the resulting class with an attrs877 # XXX: class. See `test_slotted_confused` for details. For now that's878 # XXX: OK with us.879 if not self._wrote_own_setattr:880 cd["__attrs_own_setattr__"] = False881 882 if not self._has_custom_setattr:883 for base_cls in self._cls.__bases__:884 if base_cls.__dict__.get("__attrs_own_setattr__", False):885 cd["__setattr__"] = _OBJ_SETATTR886 break887 888 # Traverse the MRO to collect existing slots889 # and check for an existing __weakref__.890 existing_slots = {}891 weakref_inherited = False892 for base_cls in self._cls.__mro__[1:-1]:893 if base_cls.__dict__.get("__weakref__", None) is not None:894 weakref_inherited = True895 existing_slots.update(896 {897 name: getattr(base_cls, name)898 for name in getattr(base_cls, "__slots__", [])899 }900 )901 902 base_names = set(self._base_names)903 904 names = self._attr_names905 if (906 self._weakref_slot907 and "__weakref__" not in getattr(self._cls, "__slots__", ())908 and "__weakref__" not in names909 and not weakref_inherited910 ):911 names += ("__weakref__",)912 913 cached_properties = {914 name: cached_prop.func915 for name, cached_prop in cd.items()916 if isinstance(cached_prop, cached_property)917 }918 919 # Collect methods with a `__class__` reference that are shadowed in the new class.920 # To know to update them.921 additional_closure_functions_to_update = []922 if cached_properties:923 class_annotations = _get_annotations(self._cls)924 for name, func in cached_properties.items():925 # Add cached properties to names for slotting.926 names += (name,)927 # Clear out function from class to avoid clashing.928 del cd[name]929 additional_closure_functions_to_update.append(func)930 annotation = inspect.signature(func).return_annotation931 if annotation is not inspect.Parameter.empty:932 class_annotations[name] = annotation933 934 original_getattr = cd.get("__getattr__")935 if original_getattr is not None:936 additional_closure_functions_to_update.append(original_getattr)937 938 cd["__getattr__"] = _make_cached_property_getattr(939 cached_properties, original_getattr, self._cls940 )941 942 # We only add the names of attributes that aren't inherited.943 # Setting __slots__ to inherited attributes wastes memory.944 slot_names = [name for name in names if name not in base_names]945 946 # There are slots for attributes from current class947 # that are defined in parent classes.948 # As their descriptors may be overridden by a child class,949 # we collect them here and update the class dict950 reused_slots = {951 slot: slot_descriptor952 for slot, slot_descriptor in existing_slots.items()953 if slot in slot_names954 }955 slot_names = [name for name in slot_names if name not in reused_slots]956 cd.update(reused_slots)957 if self._cache_hash:958 slot_names.append(_HASH_CACHE_FIELD)959 960 cd["__slots__"] = tuple(slot_names)961 962 cd["__qualname__"] = self._cls.__qualname__963 964 # Create new class based on old class and our methods.965 cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd)966 967 # The following is a fix for968 # <https://github.com/python-attrs/attrs/issues/102>.969 # If a method mentions `__class__` or uses the no-arg super(), the970 # compiler will bake a reference to the class in the method itself971 # as `method.__closure__`. Since we replace the class with a972 # clone, we rewrite these references so it keeps working.973 for item in itertools.chain(974 cls.__dict__.values(), additional_closure_functions_to_update975 ):976 if isinstance(item, (classmethod, staticmethod)):977 # Class- and staticmethods hide their functions inside.978 # These might need to be rewritten as well.979 closure_cells = getattr(item.__func__, "__closure__", None)980 elif isinstance(item, property):981 # Workaround for property `super()` shortcut (PY3-only).982 # There is no universal way for other descriptors.983 closure_cells = getattr(item.fget, "__closure__", None)984 else:985 closure_cells = getattr(item, "__closure__", None)986 987 if not closure_cells: # Catch None or the empty list.988 continue989 for cell in closure_cells:990 try:991 match = cell.cell_contents is self._cls992 except ValueError: # noqa: PERF203993 # ValueError: Cell is empty994 pass995 else:996 if match:997 cell.cell_contents = cls998 return cls999 1000 def add_repr(self, ns):1001 script, globs = _make_repr_script(self._attrs, ns)1002 1003 def _attach_repr(cls_dict, globs):1004 cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"])1005 1006 self._script_snippets.append((script, globs, _attach_repr))1007 self._repr_added = True1008 return self1009 1010 def add_str(self):1011 if not self._repr_added:1012 msg = "__str__ can only be generated if a __repr__ exists."1013 raise ValueError(msg)1014 1015 def __str__(self):1016 return self.__repr__()1017 1018 self._cls_dict["__str__"] = self._add_method_dunders(__str__)1019 return self1020 1021 def _make_getstate_setstate(self):1022 """1023 Create custom __setstate__ and __getstate__ methods.1024 """1025 # __weakref__ is not writable.1026 state_attr_names = tuple(1027 an for an in self._attr_names if an != "__weakref__"1028 )1029 1030 def slots_getstate(self):1031 """1032 Automatically created by attrs.1033 """1034 return {name: getattr(self, name) for name in state_attr_names}1035 1036 hash_caching_enabled = self._cache_hash1037 1038 def slots_setstate(self, state):1039 """1040 Automatically created by attrs.1041 """1042 __bound_setattr = _OBJ_SETATTR.__get__(self)1043 if isinstance(state, tuple):1044 # Backward compatibility with attrs instances pickled with1045 # attrs versions before v22.2.0 which stored tuples.1046 for name, value in zip(state_attr_names, state):1047 __bound_setattr(name, value)1048 else:1049 for name in state_attr_names:1050 if name in state:1051 __bound_setattr(name, state[name])1052 1053 # The hash code cache is not included when the object is1054 # serialized, but it still needs to be initialized to None to1055 # indicate that the first call to __hash__ should be a cache1056 # miss.1057 if hash_caching_enabled:1058 __bound_setattr(_HASH_CACHE_FIELD, None)1059 1060 return slots_getstate, slots_setstate1061 1062 def make_unhashable(self):1063 self._cls_dict["__hash__"] = None1064 return self1065 1066 def add_hash(self):1067 script, globs = _make_hash_script(1068 self._cls,1069 self._attrs,1070 frozen=self._frozen,1071 cache_hash=self._cache_hash,1072 )1073 1074 def attach_hash(cls_dict: dict, locs: dict) -> None:1075 cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"])1076 1077 self._script_snippets.append((script, globs, attach_hash))1078 1079 return self1080 1081 def add_init(self):1082 script, globs, annotations = _make_init_script(1083 self._cls,1084 self._attrs,1085 self._has_pre_init,1086 self._pre_init_has_args,1087 self._has_post_init,1088 self._frozen,1089 self._slots,1090 self._cache_hash,1091 self._base_attr_map,1092 self._is_exc,1093 self._on_setattr,1094 attrs_init=False,1095 )1096 1097 def _attach_init(cls_dict, globs):1098 init = globs["__init__"]1099 init.__annotations__ = annotations1100 cls_dict["__init__"] = self._add_method_dunders(init)1101 1102 self._script_snippets.append((script, globs, _attach_init))1103 1104 return self1105 1106 def add_replace(self):1107 self._cls_dict["__replace__"] = self._add_method_dunders(evolve)1108 return self1109 1110 def add_match_args(self):1111 self._cls_dict["__match_args__"] = tuple(1112 field.name1113 for field in self._attrs1114 if field.init and not field.kw_only1115 )1116 1117 def add_attrs_init(self):1118 script, globs, annotations = _make_init_script(1119 self._cls,1120 self._attrs,1121 self._has_pre_init,1122 self._pre_init_has_args,1123 self._has_post_init,1124 self._frozen,1125 self._slots,1126 self._cache_hash,1127 self._base_attr_map,1128 self._is_exc,1129 self._on_setattr,1130 attrs_init=True,1131 )1132 1133 def _attach_attrs_init(cls_dict, globs):1134 init = globs["__attrs_init__"]1135 init.__annotations__ = annotations1136 cls_dict["__attrs_init__"] = self._add_method_dunders(init)1137 1138 self._script_snippets.append((script, globs, _attach_attrs_init))1139 1140 return self1141 1142 def add_eq(self):1143 cd = self._cls_dict1144 1145 script, globs = _make_eq_script(self._attrs)1146 1147 def _attach_eq(cls_dict, globs):1148 cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"])1149 1150 self._script_snippets.append((script, globs, _attach_eq))1151 1152 cd["__ne__"] = __ne__1153 1154 return self1155 1156 def add_order(self):1157 cd = self._cls_dict1158 1159 cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = (1160 self._add_method_dunders(meth)1161 for meth in _make_order(self._cls, self._attrs)1162 )1163 1164 return self1165 1166 def add_setattr(self):1167 sa_attrs = {}1168 for a in self._attrs:1169 on_setattr = a.on_setattr or self._on_setattr1170 if on_setattr and on_setattr is not setters.NO_OP:1171 sa_attrs[a.name] = a, on_setattr1172 1173 if not sa_attrs:1174 return self1175 1176 if self._has_custom_setattr:1177 # We need to write a __setattr__ but there already is one!1178 msg = "Can't combine custom __setattr__ with on_setattr hooks."1179 raise ValueError(msg)1180 1181 # docstring comes from _add_method_dunders1182 def __setattr__(self, name, val):1183 try:1184 a, hook = sa_attrs[name]1185 except KeyError:1186 nval = val1187 else:1188 nval = hook(self, a, val)1189 1190 _OBJ_SETATTR(self, name, nval)1191 1192 self._cls_dict["__attrs_own_setattr__"] = True1193 self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__)1194 self._wrote_own_setattr = True1195 1196 return self1197 1198 def _add_method_dunders_unsafe(self, method: Callable) -> Callable:1199 """1200 Add __module__ and __qualname__ to a *method*.