|
|
|
|
|
|
|
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 |
|
|
|
|
|
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: |
|
|
|
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 |
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
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 |
|
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* |
|
""" |
|
|
|
|
|
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 |
|
|