| | |
| |
|
| |
|
| | import copy |
| |
|
| | from ._compat import PY_3_9_PLUS, get_generic_base |
| | from ._make import NOTHING, _obj_setattr, fields |
| | from .exceptions import AttrsAttributeNotFoundError |
| |
|
| |
|
| | def asdict( |
| | inst, |
| | recurse=True, |
| | filter=None, |
| | dict_factory=dict, |
| | retain_collection_types=False, |
| | value_serializer=None, |
| | ): |
| | """ |
| | Return the *attrs* attribute values of *inst* as a dict. |
| | |
| | Optionally recurse into other *attrs*-decorated classes. |
| | |
| | :param inst: Instance of an *attrs*-decorated class. |
| | :param bool recurse: Recurse into classes that are also |
| | *attrs*-decorated. |
| | :param callable filter: A callable whose return code determines whether an |
| | attribute or element is included (``True``) or dropped (``False``). Is |
| | called with the `attrs.Attribute` as the first argument and the |
| | value as the second argument. |
| | :param callable dict_factory: A callable to produce dictionaries from. For |
| | example, to produce ordered dictionaries instead of normal Python |
| | dictionaries, pass in ``collections.OrderedDict``. |
| | :param bool retain_collection_types: Do not convert to ``list`` when |
| | encountering an attribute whose type is ``tuple`` or ``set``. Only |
| | meaningful if ``recurse`` is ``True``. |
| | :param Optional[callable] value_serializer: A hook that is called for every |
| | attribute or dict key/value. It receives the current instance, field |
| | and value and must return the (updated) value. The hook is run *after* |
| | the optional *filter* has been applied. |
| | |
| | :rtype: return type of *dict_factory* |
| | |
| | :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* |
| | class. |
| | |
| | .. versionadded:: 16.0.0 *dict_factory* |
| | .. versionadded:: 16.1.0 *retain_collection_types* |
| | .. versionadded:: 20.3.0 *value_serializer* |
| | .. versionadded:: 21.3.0 If a dict has a collection for a key, it is |
| | serialized as a tuple. |
| | """ |
| | attrs = fields(inst.__class__) |
| | rv = dict_factory() |
| | for a in attrs: |
| | v = getattr(inst, a.name) |
| | if filter is not None and not filter(a, v): |
| | continue |
| |
|
| | if value_serializer is not None: |
| | v = value_serializer(inst, a, v) |
| |
|
| | if recurse is True: |
| | if has(v.__class__): |
| | rv[a.name] = asdict( |
| | v, |
| | recurse=True, |
| | filter=filter, |
| | dict_factory=dict_factory, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ) |
| | elif isinstance(v, (tuple, list, set, frozenset)): |
| | cf = v.__class__ if retain_collection_types is True else list |
| | rv[a.name] = cf( |
| | [ |
| | _asdict_anything( |
| | i, |
| | is_key=False, |
| | filter=filter, |
| | dict_factory=dict_factory, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ) |
| | for i in v |
| | ] |
| | ) |
| | elif isinstance(v, dict): |
| | df = dict_factory |
| | rv[a.name] = df( |
| | ( |
| | _asdict_anything( |
| | kk, |
| | is_key=True, |
| | filter=filter, |
| | dict_factory=df, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ), |
| | _asdict_anything( |
| | vv, |
| | is_key=False, |
| | filter=filter, |
| | dict_factory=df, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ), |
| | ) |
| | for kk, vv in v.items() |
| | ) |
| | else: |
| | rv[a.name] = v |
| | else: |
| | rv[a.name] = v |
| | return rv |
| |
|
| |
|
| | def _asdict_anything( |
| | val, |
| | is_key, |
| | filter, |
| | dict_factory, |
| | retain_collection_types, |
| | value_serializer, |
| | ): |
| | """ |
| | ``asdict`` only works on attrs instances, this works on anything. |
| | """ |
| | if getattr(val.__class__, "__attrs_attrs__", None) is not None: |
| | |
| | rv = asdict( |
| | val, |
| | recurse=True, |
| | filter=filter, |
| | dict_factory=dict_factory, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ) |
| | elif isinstance(val, (tuple, list, set, frozenset)): |
| | if retain_collection_types is True: |
| | cf = val.__class__ |
| | elif is_key: |
| | cf = tuple |
| | else: |
| | cf = list |
| |
|
| | rv = cf( |
| | [ |
| | _asdict_anything( |
| | i, |
| | is_key=False, |
| | filter=filter, |
| | dict_factory=dict_factory, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ) |
| | for i in val |
| | ] |
| | ) |
| | elif isinstance(val, dict): |
| | df = dict_factory |
| | rv = df( |
| | ( |
| | _asdict_anything( |
| | kk, |
| | is_key=True, |
| | filter=filter, |
| | dict_factory=df, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ), |
| | _asdict_anything( |
| | vv, |
| | is_key=False, |
| | filter=filter, |
| | dict_factory=df, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ), |
| | ) |
| | for kk, vv in val.items() |
| | ) |
| | else: |
| | rv = val |
| | if value_serializer is not None: |
| | rv = value_serializer(None, None, rv) |
| |
|
| | return rv |
| |
|
| |
|
| | def astuple( |
| | inst, |
| | recurse=True, |
| | filter=None, |
| | tuple_factory=tuple, |
| | retain_collection_types=False, |
| | ): |
| | """ |
| | Return the *attrs* attribute values of *inst* as a tuple. |
| | |
| | Optionally recurse into other *attrs*-decorated classes. |
| | |
| | :param inst: Instance of an *attrs*-decorated class. |
| | :param bool recurse: Recurse into classes that are also |
| | *attrs*-decorated. |
| | :param callable filter: A callable whose return code determines whether an |
| | attribute or element is included (``True``) or dropped (``False``). Is |
| | called with the `attrs.Attribute` as the first argument and the |
| | value as the second argument. |
| | :param callable tuple_factory: A callable to produce tuples from. For |
| | example, to produce lists instead of tuples. |
| | :param bool retain_collection_types: Do not convert to ``list`` |
| | or ``dict`` when encountering an attribute which type is |
| | ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is |
| | ``True``. |
| | |
| | :rtype: return type of *tuple_factory* |
| | |
| | :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* |
| | class. |
| | |
| | .. versionadded:: 16.2.0 |
| | """ |
| | attrs = fields(inst.__class__) |
| | rv = [] |
| | retain = retain_collection_types |
| | for a in attrs: |
| | v = getattr(inst, a.name) |
| | if filter is not None and not filter(a, v): |
| | continue |
| | if recurse is True: |
| | if has(v.__class__): |
| | rv.append( |
| | astuple( |
| | v, |
| | recurse=True, |
| | filter=filter, |
| | tuple_factory=tuple_factory, |
| | retain_collection_types=retain, |
| | ) |
| | ) |
| | elif isinstance(v, (tuple, list, set, frozenset)): |
| | cf = v.__class__ if retain is True else list |
| | rv.append( |
| | cf( |
| | [ |
| | astuple( |
| | j, |
| | recurse=True, |
| | filter=filter, |
| | tuple_factory=tuple_factory, |
| | retain_collection_types=retain, |
| | ) |
| | if has(j.__class__) |
| | else j |
| | for j in v |
| | ] |
| | ) |
| | ) |
| | elif isinstance(v, dict): |
| | df = v.__class__ if retain is True else dict |
| | rv.append( |
| | df( |
| | ( |
| | astuple( |
| | kk, |
| | tuple_factory=tuple_factory, |
| | retain_collection_types=retain, |
| | ) |
| | if has(kk.__class__) |
| | else kk, |
| | astuple( |
| | vv, |
| | tuple_factory=tuple_factory, |
| | retain_collection_types=retain, |
| | ) |
| | if has(vv.__class__) |
| | else vv, |
| | ) |
| | for kk, vv in v.items() |
| | ) |
| | ) |
| | else: |
| | rv.append(v) |
| | else: |
| | rv.append(v) |
| |
|
| | return rv if tuple_factory is list else tuple_factory(rv) |
| |
|
| |
|
| | def has(cls): |
| | """ |
| | Check whether *cls* is a class with *attrs* attributes. |
| | |
| | :param type cls: Class to introspect. |
| | :raise TypeError: If *cls* is not a class. |
| | |
| | :rtype: bool |
| | """ |
| | attrs = getattr(cls, "__attrs_attrs__", None) |
| | if attrs is not None: |
| | return True |
| |
|
| | |
| | generic_base = get_generic_base(cls) |
| | if generic_base is not None: |
| | generic_attrs = getattr(generic_base, "__attrs_attrs__", None) |
| | if generic_attrs is not None: |
| | |
| | cls.__attrs_attrs__ = generic_attrs |
| | return generic_attrs is not None |
| | return False |
| |
|
| |
|
| | def assoc(inst, **changes): |
| | """ |
| | Copy *inst* and apply *changes*. |
| | |
| | This is different from `evolve` that applies the changes to the arguments |
| | that create the new instance. |
| | |
| | `evolve`'s behavior is preferable, but there are `edge cases`_ where it |
| | doesn't work. Therefore `assoc` is deprecated, but will not be removed. |
| | |
| | .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 |
| | |
| | :param inst: Instance of a class with *attrs* attributes. |
| | :param changes: Keyword changes in the new copy. |
| | |
| | :return: A copy of inst with *changes* incorporated. |
| | |
| | :raise attrs.exceptions.AttrsAttributeNotFoundError: If *attr_name* |
| | couldn't be found on *cls*. |
| | :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* |
| | class. |
| | |
| | .. deprecated:: 17.1.0 |
| | Use `attrs.evolve` instead if you can. |
| | This function will not be removed du to the slightly different approach |
| | compared to `attrs.evolve`. |
| | """ |
| | new = copy.copy(inst) |
| | attrs = fields(inst.__class__) |
| | for k, v in changes.items(): |
| | a = getattr(attrs, k, NOTHING) |
| | if a is NOTHING: |
| | raise AttrsAttributeNotFoundError( |
| | f"{k} is not an attrs attribute on {new.__class__}." |
| | ) |
| | _obj_setattr(new, k, v) |
| | return new |
| |
|
| |
|
| | def evolve(*args, **changes): |
| | """ |
| | Create a new instance, based on the first positional argument with |
| | *changes* applied. |
| | |
| | :param inst: Instance of a class with *attrs* attributes. |
| | :param changes: Keyword changes in the new copy. |
| | |
| | :return: A copy of inst with *changes* incorporated. |
| | |
| | :raise TypeError: If *attr_name* couldn't be found in the class |
| | ``__init__``. |
| | :raise 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. |
| | """ |
| | |
| | |
| | if args: |
| | try: |
| | (inst,) = args |
| | except ValueError: |
| | raise TypeError( |
| | f"evolve() takes 1 positional argument, but {len(args)} " |
| | "were given" |
| | ) from None |
| | else: |
| | try: |
| | inst = changes.pop("inst") |
| | except KeyError: |
| | raise TypeError( |
| | "evolve() missing 1 required positional argument: 'inst'" |
| | ) from None |
| |
|
| | import warnings |
| |
|
| | warnings.warn( |
| | "Passing the instance per keyword argument is deprecated and " |
| | "will stop working in, or after, April 2024.", |
| | DeprecationWarning, |
| | stacklevel=2, |
| | ) |
| |
|
| | cls = inst.__class__ |
| | attrs = fields(cls) |
| | for a in attrs: |
| | if not a.init: |
| | continue |
| | attr_name = a.name |
| | init_name = a.alias |
| | if init_name not in changes: |
| | changes[init_name] = getattr(inst, attr_name) |
| |
|
| | return cls(**changes) |
| |
|
| |
|
| | def resolve_types( |
| | cls, globalns=None, localns=None, attribs=None, include_extras=True |
| | ): |
| | """ |
| | Resolve any strings and forward annotations in type annotations. |
| | |
| | This is only required if you need concrete types in `Attribute`'s *type* |
| | field. In other words, you don't need to resolve your types if you only |
| | use them for static type checking. |
| | |
| | With no arguments, names will be looked up in the module in which the class |
| | was created. If this is not what you want, e.g. if the name only exists |
| | inside a method, you may pass *globalns* or *localns* to specify other |
| | dictionaries in which to look up these names. See the docs of |
| | `typing.get_type_hints` for more details. |
| | |
| | :param type cls: Class to resolve. |
| | :param Optional[dict] globalns: Dictionary containing global variables. |
| | :param Optional[dict] localns: Dictionary containing local variables. |
| | :param Optional[list] attribs: List of attribs for the given class. |
| | This is necessary when calling from inside a ``field_transformer`` |
| | since *cls* is not an *attrs* class yet. |
| | :param bool include_extras: Resolve more accurately, if possible. |
| | Pass ``include_extras`` to ``typing.get_hints``, if supported by the |
| | typing module. On supported Python versions (3.9+), this resolves the |
| | types more accurately. |
| | |
| | :raise TypeError: If *cls* is not a class. |
| | :raise attrs.exceptions.NotAnAttrsClassError: If *cls* is not an *attrs* |
| | class and you didn't pass any attribs. |
| | :raise NameError: If types cannot be resolved because of missing variables. |
| | |
| | :returns: *cls* so you can use this function also as a class decorator. |
| | Please note that you have to apply it **after** `attrs.define`. That |
| | means the decorator has to come in the line **before** `attrs.define`. |
| | |
| | .. versionadded:: 20.1.0 |
| | .. versionadded:: 21.1.0 *attribs* |
| | .. versionadded:: 23.1.0 *include_extras* |
| | |
| | """ |
| | |
| | |
| | if getattr(cls, "__attrs_types_resolved__", None) != cls: |
| | import typing |
| |
|
| | kwargs = {"globalns": globalns, "localns": localns} |
| |
|
| | if PY_3_9_PLUS: |
| | kwargs["include_extras"] = include_extras |
| |
|
| | hints = typing.get_type_hints(cls, **kwargs) |
| | for field in fields(cls) if attribs is None else attribs: |
| | if field.name in hints: |
| | |
| | _obj_setattr(field, "type", hints[field.name]) |
| | |
| | |
| | cls.__attrs_types_resolved__ = cls |
| |
|
| | |
| | return cls |
| |
|