523 lines
17 KiB
Python
523 lines
17 KiB
Python
|
# 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 evolve(*args, **changes):
|
||
|
"""
|
||
|
Create a new instance, based on the first positional argument with
|
||
|
*changes* applied.
|
||
|
|
||
|
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)
|
||
|
|
||
|
|
||
|
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
|