Spaces:
Running
Running
# 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 | |