|
- # SPDX-License-Identifier: MIT
-
-
- import copy
-
- from ._compat import PY_3_9_PLUS, get_generic_base
- from ._make import _OBJ_SETATTR, NOTHING, 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.
-
- Args:
- inst: Instance of an *attrs*-decorated class.
-
- recurse (bool): Recurse into classes that are also *attrs*-decorated.
-
- filter (~typing.Callable):
- 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.
-
- dict_factory (~typing.Callable):
- A callable to produce dictionaries from. For example, to produce
- ordered dictionaries instead of normal Python dictionaries, pass in
- ``collections.OrderedDict``.
-
- retain_collection_types (bool):
- Do not convert to `list` when encountering an attribute whose type
- is `tuple` or `set`. Only meaningful if *recurse* is `True`.
-
- value_serializer (typing.Callable | None):
- 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.
-
- Returns:
- Return type of *dict_factory*.
-
- Raises:
- 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
- items = [
- _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
- ]
- try:
- rv[a.name] = cf(items)
- except TypeError:
- if not issubclass(cf, tuple):
- raise
- # Workaround for TypeError: cf.__new__() missing 1 required
- # positional argument (which appears, for a namedturle)
- rv[a.name] = cf(*items)
- 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:
- # Attrs class.
- 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.
-
- Args:
- inst: Instance of an *attrs*-decorated class.
-
- recurse (bool):
- Recurse into classes that are also *attrs*-decorated.
-
- filter (~typing.Callable):
- 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.
-
- tuple_factory (~typing.Callable):
- A callable to produce tuples from. For example, to produce lists
- instead of tuples.
-
- retain_collection_types (bool):
- Do not convert to `list` or `dict` when encountering an attribute
- which type is `tuple`, `dict` or `set`. Only meaningful if
- *recurse* is `True`.
-
- Returns:
- Return type of *tuple_factory*
-
- Raises:
- attrs.exceptions.NotAnAttrsClassError:
- If *cls* is not an *attrs* class.
-
- .. versionadded:: 16.2.0
- """
- attrs = fields(inst.__class__)
- rv = []
- retain = retain_collection_types # Very long. :/
- 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
- items = [
- (
- astuple(
- j,
- recurse=True,
- filter=filter,
- tuple_factory=tuple_factory,
- retain_collection_types=retain,
- )
- if has(j.__class__)
- else j
- )
- for j in v
- ]
- try:
- rv.append(cf(items))
- except TypeError:
- if not issubclass(cf, tuple):
- raise
- # Workaround for TypeError: cf.__new__() missing 1 required
- # positional argument (which appears, for a namedturle)
- rv.append(cf(*items))
- 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.
-
- Args:
- cls (type): Class to introspect.
-
- Raises:
- TypeError: If *cls* is not a class.
-
- Returns:
- bool:
- """
- attrs = getattr(cls, "__attrs_attrs__", None)
- if attrs is not None:
- return True
-
- # No attrs, maybe it's a specialized generic (A[str])?
- 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:
- # Stick it on here for speed next time.
- 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
-
- Args:
- inst: Instance of a class with *attrs* attributes.
-
- changes: Keyword changes in the new copy.
-
- Returns:
- A copy of inst with *changes* incorporated.
-
- Raises:
- attrs.exceptions.AttrsAttributeNotFoundError:
- If *attr_name* couldn't be found on *cls*.
-
- 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`, though.
- """
- new = copy.copy(inst)
- attrs = fields(inst.__class__)
- for k, v in changes.items():
- a = getattr(attrs, k, NOTHING)
- if a is NOTHING:
- msg = f"{k} is not an attrs attribute on {new.__class__}."
- raise AttrsAttributeNotFoundError(msg)
- _OBJ_SETATTR(new, k, v)
- return new
-
-
- 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 :class:`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, for example, 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.
-
- Args:
- cls (type): Class to resolve.
-
- globalns (dict | None): Dictionary containing global variables.
-
- localns (dict | None): Dictionary containing local variables.
-
- attribs (list | None):
- 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.
-
- include_extras (bool):
- 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.
-
- Raises:
- TypeError: If *cls* is not a class.
-
- attrs.exceptions.NotAnAttrsClassError:
- If *cls* is not an *attrs* class and you didn't pass any attribs.
-
- 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*
- """
- # Since calling get_type_hints is expensive we cache whether we've
- # done it already.
- 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:
- # Since fields have been frozen we must work around it.
- _OBJ_SETATTR(field, "type", hints[field.name])
- # We store the class we resolved so that subclasses know they haven't
- # been resolved.
- cls.__attrs_types_resolved__ = cls
-
- # Return the class so you can use it as a decorator too.
- return cls
|