/
niceSOFT
/
python3-attrs
Обзор
Документация
Войти
/
niceSOFT
/
python3-attrs
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/attr/_make.py
3 462 строки
106 KB
Hynek Schlawack
Stop evolve dunders from being modified (#1606)
02 авг 2026, 14:46
Не верифицирован
02 авг 2026, 14:46
f53fc54
Код
Авторство
О чём код?
# SPDX-License-Identifier: MIT from __future__ import annotations import abc import contextlib import enum import itertools import linecache import sys import types import unicodedata import weakref from collections.abc import Callable, Mapping from functools import cached_property from typing import Any, NamedTuple, TypeVar # We need to import _compat itself in addition to the _compat members to avoid # having the thread-local in the globals here. from . import _compat, _config, setters from ._compat import ( PY_3_11_PLUS, PY_3_13_PLUS, _AnnotationExtractor, _get_annotations, _lazy_is_generator, get_generic_base, ) from .exceptions import ( DefaultAlreadySetError, FrozenInstanceError, NotAnAttrsClassError, UnannotatedAttributeError, ) # This is used at least twice, so cache it here. _OBJ_SETATTR = object.__setattr__ _INIT_FACTORY_PAT = "__attr_factory_%s" _CLASSVAR_PREFIXES = ( "typing.ClassVar", "t.ClassVar", "ClassVar", "typing_extensions.ClassVar", ) # we don't use a double-underscore prefix because that triggers # name mangling when trying to create a slot for the field # (when slots=True) _HASH_CACHE_FIELD = "_attrs_cached_hash" _EMPTY_METADATA_SINGLETON = types.MappingProxyType({}) # Unique object for unequivocal getattr() defaults. _SENTINEL = object() _DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate) class _Nothing(enum.Enum): """ Sentinel to indicate the lack of a value when `None` is ambiguous. If extending attrs, you can use ``typing.Literal[NOTHING]`` to show that a value may be ``NOTHING``. .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. """ NOTHING = enum.auto() def __repr__(self): return "NOTHING" def __bool__(self): return False NOTHING = _Nothing.NOTHING """ Sentinel to indicate the lack of a value when `None` is ambiguous. When using in 3rd party code, use `attrs.NothingType` for type annotations. """ class _CacheHashWrapper(int): """ An integer subclass that pickles / copies as None This is used for non-slots classes with ``cache_hash=True``, to avoid serializing a potentially (even likely) invalid hash value. Since `None` is the default value for uncalculated hashes, whenever this is copied, the copy's value for the hash should automatically reset. See GH #613 for more details. """ def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 return _none_constructor, _args def attrib( default=NOTHING, validator=None, repr=True, cmp=None, hash=None, init=True, metadata=None, type=None, converter=None, factory=None, kw_only=None, eq=None, order=None, on_setattr=None, alias=None, ): """ Create a new field / attribute on a class. Identical to `attrs.field`, except it's not keyword-only. Consider using `attrs.field` in new code (``attr.ib`` will *never* go away, though). .. warning:: Does **nothing** unless the class is also decorated with `attr.s` (or similar)! .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. .. versionchanged:: 17.1.0 *hash* is `None` and therefore mirrors *eq* by default. .. versionadded:: 17.3.0 *type* .. deprecated:: 17.4.0 *convert* .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated *convert* to achieve consistency with other noun-based arguments. .. versionadded:: 18.1.0 ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. .. versionadded:: 18.2.0 *kw_only* .. versionchanged:: 19.2.0 *convert* keyword argument removed. .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. .. versionadded:: 19.2.0 *eq* and *order* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 .. versionchanged:: 21.1.0 *eq*, *order*, and *cmp* also accept a custom callable .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 22.2.0 *alias* .. versionchanged:: 25.4.0 *kw_only* can now be None, and its default is also changed from False to None. .. versionchanged:: 26.2.0 *on_setattr* hooks can now be generator functions to run code before and after an attribute is set. """ eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq, order, True ) if hash is not None and hash is not True and hash is not False: msg = "Invalid value for hash. Must be True, False, or None." raise TypeError(msg) if factory is not None: if default is not NOTHING: msg = ( "The `default` and `factory` arguments are mutually exclusive." ) raise ValueError(msg) if not callable(factory): msg = "The `factory` argument must be a callable." raise ValueError(msg) default = Factory(factory) if metadata is None: metadata = {} # Apply syntactic sugar by auto-wrapping. if isinstance(on_setattr, (list, tuple)): on_setattr = setters.pipe(*on_setattr) if validator and isinstance(validator, (list, tuple)): validator = and_(*validator) if converter and isinstance(converter, (list, tuple)): converter = pipe(*converter) return _CountingAttr( default=default, validator=validator, repr=repr, cmp=None, hash=hash, init=init, converter=converter, metadata=metadata, type=type, kw_only=kw_only, eq=eq, eq_key=eq_key, order=order, order_key=order_key, on_setattr=on_setattr, alias=alias, ) def _compile_and_eval( script: str, globs: dict[str, Any] | None, locs: Mapping[str, object] | None = None, filename: str = "", ) -> None: """ Evaluate the script with the given global (globs) and local (locs) variables. """ bytecode = compile(script, filename, "exec") eval(bytecode, globs, locs) def _linecache_and_compile( script: str, filename: str, globs: dict[str, Any] | None, locals: Mapping[str, object] | None = None, ) -> dict[str, Any]: """ Cache the script with _linecache_, compile it and return the _locals_. """ locs = {} if locals is None else locals # In order of debuggers like PDB being able to step through the code, # we add a fake linecache entry. count = 1 base_filename = filename while True: linecache_tuple = ( len(script), None, script.splitlines(True), filename, ) old_val = linecache.cache.setdefault(filename, linecache_tuple) if old_val == linecache_tuple: break filename = f"{base_filename[:-1]}-{count}>" count += 1 _compile_and_eval(script, globs, locs, filename) return locs def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type: """ Create a tuple subclass to hold `Attribute`s for an `attrs` class. The subclass is a bare tuple with properties for names. class MyClassAttributes(tuple): __slots__ = () x = property(itemgetter(0)) """ attr_class_name = f"{cls_name}Attributes" body = {} for i, attr_name in enumerate(attr_names): def getter(self, i=i): return self[i] body[attr_name] = property(getter) return type(attr_class_name, (tuple,), body) # Tuple class for extracted attributes from a class definition. # `base_attrs` is a subset of `attrs`. class _Attributes(NamedTuple): attrs: type base_attrs: list[Attribute] base_attrs_map: dict[str, type] def _is_class_var(annot): """ Check whether *annot* is a typing.ClassVar. The string comparison hack is used to avoid evaluating all string annotations which would put attrs-based classes at a performance disadvantage compared to plain old classes. """ annot = getattr(annot, "__forward_arg__", annot) annot = str(annot) # Annotation can be quoted. if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): annot = annot[1:-1] return annot.startswith(_CLASSVAR_PREFIXES) def _has_own_attribute(cls, attrib_name): """ Check whether *cls* defines *attrib_name* (and doesn't just inherit it). """ return attrib_name in cls.__dict__ def _collect_base_attrs( cls, taken_attr_names ) -> tuple[list[Attribute], dict[str, type]]: """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. """ base_attrs = [] base_attr_map = {} # A dictionary of base attrs to their classes. # Traverse the MRO and collect attributes. for base_cls in reversed(cls.__mro__[1:-1]): for a in getattr(base_cls, "__attrs_attrs__", []): if a.inherited or a.name in taken_attr_names: continue a = a.evolve(inherited=True) # noqa: PLW2901 base_attrs.append(a) base_attr_map[a.name] = base_cls # For each name, only keep the freshest definition i.e. the furthest at the # back. base_attr_map is fine because it gets overwritten with every new # instance. filtered = [] seen = set() for a in reversed(base_attrs): if a.name in seen: continue filtered.insert(0, a) seen.add(a.name) return filtered, base_attr_map def _collect_base_attrs_broken(cls, taken_attr_names): """ Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. N.B. *taken_attr_names* will be mutated. Adhere to the old incorrect behavior. Notably it collects from the front and considers inherited attributes which leads to the buggy behavior reported in #428. """ base_attrs = [] base_attr_map = {} # A dictionary of base attrs to their classes. # Traverse the MRO and collect attributes. for base_cls in cls.__mro__[1:-1]: for a in getattr(base_cls, "__attrs_attrs__", []): if a.name in taken_attr_names: continue a = a.evolve(inherited=True) # noqa: PLW2901 taken_attr_names.add(a.name) base_attrs.append(a) base_attr_map[a.name] = base_cls return base_attrs, base_attr_map def _transform_attrs( cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer, ) -> _Attributes: """ Transform all `_CountingAttr`s on a class into `Attribute`s. If *these* is passed, use that and don't look for them on the class. If *collect_by_mro* is True, collect them in the correct MRO order, otherwise use the old -- incorrect -- order. See #428. Return an `_Attributes`. """ cd = cls.__dict__ anns = _get_annotations(cls) if these is not None: ca_list = list(these.items()) elif auto_attribs is True: ca_names = { name for name, attr in cd.items() if attr.__class__ is _CountingAttr } ca_list = [] annot_names = set() for attr_name, type in anns.items(): if _is_class_var(type): continue annot_names.add(attr_name) a = cd.get(attr_name, NOTHING) if a.__class__ is not _CountingAttr: a = attrib(a) ca_list.append((attr_name, a)) unannotated = ca_names - annot_names if unannotated: raise UnannotatedAttributeError( "The following `attr.ib`s lack a type annotation: " + ", ".join( sorted(unannotated, key=lambda n: cd.get(n).counter) ) + "." ) else: ca_list = sorted( ( (name, attr) for name, attr in cd.items() if attr.__class__ is _CountingAttr ), key=lambda e: e[1].counter, ) fca = Attribute.from_counting_attr no = ClassProps.KeywordOnly.NO own_attrs = [ fca( attr_name, ca, kw_only is not no, anns.get(attr_name), ) for attr_name, ca in ca_list ] if collect_by_mro: base_attrs, base_attr_map = _collect_base_attrs( cls, {a.name for a in own_attrs} ) else: base_attrs, base_attr_map = _collect_base_attrs_broken( cls, {a.name for a in own_attrs} ) if kw_only is ClassProps.KeywordOnly.FORCE: own_attrs = [a.evolve(kw_only=True) for a in own_attrs] base_attrs = [a.evolve(kw_only=True) for a in base_attrs] attrs = base_attrs + own_attrs # Resolve default field alias before executing field_transformer, so that # the transformer receives fully populated Attribute objects with usable # alias values. for a in attrs: if not a.alias: # Evolve is very slow, so we hold our nose and do it dirty. _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) _OBJ_SETATTR.__get__(a)("alias_is_default", True) if field_transformer is not None: attrs = tuple(field_transformer(cls, attrs)) # Check attr order after executing the field_transformer. # Mandatory vs non-mandatory attr order only matters when they are part of # the __init__ signature and when they aren't kw_only (which are moved to # the end and can be mandatory or non-mandatory in any order, as they will # be specified as keyword args anyway). Check the order of those attrs: had_default = False for a in (a for a in attrs if a.init is not False and a.kw_only is False): if had_default is True and a.default is NOTHING: msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" raise ValueError(msg) if had_default is False and a.default is not NOTHING: had_default = True # Resolve default field alias for any new attributes that the # field_transformer may have added without setting an alias. for a in attrs: if not a.alias: _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) _OBJ_SETATTR.__get__(a)("alias_is_default", True) # Create AttrsClass *after* applying the field_transformer since it may # add or remove attributes! attr_names = [a.name for a in attrs] AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map) def _make_cached_property_getattr(cached_properties, original_getattr, cls): lines = [ # Wrapped to get `__class__` into closure cell for super() # (It will be replaced with the newly constructed class after construction). "def wrapper(_cls):", " __class__ = _cls", " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", " func = cached_properties.get(item)", " if func is not None:", " result = func(self)", " _setter = _cached_setattr_get(self)", " _setter(item, result)", " return result", ] if original_getattr is not None: lines.append( " return original_getattr(self, item)", ) else: lines.extend( [ " try:", " return super().__getattribute__(item)", " except AttributeError:", " if not hasattr(super(), '__getattr__'):", " raise", " return super().__getattr__(item)", " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", " raise AttributeError(original_error)", ] ) lines.extend( [ " return __getattr__", "__getattr__ = wrapper(_cls)", ] ) unique_filename = _generate_unique_filename(cls, "getattr") glob = { "cached_properties": cached_properties, "_cached_setattr_get": _OBJ_SETATTR.__get__, "original_getattr": original_getattr, } return _linecache_and_compile( "\n".join(lines), unique_filename, glob, locals={"_cls": cls} )["__getattr__"] def _frozen_setattrs(self, name, value): """ Attached to frozen classes as __setattr__. """ if isinstance(self, BaseException) and name in ( "__cause__", "__context__", "__traceback__", "__suppress_context__", "__notes__", ): BaseException.__setattr__(self, name, value) return raise FrozenInstanceError def _frozen_delattrs(self, name): """ Attached to frozen classes as __delattr__. """ if isinstance(self, BaseException) and name == "__notes__": BaseException.__delattr__(self, name) return raise FrozenInstanceError def evolve(*args, **changes): """ Create a new instance, based on the first positional argument with *changes* applied. .. tip:: On Python 3.13 and later, you can also use `copy.replace` instead. Args: inst: Instance of a class with *attrs* attributes. *inst* must be passed as a positional argument. changes: Keyword changes in the new copy. Returns: A copy of inst with *changes* incorporated. Raises: TypeError: If *attr_name* couldn't be found in the class ``__init__``. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. .. versionadded:: 17.1.0 .. deprecated:: 23.1.0 It is now deprecated to pass the instance using the keyword argument *inst*. It will raise a warning until at least April 2024, after which it will become an error. Always pass the instance as a positional argument. .. versionchanged:: 24.1.0 *inst* can't be passed as a keyword argument anymore. """ try: (inst,) = args except ValueError: msg = ( f"evolve() takes 1 positional argument, but {len(args)} were given" ) raise TypeError(msg) from None cls = inst.__class__ attrs = fields(cls) for a in attrs: if not a.init: continue attr_name = a.name # To deal with private attributes. init_name = a.alias if init_name not in changes: changes[init_name] = getattr(inst, attr_name) return cls(**changes) class _ClassBuilder: """ Iteratively build *one* class. """ __slots__ = ( "_add_method_dunders", "_attr_names", "_attrs", "_base_attr_map", "_base_names", "_cache_hash", "_cls", "_cls_dict", "_delete_attribs", "_frozen", "_has_custom_setattr", "_has_post_init", "_has_pre_init", "_is_exc", "_on_setattr", "_pre_init_has_args", "_repr_added", "_script_snippets", "_slots", "_weakref_slot", "_wrote_own_setattr", ) def __init__( self, cls: type, these, auto_attribs: bool, props: ClassProps, has_custom_setattr: bool, ): attrs, base_attrs, base_map = _transform_attrs( cls, these, auto_attribs, props.kw_only, props.collected_fields_by_mro, props.field_transformer, ) self._cls = cls self._cls_dict = dict(cls.__dict__) if props.is_slotted else {} self._attrs = attrs self._base_names = {a.name for a in base_attrs} self._base_attr_map = base_map self._attr_names = tuple(a.name for a in attrs) self._slots = props.is_slotted self._frozen = props.is_frozen self._weakref_slot = props.has_weakref_slot self._cache_hash = ( props.hashability is ClassProps.Hashability.HASHABLE_CACHED ) self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) self._pre_init_has_args = False if self._has_pre_init: # Check if the pre init method has more arguments than just `self` # We want to pass arguments if pre init expects arguments import inspect pre_init_func = cls.__attrs_pre_init__ pre_init_signature = inspect.signature(pre_init_func) self._pre_init_has_args = len(pre_init_signature.parameters) > 1 self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) self._delete_attribs = not bool(these) self._is_exc = props.is_exception self._on_setattr = props.on_setattr_hook self._has_custom_setattr = has_custom_setattr self._wrote_own_setattr = False self._cls_dict["__attrs_attrs__"] = self._attrs self._cls_dict["__attrs_props__"] = props if props.is_frozen: self._cls_dict["__setattr__"] = _frozen_setattrs self._cls_dict["__delattr__"] = _frozen_delattrs self._wrote_own_setattr = True elif self._on_setattr in ( _DEFAULT_ON_SETATTR, setters.validate, setters.convert, ): has_validator = has_converter = False for a in attrs: if a.validator is not None: has_validator = True if a.converter is not None: has_converter = True if has_validator and has_converter: break if ( ( self._on_setattr == _DEFAULT_ON_SETATTR and not (has_validator or has_converter) ) or (self._on_setattr == setters.validate and not has_validator) or (self._on_setattr == setters.convert and not has_converter) ): # If class-level on_setattr is set to convert + validate, but # there's no field to convert or validate, pretend like there's # no on_setattr. self._on_setattr = None if props.added_pickling: ( self._cls_dict["__getstate__"], self._cls_dict["__setstate__"], ) = self._make_getstate_setstate() # tuples of script, globs, hook self._script_snippets: list[ tuple[str, dict, Callable[[dict, dict], Any]] ] = [] self._repr_added = False # We want to only do this check once; in 99.9% of cases these # exist. if not hasattr(self._cls, "__module__") or not hasattr( self._cls, "__qualname__" ): self._add_method_dunders = self._add_method_dunders_safe else: self._add_method_dunders = self._add_method_dunders_unsafe def __repr__(self): return f"<_ClassBuilder(cls={self._cls.__name__})>" def _eval_snippets(self) -> None: """ Evaluate any registered snippets in one go. """ script = "\n".join([snippet[0] for snippet in self._script_snippets]) globs = {} for _, snippet_globs, _ in self._script_snippets: globs.update(snippet_globs) locs = _linecache_and_compile( script, _generate_unique_filename(self._cls, "methods"), globs, ) for _, _, hook in self._script_snippets: hook(self._cls_dict, locs) def build_class(self): """ Finalize class based on the accumulated configuration. Builder cannot be used after calling this method. """ self._eval_snippets() if self._slots is True: cls = self._create_slots_class() self._cls.__attrs_base_of_slotted__ = weakref.ref(cls) else: cls = abc.update_abstractmethods(self._patch_original_class()) # The method gets only called if it's not inherited from a base class. # _has_own_attribute does NOT work properly for classmethods. if ( getattr(cls, "__attrs_init_subclass__", None) and "__attrs_init_subclass__" not in cls.__dict__ ): cls.__attrs_init_subclass__() return cls def _patch_original_class(self): """ Apply accumulated methods and return the class. """ cls = self._cls base_names = self._base_names # Clean class of attribute definitions (`attr.ib()`s). if self._delete_attribs: for name in self._attr_names: if ( name not in base_names and getattr(cls, name, _SENTINEL) is not _SENTINEL ): # An AttributeError can happen if a base class defines a # class variable and we want to set an attribute with the # same name by using only a type annotation. with contextlib.suppress(AttributeError): delattr(cls, name) # Attach our dunder methods. for name, value in self._cls_dict.items(): setattr(cls, name, value) # If we've inherited an attrs __setattr__ and don't write our own, # reset it to object's. if not self._wrote_own_setattr and getattr( cls, "__attrs_own_setattr__", False ): cls.__attrs_own_setattr__ = False if not self._has_custom_setattr: cls.__setattr__ = _OBJ_SETATTR return cls def _create_slots_class(self): """ Build and return a new class with a `__slots__` attribute. """ cd = { k: v for k, v in self._cls_dict.items() if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") } # 3.14.0rc2+ if hasattr(sys, "_clear_type_descriptors"): sys._clear_type_descriptors(self._cls) # If our class doesn't have its own implementation of __setattr__ # (either from the user or by us), check the bases, if one of them has # an attrs-made __setattr__, that needs to be reset. We don't walk the # MRO because we only care about our immediate base classes. # XXX: This can be confused by subclassing a slotted attrs class with # XXX: a non-attrs class and subclass the resulting class with an attrs # XXX: class. See `test_slotted_confused` for details. For now that's # XXX: OK with us. if not self._wrote_own_setattr: cd["__attrs_own_setattr__"] = False if not self._has_custom_setattr: for base_cls in self._cls.__bases__: if base_cls.__dict__.get("__attrs_own_setattr__", False): cd["__setattr__"] = _OBJ_SETATTR break # Traverse the MRO to collect existing slots # and check for an existing __weakref__. existing_slots = {} weakref_inherited = False for base_cls in self._cls.__mro__[1:-1]: if base_cls.__dict__.get("__weakref__", None) is not None: weakref_inherited = True existing_slots.update( { name: getattr(base_cls, name) for name in getattr(base_cls, "__slots__", []) } ) base_names = set(self._base_names) names = self._attr_names if ( self._weakref_slot and "__weakref__" not in getattr(self._cls, "__slots__", ()) and "__weakref__" not in names and not weakref_inherited ): names += ("__weakref__",) cached_properties = { name: cached_prop.func for name, cached_prop in cd.items() if isinstance(cached_prop, cached_property) } # Collect methods with a `__class__` reference that are shadowed in the new class. # To know to update them. additional_closure_functions_to_update = [] if cached_properties: import inspect class_annotations = _get_annotations(self._cls) for name, func in cached_properties.items(): # Add cached properties to names for slotting. names += (name,) # Clear out function from class to avoid clashing. del cd[name] additional_closure_functions_to_update.append(func) annotation = inspect.signature(func).return_annotation if annotation is not inspect.Parameter.empty: class_annotations[name] = annotation original_getattr = cd.get("__getattr__") if original_getattr is not None: additional_closure_functions_to_update.append(original_getattr) cd["__getattr__"] = _make_cached_property_getattr( cached_properties, original_getattr, self._cls ) # We only add the names of attributes that aren't inherited. # Setting __slots__ to inherited attributes wastes memory. slot_names = [name for name in names if name not in base_names] # There are slots for attributes from current class # that are defined in parent classes. # As their descriptors may be overridden by a child class, # we collect them here and update the class dict reused_slots = { slot: slot_descriptor for slot, slot_descriptor in existing_slots.items() if slot in slot_names } slot_names = [name for name in slot_names if name not in reused_slots] cd.update(reused_slots) if self._cache_hash: slot_names.append(_HASH_CACHE_FIELD) cd["__slots__"] = tuple(slot_names) cd["__qualname__"] = self._cls.__qualname__ # Create new class based on old class and our methods. cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) # The following is a fix for # <https://github.com/python-attrs/attrs/issues/102>. # If a method mentions `__class__` or uses the no-arg super(), the # compiler will bake a reference to the class in the method itself # as `method.__closure__`. Since we replace the class with a # clone, we rewrite these references so it keeps working. for item in itertools.chain( cls.__dict__.values(), additional_closure_functions_to_update ): if isinstance(item, (classmethod, staticmethod)): # Class- and staticmethods hide their functions inside. # These might need to be rewritten as well. closure_cells = getattr(item.__func__, "__closure__", None) elif isinstance(item, property): # Workaround for property `super()` shortcut (PY3-only). # There is no universal way for other descriptors. closure_cells = getattr(item.fget, "__closure__", None) else: closure_cells = getattr(item, "__closure__", None) if not closure_cells: # Catch None or the empty list. continue for cell in closure_cells: try: match = cell.cell_contents is self._cls except ValueError: # noqa: PERF203 # ValueError: Cell is empty pass else: if match: cell.cell_contents = cls return cls def add_repr(self, ns): script, globs = _make_repr_script(self._attrs, ns) def _attach_repr(cls_dict, globs): cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"]) self._script_snippets.append((script, globs, _attach_repr)) self._repr_added = True return self def add_str(self): if not self._repr_added: msg = "__str__ can only be generated if a __repr__ exists." raise ValueError(msg) def __str__(self): return self.__repr__() self._cls_dict["__str__"] = self._add_method_dunders(__str__) return self def _make_getstate_setstate(self): """ Create custom __setstate__ and __getstate__ methods. """ # __weakref__ is not writable. state_attr_names = tuple( an for an in self._attr_names if an != "__weakref__" ) def slots_getstate(self): """ Automatically created by attrs. """ return {name: getattr(self, name) for name in state_attr_names} hash_caching_enabled = self._cache_hash def slots_setstate(self, state): """ Automatically created by attrs. """ __bound_setattr = _OBJ_SETATTR.__get__(self) if isinstance(state, tuple): # Backward compatibility with attrs instances pickled with # attrs versions before v22.2.0 which stored tuples. for name, value in zip(state_attr_names, state, strict=False): __bound_setattr(name, value) else: for name in state_attr_names: if name in state: __bound_setattr(name, state[name]) # The hash code cache is not included when the object is # serialized, but it still needs to be initialized to None to # indicate that the first call to __hash__ should be a cache # miss. if hash_caching_enabled: __bound_setattr(_HASH_CACHE_FIELD, None) return slots_getstate, slots_setstate def make_unhashable(self): self._cls_dict["__hash__"] = None return self def add_hash(self): script, globs = _make_hash_script( self._cls, self._attrs, frozen=self._frozen, cache_hash=self._cache_hash, ) def attach_hash(cls_dict: dict, locs: dict) -> None: cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"]) self._script_snippets.append((script, globs, attach_hash)) return self def add_init(self): script, globs, annotations = _make_init_script( self._cls, self._attrs, self._has_pre_init, self._pre_init_has_args, self._has_post_init, self._frozen, self._slots, self._cache_hash, self._base_attr_map, self._is_exc, self._on_setattr, attrs_init=False, ) def _attach_init(cls_dict, globs): init = globs["__init__"] init.__annotations__ = annotations cls_dict["__init__"] = self._add_method_dunders(init) self._script_snippets.append((script, globs, _attach_init)) return self def add_replace(self): # We create a local `evolve` proxy because it gets modified in place. def __replace__(*args, **changes): return evolve(*args, **changes) self._cls_dict["__replace__"] = self._add_method_dunders(__replace__) return self def add_match_args(self): self._cls_dict["__match_args__"] = tuple( field.name for field in self._attrs if field.init and not field.kw_only ) def add_attrs_init(self): script, globs, annotations = _make_init_script( self._cls, self._attrs, self._has_pre_init, self._pre_init_has_args, self._has_post_init, self._frozen, self._slots, self._cache_hash, self._base_attr_map, self._is_exc, self._on_setattr, attrs_init=True, ) def _attach_attrs_init(cls_dict, globs): init = globs["__attrs_init__"] init.__annotations__ = annotations cls_dict["__attrs_init__"] = self._add_method_dunders(init) self._script_snippets.append((script, globs, _attach_attrs_init)) return self def add_eq(self): cd = self._cls_dict script, globs = _make_eq_script(self._attrs) def _attach_eq(cls_dict, globs): cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"]) self._script_snippets.append((script, globs, _attach_eq)) cd["__ne__"] = __ne__ return self def add_order(self): cd = self._cls_dict cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( self._add_method_dunders(meth) for meth in _make_order(self._cls, self._attrs) ) return self def add_setattr(self): sa_attrs = {} for a in self._attrs: on_setattr = a.on_setattr or self._on_setattr if on_setattr and on_setattr is not setters.NO_OP: sa_attrs[a.name] = ( a, on_setattr, _lazy_is_generator(on_setattr), ) if not sa_attrs: return self if self._has_custom_setattr: # We need to write a __setattr__ but there already is one! msg = "Can't combine custom __setattr__ with on_setattr hooks." raise ValueError(msg) # docstring comes from _add_method_dunders def __setattr__(self, name, val): try: a, hook, is_gen = sa_attrs[name] except KeyError: _OBJ_SETATTR(self, name, val) return if is_gen(): gen = hook(self, a, val) nval = next(gen) _OBJ_SETATTR(self, name, nval) try: next(gen) except StopIteration: return gen.close() msg = "Generator on_setattr hook yielded more than once." raise RuntimeError(msg) nval = hook(self, a, val) _OBJ_SETATTR(self, name, nval) return self._cls_dict["__attrs_own_setattr__"] = True self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) self._wrote_own_setattr = True return self def _add_method_dunders_unsafe(self, method: Callable) -> Callable: """ Add __module__ and __qualname__ to a *method*. """ method.__module__ = self._cls.__module__ method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" method.__doc__ = ( f"Method generated by attrs for class {self._cls.__qualname__}." ) return method def _add_method_dunders_safe(self, method: Callable) -> Callable: """ Add __module__ and __qualname__ to a *method* if possible. """ with contextlib.suppress(AttributeError): method.__module__ = self._cls.__module__ with contextlib.suppress(AttributeError): method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" with contextlib.suppress(AttributeError): method.__doc__ = f"Method generated by attrs for class {self._cls.__qualname__}." return method def _determine_attrs_eq_order(cmp, eq, order, default_eq): """ Validate the combination of *cmp*, *eq*, and *order*. Derive the effective values of eq and order. If *eq* is None, set it to *default_eq*. """ if cmp is not None and any((eq is not None, order is not None)): msg = "Don't mix `cmp` with `eq' and `order`." raise ValueError(msg) # cmp takes precedence due to bw-compatibility. if cmp is not None: return cmp, cmp # If left None, equality is set to the specified default and ordering # mirrors equality. if eq is None: eq = default_eq if order is None: order = eq if eq is False and order is True: msg = "`order` can only be True if `eq` is True too." raise ValueError(msg) return eq, order def _determine_attrib_eq_order(cmp, eq, order, default_eq): """ Validate the combination of *cmp*, *eq*, and *order*. Derive the effective values of eq and order. If *eq* is None, set it to *default_eq*. """ if cmp is not None and any((eq is not None, order is not None)): msg = "Don't mix `cmp` with `eq' and `order`." raise ValueError(msg) def decide_callable_or_boolean(value): """ Decide whether a key function is used. """ if callable(value): value, key = True, value else: key = None return value, key # cmp takes precedence due to bw-compatibility. if cmp is not None: cmp, cmp_key = decide_callable_or_boolean(cmp) return cmp, cmp_key, cmp, cmp_key # If left None, equality is set to the specified default and ordering # mirrors equality. if eq is None: eq, eq_key = default_eq, None else: eq, eq_key = decide_callable_or_boolean(eq) if order is None: order, order_key = eq, eq_key else: order, order_key = decide_callable_or_boolean(order) if eq is False and order is True: msg = "`order` can only be True if `eq` is True too." raise ValueError(msg) return eq, eq_key, order, order_key def _determine_whether_to_implement( cls, flag, auto_detect, dunders, default=True ): """ Check whether we should implement a set of methods for *cls*. *flag* is the argument passed into @attr.s like 'init', *auto_detect* the same as passed into @attr.s and *dunders* is a tuple of attribute names whose presence signal that the user has implemented it themselves. Return *default* if no reason for either for or against is found. """ if flag is True or flag is False: return flag if flag is None and auto_detect is False: return default # Logically, flag is None and auto_detect is True here. for dunder in dunders: if _has_own_attribute(cls, dunder): return False return default def attrs( maybe_cls=None, these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True, unsafe_hash=None, force_kw_only=True, ): r""" A class decorator that adds :term:`dunder methods` according to the specified attributes using `attr.ib` or the *these* argument. Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will *never* go away, though). Args: collect_by_mro (bool): If True, *attrs* collects attributes from base classes correctly according to the `method resolution order <https://docs.python.org/3/howto/mro.html>`_. If False, *attrs* will mimic the (wrong) behavior of `dataclasses` and :pep:`681`. See also `issue #428 <https://github.com/python-attrs/attrs/issues/428>`_. repr_ns (str): When using nested classes, there was no way in Python 2 to automatically detect that. This argument allows to set a custom name for a more meaningful ``repr`` output. This argument is pointless in Python 3 and is therefore deprecated. .. caution:: Refer to `attrs.define` for the rest of the parameters, but note that they can have different defaults. Notably, leaving *on_setattr* as `None` will **not** add any hooks. .. versionadded:: 16.0.0 *slots* .. versionadded:: 16.1.0 *frozen* .. versionadded:: 16.3.0 *str* .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. .. versionchanged:: 17.1.0 *hash* supports `None` as value which is also the default now. .. versionadded:: 17.3.0 *auto_attribs* .. versionchanged:: 18.1.0 If *these* is passed, no attributes are deleted from the class body. .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. .. versionadded:: 18.2.0 *weakref_slot* .. deprecated:: 18.2.0 ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a `DeprecationWarning` if the classes compared are subclasses of each other. ``__eq`` and ``__ne__`` never tried to compared subclasses to each other. .. versionchanged:: 19.2.0 ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider subclasses comparable anymore. .. versionadded:: 18.2.0 *kw_only* .. versionadded:: 18.2.0 *cache_hash* .. versionadded:: 19.1.0 *auto_exc* .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. .. versionadded:: 19.2.0 *eq* and *order* .. versionadded:: 20.1.0 *auto_detect* .. versionadded:: 20.1.0 *collect_by_mro* .. versionadded:: 20.1.0 *getstate_setstate* .. versionadded:: 20.1.0 *on_setattr* .. versionadded:: 20.3.0 *field_transformer* .. versionchanged:: 21.1.0 ``init=False`` injects ``__attrs_init__`` .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` .. versionchanged:: 21.1.0 *cmp* undeprecated .. versionadded:: 21.3.0 *match_args* .. versionadded:: 22.2.0 *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). .. deprecated:: 24.1.0 *repr_ns* .. versionchanged:: 24.1.0 Instances are not compared as tuples of attributes anymore, but using a big ``and`` condition. This is faster and has more correct behavior for uncomparable values like `math.nan`. .. versionadded:: 24.1.0 If a class has an *inherited* classmethod called ``__attrs_init_subclass__``, it is executed after the class is created. .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. .. versionchanged:: 25.4.0 *kw_only* now only applies to attributes defined in the current class, and respects attribute-level ``kw_only=False`` settings. .. versionadded:: 25.4.0 *force_kw_only* """ if repr_ns is not None: import warnings warnings.warn( DeprecationWarning( "The `repr_ns` argument is deprecated and will be removed in or after August 2025." ), stacklevel=2, ) eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) # unsafe_hash takes precedence due to PEP 681. if unsafe_hash is not None: hash = unsafe_hash if isinstance(on_setattr, (list, tuple)): on_setattr = setters.pipe(*on_setattr) def wrap(cls): nonlocal hash is_frozen = frozen or _has_frozen_base_class(cls) is_exc = auto_exc is True and issubclass(cls, BaseException) has_own_setattr = auto_detect and _has_own_attribute( cls, "__setattr__" ) if has_own_setattr and is_frozen: msg = "Can't freeze a class with a custom __setattr__." raise ValueError(msg) eq = not is_exc and _determine_whether_to_implement( cls, eq_, auto_detect, ("__eq__", "__ne__") ) Hashability = ClassProps.Hashability if is_exc: hashability = Hashability.LEAVE_ALONE elif hash is True: hashability = ( Hashability.HASHABLE_CACHED if cache_hash else Hashability.HASHABLE ) elif hash is False: hashability = Hashability.LEAVE_ALONE elif hash is None: if auto_detect is True and _has_own_attribute(cls, "__hash__"): hashability = Hashability.LEAVE_ALONE elif eq is True and is_frozen is True: hashability = ( Hashability.HASHABLE_CACHED if cache_hash else Hashability.HASHABLE ) elif eq is False: hashability = Hashability.LEAVE_ALONE else: hashability = Hashability.UNHASHABLE else: msg = "Invalid value for hash. Must be True, False, or None." raise TypeError(msg) KeywordOnly = ClassProps.KeywordOnly if kw_only: kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES else: kwo = KeywordOnly.NO props = ClassProps( is_exception=is_exc, is_frozen=is_frozen, is_slotted=slots, collected_fields_by_mro=collect_by_mro, added_init=_determine_whether_to_implement( cls, init, auto_detect, ("__init__",) ), added_repr=_determine_whether_to_implement( cls, repr, auto_detect, ("__repr__",) ), added_eq=eq, added_ordering=not is_exc and _determine_whether_to_implement( cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__"), ), hashability=hashability, added_match_args=match_args, kw_only=kwo, has_weakref_slot=weakref_slot, added_str=str, added_pickling=_determine_whether_to_implement( cls, getstate_setstate, auto_detect, ("__getstate__", "__setstate__"), default=slots, ), on_setattr_hook=on_setattr, field_transformer=field_transformer, ) if not props.is_hashable and cache_hash: msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." raise TypeError(msg) builder = _ClassBuilder( cls, these, auto_attribs=auto_attribs, props=props, has_custom_setattr=has_own_setattr, ) if props.added_repr: builder.add_repr(repr_ns) if props.added_str: builder.add_str() if props.added_eq: builder.add_eq() if props.added_ordering: builder.add_order() if not frozen: builder.add_setattr() if props.is_hashable: builder.add_hash() elif props.hashability is Hashability.UNHASHABLE: builder.make_unhashable() if props.added_init: builder.add_init() else: builder.add_attrs_init() if cache_hash: msg = "Invalid value for cache_hash. To use hash caching, init must be True." raise TypeError(msg) if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): builder.add_replace() if match_args and not _has_own_attribute(cls, "__match_args__"): builder.add_match_args() return builder.build_class() # maybe_cls's type depends on the usage of the decorator. It's a class # if it's used as `@attrs` but `None` if used as `@attrs()`. if maybe_cls is None: return wrap return wrap(maybe_cls) _attrs = attrs """ Internal alias so we can use it in functions that take an argument called *attrs*. """ def _has_frozen_base_class(cls): """ Check whether *cls* has a frozen ancestor by looking at its __setattr__. """ return cls.__setattr__ is _frozen_setattrs def _generate_unique_filename(cls: type, func_name: str) -> str: """ Create a "filename" suitable for a function being generated. """ return ( f"<attrs generated {func_name} {cls.__module__}." f"{getattr(cls, '__qualname__', cls.__name__)}>" ) def _make_hash_script( cls: type, attrs: list[Attribute], frozen: bool, cache_hash: bool ) -> tuple[str, dict]: attrs = tuple( a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) ) tab = " " type_hash = hash(_generate_unique_filename(cls, "hash")) # If eq is custom generated, we need to include the functions in globs globs = {} hash_def = "def __hash__(self" hash_func = "hash((" closing_braces = "))" if not cache_hash: hash_def += "):" else: hash_def += ", *" hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" hash_func = "_cache_wrapper(" + hash_func closing_braces += ")" method_lines = [hash_def] def append_hash_computation_lines(prefix, indent): """ Generate the code for actually computing the hash code. Below this will either be returned directly or used to compute a value which is then cached, depending on the value of cache_hash """ method_lines.extend( [ indent + prefix + hash_func, indent + f" {type_hash},", ] ) for a in attrs: if a.eq_key: cmp_name = f"_{a.name}_key" globs[cmp_name] = a.eq_key method_lines.append( indent + f" {cmp_name}(self.{a.name})," ) else: method_lines.append(indent + f" self.{a.name},") method_lines.append(indent + " " + closing_braces) if cache_hash: method_lines.append(tab + f"if self.{_HASH_CACHE_FIELD} is None:") if frozen: append_hash_computation_lines( f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ", tab * 2 ) method_lines.append(tab * 2 + ")") # close __setattr__ else: append_hash_computation_lines( f"self.{_HASH_CACHE_FIELD} = ", tab * 2 ) method_lines.append(tab + f"return self.{_HASH_CACHE_FIELD}") else: append_hash_computation_lines("return ", tab) script = "\n".join(method_lines) return script, globs def _add_hash(cls: type, attrs: list[Attribute]): """ Add a hash method to *cls*. """ script, globs = _make_hash_script( cls, attrs, frozen=False, cache_hash=False ) _compile_and_eval( script, globs, filename=_generate_unique_filename(cls, "__hash__") ) cls.__hash__ = globs["__hash__"] return cls def __ne__(self, other): """ Check equality and either forward a NotImplemented or return the result negated. """ result = self.__eq__(other) if result is NotImplemented: return NotImplemented return not result def _make_eq_script(attrs: list) -> tuple[str, dict]: """ Create __eq__ method for *cls* with *attrs*. """ attrs = [a for a in attrs if a.eq] lines = [ "def __eq__(self, other):", " if other.__class__ is not self.__class__:", " return NotImplemented", ] globs = {} if attrs: lines.append(" return (") for a in attrs: if a.eq_key: cmp_name = f"_{a.name}_key" # Add the key function to the global namespace # of the evaluated function. globs[cmp_name] = a.eq_key lines.append( f" {cmp_name}(self.{a.name}) == {cmp_name}(other.{a.name})" ) else: lines.append(f" self.{a.name} == other.{a.name}") if a is not attrs[-1]: lines[-1] = f"{lines[-1]} and" lines.append(" )") else: lines.append(" return True") script = "\n".join(lines) return script, globs def _make_order(cls, attrs): """ Create ordering methods for *cls* with *attrs*. """ attrs = [a for a in attrs if a.order] def attrs_to_tuple(obj): """ Save us some typing. """ return tuple( key(value) if key else value for value, key in ( (getattr(obj, a.name), a.order_key) for a in attrs ) ) def __lt__(self, other): """ Automatically created by attrs. """ if other.__class__ is self.__class__: return attrs_to_tuple(self) < attrs_to_tuple(other) return NotImplemented def __le__(self, other): """ Automatically created by attrs. """ if other.__class__ is self.__class__: return attrs_to_tuple(self) <= attrs_to_tuple(other) return NotImplemented def __gt__(self, other): """ Automatically created by attrs. """ if other.__class__ is self.__class__: return attrs_to_tuple(self) > attrs_to_tuple(other) return NotImplemented def __ge__(self, other): """ Automatically created by attrs. """ if other.__class__ is self.__class__: return attrs_to_tuple(self) >= attrs_to_tuple(other) return NotImplemented return __lt__, __le__, __gt__, __ge__ def _add_eq(cls, attrs=None): """ Add equality methods to *cls* with *attrs*. """ if attrs is None: attrs = cls.__attrs_attrs__ script, globs = _make_eq_script(attrs) _compile_and_eval( script, globs, filename=_generate_unique_filename(cls, "__eq__") ) cls.__eq__ = globs["__eq__"] cls.__ne__ = __ne__ return cls def _make_repr_script(attrs, ns) -> tuple[str, dict]: """ Create the source and globs for a __repr__ and return it. """ # Figure out which attributes to include, and which function to use to # format them. The a.repr value can be either bool or a custom # callable. attr_names_with_reprs = tuple( (a.name, (repr if a.repr is True else a.repr), a.init) for a in attrs if a.repr is not False ) globs = { name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr } globs["_compat"] = _compat globs["AttributeError"] = AttributeError globs["NOTHING"] = NOTHING attribute_fragments = [] for name, r, i in attr_names_with_reprs: accessor = ( "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' ) fragment = ( "%s={%s!r}" % (name, accessor) if r == repr else "%s={%s_repr(%s)}" % (name, name, accessor) ) attribute_fragments.append(fragment) repr_fragment = ", ".join(attribute_fragments) if ns is None: cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' else: cls_name_fragment = ns + ".{self.__class__.__name__}" lines = [ "def __repr__(self):", " try:", " already_repring = _compat.repr_context.already_repring", " except AttributeError:", " already_repring = {id(self),}", " _compat.repr_context.already_repring = already_repring", " else:", " if id(self) in already_repring:", " return '...'", " else:", " already_repring.add(id(self))", " try:", f" return f'{cls_name_fragment}({repr_fragment})'", " finally:", " already_repring.remove(id(self))", ] return "\n".join(lines), globs def _add_repr(cls, ns=None, attrs=None): """ Add a repr method to *cls*. """ if attrs is None: attrs = cls.__attrs_attrs__ script, globs = _make_repr_script(attrs, ns) _compile_and_eval( script, globs, filename=_generate_unique_filename(cls, "__repr__") ) cls.__repr__ = globs["__repr__"] return cls def fields(cls): """ Return the tuple of *attrs* attributes for a class or instance. The tuple also allows accessing the fields by their names (see below for examples). Args: cls (type): Class or instance to introspect. Raises: TypeError: If *cls* is neither a class nor an *attrs* instance. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. Returns: tuple (with name accessors) of `attrs.Attribute` .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields by name. .. versionchanged:: 23.1.0 Add support for generic classes. .. versionchanged:: 26.1.0 Add support for instances. """ generic_base = get_generic_base(cls) if generic_base is None and not isinstance(cls, type): type_ = type(cls) if getattr(type_, "__attrs_attrs__", None) is None: msg = "Passed object must be a class or attrs instance." raise TypeError(msg) return fields(type_) attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: if generic_base is not None: attrs = getattr(generic_base, "__attrs_attrs__", None) if attrs is not None: # Even though this is global state, stick it on here to speed # it up. We rely on `cls` being cached for this to be # efficient. cls.__attrs_attrs__ = attrs return attrs msg = f"{cls!r} is not an attrs-decorated class." raise NotAnAttrsClassError(msg) return attrs def fields_dict(cls): """ Return an ordered dictionary of *attrs* attributes for a class, whose keys are the attribute names. Args: cls (type): Class to introspect. Raises: TypeError: If *cls* is not a class. attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* class. Returns: dict[str, attrs.Attribute]: Dict of attribute name to definition .. versionadded:: 18.1.0 """ if not isinstance(cls, type): msg = "Passed object must be a class." raise TypeError(msg) attrs = getattr(cls, "__attrs_attrs__", None) if attrs is None: msg = f"{cls!r} is not an attrs-decorated class." raise NotAnAttrsClassError(msg) return {a.name: a for a in attrs} def validate(inst): """ Validate all attributes on *inst* that have a validator. Leaves all exceptions through. Args: inst: Instance of a class with *attrs* attributes. """ if _config._run_validators is False: return for a in fields(inst.__class__): v = a.validator if v is not None: v(inst, a, getattr(inst, a.name)) def _is_slot_attr(a_name, base_attr_map): """ Check if the attribute name comes from a slot class. """ cls = base_attr_map.get(a_name) return cls and "__slots__" in cls.__dict__ def _make_init_script( cls, attrs, pre_init, pre_init_has_args, post_init, frozen, slots, cache_hash, base_attr_map, is_exc, cls_on_setattr, attrs_init, ) -> tuple[str, dict, dict]: has_cls_on_setattr = ( cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP ) if frozen and has_cls_on_setattr: msg = "Frozen classes can't use on_setattr." raise ValueError(msg) needs_cached_setattr = cache_hash or frozen filtered_attrs = [] attr_dict = {} for a in attrs: if not a.init and a.default is NOTHING: continue filtered_attrs.append(a) attr_dict[a.name] = a if a.on_setattr is not None: if frozen is True and a.on_setattr is not setters.NO_OP: msg = "Frozen classes can't use on_setattr." raise ValueError(msg) needs_cached_setattr = True elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: needs_cached_setattr = True script, globs, annotations = _attrs_to_init_script( filtered_attrs, frozen, slots, pre_init, pre_init_has_args, post_init, cache_hash, base_attr_map, is_exc, needs_cached_setattr, has_cls_on_setattr, "__attrs_init__" if attrs_init else "__init__", ) if cls.__module__ in sys.modules: # This makes typing.get_type_hints(CLS.__init__) resolve string types. globs.update(sys.modules[cls.__module__].__dict__) globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) if needs_cached_setattr: # Save the lookup overhead in __init__ if we need to circumvent # setattr hooks. globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__ return script, globs, annotations def _setattr(attr_name: str, value_var: str, has_on_setattr: bool) -> str: """ Use the cached object.setattr to set *attr_name* to *value_var*. """ return f"_setattr('{attr_name}', {value_var})" def _setattr_with_converter( attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter ) -> str: """ Use the cached object.setattr to set *attr_name* to *value_var*, but run its converter first. """ return f"_setattr('{attr_name}', {converter._fmt_converter_call(attr_name, value_var)})" def _assign(attr_name: str, value: str, has_on_setattr: bool) -> str: """ Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise relegate to _setattr. """ if has_on_setattr: return _setattr(attr_name, value, True) return f"self.{attr_name} = {value}" def _assign_with_converter( attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter ) -> str: """ Unless *attr_name* has an on_setattr hook, use normal assignment after conversion. Otherwise relegate to _setattr_with_converter. """ if has_on_setattr: return _setattr_with_converter(attr_name, value_var, True, converter) return f"self.{attr_name} = {converter._fmt_converter_call(attr_name, value_var)}" def _determine_setters( frozen: bool, slots: bool, base_attr_map: dict[str, type] ): """ Determine the correct setter functions based on whether a class is frozen and/or slotted. """ if frozen is True: if slots is True: return (), _setattr, _setattr_with_converter # Dict frozen classes assign directly to __dict__. # But only if the attribute doesn't come from an ancestor slot # class. # Note _inst_dict will be used again below if cache_hash is True def fmt_setter( attr_name: str, value_var: str, has_on_setattr: bool ) -> str: if _is_slot_attr(attr_name, base_attr_map): return _setattr(attr_name, value_var, has_on_setattr) return f"_inst_dict['{attr_name}'] = {value_var}" def fmt_setter_with_converter( attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter, ) -> str: if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): return _setattr_with_converter( attr_name, value_var, has_on_setattr, converter ) return f"_inst_dict['{attr_name}'] = {converter._fmt_converter_call(attr_name, value_var)}" return ( ("_inst_dict = self.__dict__",), fmt_setter, fmt_setter_with_converter, ) # Not frozen -- we can just assign directly. return (), _assign, _assign_with_converter def _attrs_to_init_script( attrs: list[Attribute], is_frozen: bool, is_slotted: bool, call_pre_init: bool, pre_init_has_args: bool, call_post_init: bool, does_cache_hash: bool, base_attr_map: dict[str, type], is_exc: bool, needs_cached_setattr: bool, has_cls_on_setattr: bool, method_name: str, ) -> tuple[str, dict, dict]: """ Return a script of an initializer for *attrs*, a dict of globals, and annotations for the initializer. The globals are required by the generated script. """ lines = ["self.__attrs_pre_init__()"] if call_pre_init else [] if needs_cached_setattr: lines.append( # Circumvent the __setattr__ descriptor to save one lookup per # assignment. Note _setattr will be used again below if # does_cache_hash is True. "_setattr = _cached_setattr_get(self)" ) extra_lines, fmt_setter, fmt_setter_with_converter = _determine_setters( is_frozen, is_slotted, base_attr_map ) lines.extend(extra_lines) args = [] # Parameters in the definition of __init__ pre_init_args = [] # Parameters in the call to __attrs_pre_init__ kw_only_args = [] # Used for both 'args' and 'pre_init_args' above attrs_to_validate = [] # This is a dictionary of names to validator and converter callables. # Injecting this into __init__ globals lets us avoid lookups. names_for_globals = {} annotations = {"return": None} for a in attrs: if a.validator: attrs_to_validate.append(a) attr_name = a.name has_on_setattr = a.on_setattr is not None or ( a.on_setattr is not setters.NO_OP and has_cls_on_setattr ) # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not # explicitly provided arg_name = a.alias has_factory = isinstance(a.default, Factory) maybe_self = "self" if has_factory and a.default.takes_self else "" if a.converter is not None and not isinstance(a.converter, Converter): converter = Converter(a.converter) else: converter = a.converter if a.init is False: if has_factory: init_factory_name = _INIT_FACTORY_PAT % (a.name,) if converter is not None: lines.append( fmt_setter_with_converter( attr_name, init_factory_name + f"({maybe_self})", has_on_setattr, converter, ) ) names_for_globals[converter._get_global_name(a.name)] = ( converter.converter ) else: lines.append( fmt_setter( attr_name, init_factory_name + f"({maybe_self})", has_on_setattr, ) ) names_for_globals[init_factory_name] = a.default.factory elif converter is not None: lines.append( fmt_setter_with_converter( attr_name, f"attr_dict['{attr_name}'].default", has_on_setattr, converter, ) ) names_for_globals[converter._get_global_name(a.name)] = ( converter.converter ) else: lines.append( fmt_setter( attr_name, f"attr_dict['{attr_name}'].default", has_on_setattr, ) ) elif a.default is not NOTHING and not has_factory: arg = f"{arg_name}=attr_dict['{attr_name}'].default" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) pre_init_args.append(arg_name) if converter is not None: lines.append( fmt_setter_with_converter( attr_name, arg_name, has_on_setattr, converter ) ) names_for_globals[converter._get_global_name(a.name)] = ( converter.converter ) else: lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) elif has_factory: arg = f"{arg_name}=NOTHING" if a.kw_only: kw_only_args.append(arg) else: args.append(arg) pre_init_args.append(arg_name) lines.append(f"if {arg_name} is not NOTHING:") init_factory_name = _INIT_FACTORY_PAT % (a.name,) if converter is not None: lines.append( " " + fmt_setter_with_converter( attr_name, arg_name, has_on_setattr, converter ) ) lines.append("else:") lines.append( " " + fmt_setter_with_converter( attr_name, init_factory_name + "(" + maybe_self + ")", has_on_setattr, converter, ) ) names_for_globals[converter._get_global_name(a.name)] = ( converter.converter ) else: lines.append( " " + fmt_setter(attr_name, arg_name, has_on_setattr) ) lines.append("else:") lines.append( " " + fmt_setter( attr_name, init_factory_name + "(" + maybe_self + ")", has_on_setattr, ) ) names_for_globals[init_factory_name] = a.default.factory else: if a.kw_only: kw_only_args.append(arg_name) else: args.append(arg_name) pre_init_args.append(arg_name) if converter is not None: lines.append( fmt_setter_with_converter( attr_name, arg_name, has_on_setattr, converter ) ) names_for_globals[converter._get_global_name(a.name)] = ( converter.converter ) else: lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) if a.init is True: if a.type is not None and converter is None: annotations[arg_name] = a.type elif converter is not None and converter._first_param_type: # Use the type from the converter if present. annotations[arg_name] = converter._first_param_type if attrs_to_validate: # we can skip this if there are no validators. names_for_globals["_config"] = _config lines.append("if _config._run_validators is True:") for a in attrs_to_validate: val_name = "__attr_validator_" + a.name attr_name = "__attr_" + a.name lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a if call_post_init: lines.append("self.__attrs_post_init__()") # Because this is set only after __attrs_post_init__ is called, a crash # will result if post-init tries to access the hash code. This seemed # preferable to setting this beforehand, in which case alteration to field # values during post-init combined with post-init accessing the hash code # would result in silent bugs. if does_cache_hash: if is_frozen: if is_slotted: init_hash_cache = f"_setattr('{_HASH_CACHE_FIELD}', None)" else: init_hash_cache = f"_inst_dict['{_HASH_CACHE_FIELD}'] = None" else: init_hash_cache = f"self.{_HASH_CACHE_FIELD} = None" lines.append(init_hash_cache) # For exceptions we rely on BaseException.__init__ for proper # initialization. if is_exc: vals = ",".join(f"self.{a.name}" for a in attrs if a.init) lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) pre_init_args = ", ".join(pre_init_args) if kw_only_args: # leading comma & kw_only args args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" pre_init_kw_only_args = ", ".join( [ f"{kw_arg_name}={kw_arg_name}" # We need to remove the defaults from the kw_only_args. for kw_arg_name in (kwa.split("=")[0] for kwa in kw_only_args) ] ) pre_init_args += ", " if pre_init_args else "" pre_init_args += pre_init_kw_only_args if call_pre_init and pre_init_has_args: # If pre init method has arguments, pass the values given to __init__. lines[0] = f"self.__attrs_pre_init__({pre_init_args})" # Python <3.12 doesn't allow backslashes in f-strings. NL = "\n " return ( f"""def {method_name}(self, {args}): {NL.join(lines) if lines else "pass"} """, names_for_globals, annotations, ) def _default_init_alias_for(name: str) -> str: """ The default __init__ parameter name for a field. This performs private-name adjustment via leading-unscore stripping, and is the default value of Attribute.alias if not provided. """ return name.lstrip("_") class Attribute: """ *Read-only* representation of an attribute. .. warning:: You should never instantiate this class yourself. The class has *all* arguments of `attr.ib` (except for ``factory`` which is only syntactic sugar for ``default=Factory(...)`` plus the following: - ``name`` (`str`): The name of the attribute. - ``alias`` (`str`): The __init__ parameter name of the attribute, after any explicit overrides and default private-attribute-name handling. - ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically generated (``True``) or explicitly provided by the user (``False``). - ``inherited`` (`bool`): Whether or not that attribute has been inherited from a base class. - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables that are used for comparing and ordering objects by this attribute, respectively. These are set by passing a callable to `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also :ref:`comparison customization <custom-comparison>`. Instances of this class are frequently used for introspection purposes like: - `fields` returns a tuple of them. - Validators get them passed as the first argument. - The :ref:`field transformer <transform-fields>` hook receives a list of them. - The ``alias`` property exposes the __init__ parameter name of the field, with any overrides and default private-attribute handling applied. .. versionadded:: 20.1.0 *inherited* .. versionadded:: 20.1.0 *on_setattr* .. versionchanged:: 20.2.0 *inherited* is not taken into account for equality checks and hashing anymore. .. versionadded:: 21.1.0 *eq_key* and *order_key* .. versionadded:: 22.2.0 *alias* .. versionadded:: 26.1.0 *alias_is_default* For the full version history of the fields, see `attr.ib`. """ # These slots must NOT be reordered because we use them later for # instantiation. __slots__ = ( # noqa: RUF023 "name", "default", "validator", "repr", "eq", "eq_key", "order", "order_key", "hash", "init", "metadata", "type", "converter", "kw_only", "inherited", "on_setattr", "alias", "alias_is_default", ) def __init__( self, name, default, validator, repr, cmp, # XXX: unused, remove along with other cmp code. hash, init, inherited, metadata=None, type=None, converter=None, kw_only=False, eq=None, eq_key=None, order=None, order_key=None, on_setattr=None, alias=None, alias_is_default=None, ): eq, eq_key, order, order_key = _determine_attrib_eq_order( cmp, eq_key or eq, order_key or order, True ) # Cache this descriptor here to speed things up later. bound_setattr = _OBJ_SETATTR.__get__(self) # Despite the big red warning, people *do* instantiate `Attribute` # themselves. bound_setattr("name", name) bound_setattr("default", default) bound_setattr("validator", validator) bound_setattr("repr", repr) bound_setattr("eq", eq) bound_setattr("eq_key", eq_key) bound_setattr("order", order) bound_setattr("order_key", order_key) bound_setattr("hash", hash) bound_setattr("init", init) bound_setattr("converter", converter) bound_setattr( "metadata", ( types.MappingProxyType(dict(metadata)) # Shallow copy if metadata else _EMPTY_METADATA_SINGLETON ), ) bound_setattr("type", type) bound_setattr("kw_only", kw_only) bound_setattr("inherited", inherited) bound_setattr("on_setattr", on_setattr) bound_setattr("alias", alias) bound_setattr( "alias_is_default", alias is None if alias_is_default is None else alias_is_default, ) def __setattr__(self, name, value): raise FrozenInstanceError @classmethod def from_counting_attr( cls, name: str, ca: _CountingAttr, kw_only: bool, type=None ): # The 'kw_only' argument is the class-level setting, and is used if the # attribute itself does not explicitly set 'kw_only'. # type holds the annotated value. deal with conflicts: if type is None: type = ca.type elif ca.type is not None: msg = f"Type annotation and type argument cannot both be present for '{name}'." raise ValueError(msg) return cls( name, ca._default, ca._validator, ca.repr, None, ca.hash, ca.init, False, ca.metadata, type, ca._converter, kw_only if ca.kw_only is None else ca.kw_only, ca.eq, ca.eq_key, ca.order, ca.order_key, ca.on_setattr, ca.alias, ca.alias is None, ) # Don't use attrs.evolve since fields(Attribute) doesn't work def evolve(self, **changes): """ Copy *self* and apply *changes*. This works similarly to `attrs.evolve` but that function does not work with :class:`attrs.Attribute`. It is mainly meant to be used for `transform-fields`. .. versionadded:: 20.3.0 """ import copy new = copy.copy(self) new._setattrs(changes.items()) if "alias" in changes and "alias_is_default" not in changes: # Explicit alias provided -- no longer the default. _OBJ_SETATTR.__get__(new)("alias_is_default", False) elif ( "name" in changes and "alias" not in changes # Don't auto-generate alias if the user picked picked the old one. and self.alias_is_default ): # Name changed, alias was auto-generated -- update it. _OBJ_SETATTR.__get__(new)( "alias", _default_init_alias_for(new.name) ) return new # Don't use _add_pickle since fields(Attribute) doesn't work def __getstate__(self): """ Play nice with pickle. """ return tuple( getattr(self, name) if name != "metadata" else dict(self.metadata) for name in self.__slots__ ) def __setstate__(self, state): """ Play nice with pickle. """ if len(state) < len(self.__slots__): # Pre-26.1.0 pickle without alias_is_default -- infer it # heuristically. state_dict = dict(zip(self.__slots__, state, strict=False)) alias_is_default = state_dict.get( "alias" ) is None or state_dict.get("alias") == _default_init_alias_for( state_dict["name"] ) state = (*state, alias_is_default) self._setattrs(zip(self.__slots__, state, strict=True)) def _setattrs(self, name_values_pairs): bound_setattr = _OBJ_SETATTR.__get__(self) for name, value in name_values_pairs: if name != "metadata": bound_setattr(name, value) else: bound_setattr( name, ( types.MappingProxyType(dict(value)) if value else _EMPTY_METADATA_SINGLETON ), ) _a = [ Attribute( name=name, default=NOTHING, validator=None, repr=(name != "alias_is_default"), cmp=None, eq=True, order=False, hash=(name != "metadata"), init=True, inherited=False, alias=_default_init_alias_for(name), ) for name in Attribute.__slots__ ] Attribute = _add_hash( _add_eq( _add_repr(Attribute, attrs=_a), attrs=[a for a in _a if a.name != "inherited"], ), attrs=[a for a in _a if a.hash and a.name != "inherited"], ) class _CountingAttr: """ Intermediate representation of attributes that uses a counter to preserve the order in which the attributes have been defined. *Internal* data structure of the attrs library. Running into is most likely the result of a bug like a forgotten `@attr.s` decorator. """ __slots__ = ( "_converter", "_default", "_validator", "alias", "counter", "eq", "eq_key", "hash", "init", "kw_only", "metadata", "on_setattr", "order", "order_key", "repr", "type", ) __attrs_attrs__ = ( *tuple( Attribute( name=name, alias=_default_init_alias_for(name), default=NOTHING, validator=None, repr=True, cmp=None, hash=True, init=True, kw_only=False, eq=True, eq_key=None, order=False, order_key=None, inherited=False, on_setattr=None, ) for name in ( "counter", "_default", "repr", "eq", "order", "hash", "init", "on_setattr", "alias", ) ), Attribute( name="metadata", alias="metadata", default=None, validator=None, repr=True, cmp=None, hash=False, init=True, kw_only=False, eq=True, eq_key=None, order=False, order_key=None, inherited=False, on_setattr=None, ), ) cls_counter = 0 def __init__( self, default, validator, repr, cmp, hash, init, converter, metadata, type, kw_only, eq, eq_key, order, order_key, on_setattr, alias, ): _CountingAttr.cls_counter += 1 self.counter = _CountingAttr.cls_counter self._default = default self._validator = validator self._converter = converter self.repr = repr self.eq = eq self.eq_key = eq_key self.order = order self.order_key = order_key self.hash = hash self.init = init self.metadata = metadata self.type = type self.kw_only = kw_only self.on_setattr = on_setattr self.alias = alias def validator(self, meth): """ Decorator that adds *meth* to the list of validators. Returns *meth* unchanged. .. versionadded:: 17.1.0 """ if self._validator is None: self._validator = meth else: self._validator = and_(self._validator, meth) return meth def default(self, meth): """ Decorator that allows to set the default for an attribute. Returns *meth* unchanged. Raises: DefaultAlreadySetError: If default has been set before. .. versionadded:: 17.1.0 """ if self._default is not NOTHING: raise DefaultAlreadySetError self._default = Factory(meth, takes_self=True) return meth def converter(self, meth): """ Decorator that appends *meth* to the list of converters. Returns *meth* unchanged. .. versionadded:: 26.2.0 """ decorated_converter = Converter( lambda value, _self, field: meth(_self, field, value), takes_self=True, takes_field=True, ) if self._converter is None: self._converter = decorated_converter else: self._converter = pipe(self._converter, decorated_converter) return meth _CountingAttr = _add_eq(_add_repr(_CountingAttr)) class ClassProps: """ Effective class properties as derived from parameters to `attr.s()` or `define()` decorators. This is the same data structure that *attrs* uses internally to decide how to construct the final class. Warning: This feature is currently **experimental** and is not covered by our strict backwards-compatibility guarantees. Attributes: is_exception (bool): Whether the class is treated as an exception class. is_slotted (bool): Whether the class is `slotted <slotted classes>`. has_weakref_slot (bool): Whether the class has a slot for weak references. is_frozen (bool): Whether the class is frozen. kw_only (KeywordOnly): Whether / how the class enforces keyword-only arguments on the ``__init__`` method. collected_fields_by_mro (bool): Whether the class fields were collected by method resolution order. That is, correctly but unlike `dataclasses`. added_init (bool): Whether the class has an *attrs*-generated ``__init__`` method. added_repr (bool): Whether the class has an *attrs*-generated ``__repr__`` method. added_eq (bool): Whether the class has *attrs*-generated equality methods. added_ordering (bool): Whether the class has *attrs*-generated ordering methods. hashability (Hashability): How `hashable <hashing>` the class is. added_match_args (bool): Whether the class supports positional `match <match>` over its fields. added_str (bool): Whether the class has an *attrs*-generated ``__str__`` method. added_pickling (bool): Whether the class has *attrs*-generated ``__getstate__`` and ``__setstate__`` methods for `pickle`. on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None): The class's ``__setattr__`` hook. field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None): The class's `field transformers <transform-fields>`. .. versionadded:: 25.4.0 """ class Hashability(enum.Enum): """ The hashability of a class. .. versionadded:: 25.4.0 """ HASHABLE = "hashable" """Write a ``__hash__``.""" HASHABLE_CACHED = "hashable_cache" """Write a ``__hash__`` and cache the hash.""" UNHASHABLE = "unhashable" """Set ``__hash__`` to ``None``.""" LEAVE_ALONE = "leave_alone" """Don't touch ``__hash__``.""" class KeywordOnly(enum.Enum): """ How attributes should be treated regarding keyword-only parameters. .. versionadded:: 25.4.0 """ NO = "no" """Attributes are not keyword-only.""" YES = "yes" """Attributes in current class without kw_only=False are keyword-only.""" FORCE = "force" """All attributes are keyword-only.""" __slots__ = ( # noqa: RUF023 -- order matters for __init__ "is_exception", "is_slotted", "has_weakref_slot", "is_frozen", "kw_only", "collected_fields_by_mro", "added_init", "added_repr", "added_eq", "added_ordering", "hashability", "added_match_args", "added_str", "added_pickling", "on_setattr_hook", "field_transformer", ) def __init__( self, is_exception, is_slotted, has_weakref_slot, is_frozen, kw_only, collected_fields_by_mro, added_init, added_repr, added_eq, added_ordering, hashability, added_match_args, added_str, added_pickling, on_setattr_hook, field_transformer, ): self.is_exception = is_exception self.is_slotted = is_slotted self.has_weakref_slot = has_weakref_slot self.is_frozen = is_frozen self.kw_only = kw_only self.collected_fields_by_mro = collected_fields_by_mro self.added_init = added_init self.added_repr = added_repr self.added_eq = added_eq self.added_ordering = added_ordering self.hashability = hashability self.added_match_args = added_match_args self.added_str = added_str self.added_pickling = added_pickling self.on_setattr_hook = on_setattr_hook self.field_transformer = field_transformer @property def is_hashable(self): return ( self.hashability is ClassProps.Hashability.HASHABLE or self.hashability is ClassProps.Hashability.HASHABLE_CACHED ) _cas = [ Attribute( name=name, default=NOTHING, validator=None, repr=True, cmp=None, eq=True, order=False, hash=True, init=True, inherited=False, alias=_default_init_alias_for(name), ) for name in ClassProps.__slots__ ] ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas) class Factory: """ Stores a factory callable. If passed as the default value to `attrs.field`, the factory is used to generate a new value. Args: factory (typing.Callable): A callable that takes either none or exactly one mandatory positional argument depending on *takes_self*. takes_self (bool): Pass the partially initialized instance that is being initialized as a positional argument. .. versionadded:: 17.1.0 *takes_self* """ __slots__ = ("factory", "takes_self") def __init__(self, factory, takes_self=False): self.factory = factory self.takes_self = takes_self def __getstate__(self): """ Play nice with pickle. """ return tuple(getattr(self, name) for name in self.__slots__) def __setstate__(self, state): """ Play nice with pickle. """ for name, value in zip(self.__slots__, state, strict=True): setattr(self, name, value) _f = [ Attribute( name=name, default=NOTHING, validator=None, repr=True, cmp=None, eq=True, order=False, hash=True, init=True, inherited=False, ) for name in Factory.__slots__ ] Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) class Converter: """ Stores a converter callable. Allows for the wrapped converter to take additional arguments. The arguments are passed in the order they are documented. Args: converter (Callable): A callable that converts the passed value. takes_self (bool): Pass the partially initialized instance that is being initialized as a positional argument. (default: `False`) takes_field (bool): Pass the field definition (an :class:`Attribute`) into the converter as a positional argument. (default: `False`) .. versionadded:: 24.1.0 """ __slots__ = ( "__call__", "_first_param_type", "_global_name", "converter", "takes_field", "takes_self", ) def __init__(self, converter, *, takes_self=False, takes_field=False): self.converter = converter self.takes_self = takes_self self.takes_field = takes_field ex = _AnnotationExtractor(converter) self._first_param_type = ex.get_first_param_type() if not (self.takes_self or self.takes_field): self.__call__ = lambda value, _, __: self.converter(value) elif self.takes_self and not self.takes_field: self.__call__ = lambda value, instance, __: self.converter( value, instance ) elif not self.takes_self and self.takes_field: self.__call__ = lambda value, __, field: self.converter( value, field ) else: self.__call__ = self.converter rt = ex.get_return_type() if rt is not None: self.__call__.__annotations__["return"] = rt @staticmethod def _get_global_name(attr_name: str) -> str: """ Return the name that a converter for an attribute name *attr_name* would have. """ return f"__attr_converter_{attr_name}" def _fmt_converter_call(self, attr_name: str, value_var: str) -> str: """ Return a string that calls the converter for an attribute name *attr_name* and the value in variable named *value_var* according to `self.takes_self` and `self.takes_field`. """ if not (self.takes_self or self.takes_field): return f"{self._get_global_name(attr_name)}({value_var})" if self.takes_self and self.takes_field: return f"{self._get_global_name(attr_name)}({value_var}, self, attr_dict['{attr_name}'])" if self.takes_self: return f"{self._get_global_name(attr_name)}({value_var}, self)" return f"{self._get_global_name(attr_name)}({value_var}, attr_dict['{attr_name}'])" def __getstate__(self): """ Return a dict containing only converter and takes_self -- the rest gets computed when loading. """ return { "converter": self.converter, "takes_self": self.takes_self, "takes_field": self.takes_field, } def __setstate__(self, state): """ Load instance from state. """ self.__init__(**state) _f = [ Attribute( name=name, default=NOTHING, validator=None, repr=True, cmp=None, eq=True, order=False, hash=True, init=True, inherited=False, ) for name in ("converter", "takes_self", "takes_field") ] Converter = _add_hash( _add_eq(_add_repr(Converter, attrs=_f), attrs=_f), attrs=_f ) def make_class( name, attrs, bases=(object,), class_body=None, **attributes_arguments ): r""" A quick way to create a new class called *name* with *attrs*. .. note:: ``make_class()`` is a thin wrapper around `attr.s`, not `attrs.define` which means that it doesn't come with some of the improved defaults. For example, if you want the same ``on_setattr`` behavior as in `attrs.define`, you have to pass the hooks yourself: ``make_class(..., on_setattr=setters.pipe(setters.convert, setters.validate)`` .. warning:: It is *your* duty to ensure that the class name and the attribute names are valid identifiers. ``make_class()`` will *not* validate them for you. Args: name (str): The name for the new class. attrs (list | dict): A list of names or a dictionary of mappings of names to `attr.ib`\ s / `attrs.field`\ s. The order is deduced from the order of the names or attributes inside *attrs*. Otherwise the order of the definition of the attributes is used. bases (tuple[type, ...]): Classes that the new class will subclass. class_body (dict): An optional dictionary of class attributes for the new class. attributes_arguments: Passed unmodified to `attr.s`. Returns: type: A new class with *attrs*. .. versionadded:: 17.1.0 *bases* .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. .. versionchanged:: 23.2.0 *class_body* .. versionchanged:: 25.2.0 Class names can now be unicode. """ # Class identifiers are converted into the normal form NFKC while parsing name = unicodedata.normalize("NFKC", name) if isinstance(attrs, dict): cls_dict = attrs elif isinstance(attrs, (list, tuple)): cls_dict = {a: attrib() for a in attrs} else: msg = "attrs argument must be a dict or a list." raise TypeError(msg) pre_init = cls_dict.pop("__attrs_pre_init__", None) post_init = cls_dict.pop("__attrs_post_init__", None) user_init = cls_dict.pop("__init__", None) body = {} if class_body is not None: body.update(class_body) if pre_init is not None: body["__attrs_pre_init__"] = pre_init if post_init is not None: body["__attrs_post_init__"] = post_init if user_init is not None: body["__init__"] = user_init type_ = types.new_class(name, bases, {}, lambda ns: ns.update(body)) # For pickling to work, the __module__ variable needs to be set to the # frame where the class is created. Bypass this step in environments where # sys._getframe is not defined (Jython for example) or sys._getframe is not # defined for arguments greater than 0 (IronPython). with contextlib.suppress(AttributeError, ValueError): type_.__module__ = sys._getframe(1).f_globals.get( "__name__", "__main__" ) # We do it here for proper warnings with meaningful stacklevel. cmp = attributes_arguments.pop("cmp", None) ( attributes_arguments["eq"], attributes_arguments["order"], ) = _determine_attrs_eq_order( cmp, attributes_arguments.get("eq"), attributes_arguments.get("order"), True, ) cls = _attrs(these=cls_dict, **attributes_arguments)(type_) # Only add type annotations now or "_attrs()" will complain: cls.__annotations__ = { k: v.type for k, v in cls_dict.items() if v.type is not None } return cls # These are required by within this module so we define them here and merely # import into .validators / .converters. @attrs(slots=True, unsafe_hash=True) class _AndValidator: """ Compose many validators to a single one. """ _validators = attrib() def __call__(self, inst, attr, value): for v in self._validators: v(inst, attr, value) def and_(*validators): """ A validator that composes multiple validators into one. When called on a value, it runs all wrapped validators. Args: validators (~collections.abc.Iterable[typing.Callable]): Arbitrary number of validators. .. versionadded:: 17.1.0 """ vals = [] for validator in validators: vals.extend( validator._validators if isinstance(validator, _AndValidator) else [validator] ) return _AndValidator(tuple(vals)) def pipe(*converters): """ A converter that composes multiple converters into one. When called on a value, it runs all wrapped converters, returning the *last* value. Type annotations will be inferred from the wrapped converters', if they have any. Args: converters (~collections.abc.Iterable[typing.Callable]): Arbitrary number of converters. .. versionadded:: 20.1.0 """ return_instance = any(isinstance(c, Converter) for c in converters) if return_instance: def pipe_converter(val, inst, field): for c in converters: val = ( c(val, inst, field) if isinstance(c, Converter) else c(val) ) return val else: def pipe_converter(val): for c in converters: val = c(val) return val if not converters: # If the converter list is empty, pipe_converter is the identity. A = TypeVar("A") pipe_converter.__annotations__.update({"val": A, "return": A}) else: # Get parameter type from first converter. t = _AnnotationExtractor(converters[0]).get_first_param_type() if t: pipe_converter.__annotations__["val"] = t last = converters[-1] if not PY_3_11_PLUS and isinstance(last, Converter): last = last.__call__ # Get return type from last converter. rt = _AnnotationExtractor(last).get_return_type() if rt: pipe_converter.__annotations__["return"] = rt if return_instance: return Converter(pipe_converter, takes_self=True, takes_field=True) return pipe_converter