|
|
|
|
|
|
|
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 |
|
|