Spaces:
Sleeping
Sleeping
| # SPDX-License-Identifier: MIT | |
| from __future__ import annotations | |
| import abc | |
| import contextlib | |
| import copy | |
| import enum | |
| import inspect | |
| 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_10_PLUS, | |
| PY_3_11_PLUS, | |
| PY_3_13_PLUS, | |
| _AnnotationExtractor, | |
| _get_annotations, | |
| 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. | |
| """ | |
| 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 = 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 | |
| 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 = self._patch_original_class() | |
| if PY_3_10_PLUS: | |
| cls = abc.update_abstractmethods(cls) | |
| # 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: | |
| 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): | |
| __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): | |
| self._cls_dict["__replace__"] = self._add_method_dunders(evolve) | |
| 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 | |
| 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 = sa_attrs[name] | |
| except KeyError: | |
| nval = val | |
| else: | |
| nval = hook(self, a, val) | |
| _OBJ_SETATTR(self, name, nval) | |
| 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: | |
| 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 ( | |
| PY_3_10_PLUS | |
| and 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 | |
| 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 | |
| """ | |
| 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)) | |
| 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)) | |
| 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__ = ( | |
| "_default", | |
| "_validator", | |
| "alias", | |
| "converter", | |
| "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 | |
| _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 | |
| 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): | |
| 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 | |
| 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. | |
| 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. | |
| 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 | |