| | |
| |
|
| |
|
| | import copy |
| |
|
| | from ._compat import get_generic_base |
| | from ._make import _OBJ_SETATTR, NOTHING, fields |
| | from .exceptions import AttrsAttributeNotFoundError |
| |
|
| |
|
| | _ATOMIC_TYPES = frozenset( |
| | { |
| | type(None), |
| | bool, |
| | int, |
| | float, |
| | str, |
| | complex, |
| | bytes, |
| | type(...), |
| | type, |
| | range, |
| | property, |
| | } |
| | ) |
| |
|
| |
|
| | 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: |
| | value_type = type(v) |
| | if value_type in _ATOMIC_TYPES: |
| | rv[a.name] = v |
| | elif has(value_type): |
| | rv[a.name] = asdict( |
| | v, |
| | recurse=True, |
| | filter=filter, |
| | dict_factory=dict_factory, |
| | retain_collection_types=retain_collection_types, |
| | value_serializer=value_serializer, |
| | ) |
| | elif issubclass(value_type, (tuple, list, set, frozenset)): |
| | cf = value_type 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 issubclass(value_type, 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. |
| | """ |
| | val_type = type(val) |
| | if val_type in _ATOMIC_TYPES: |
| | rv = val |
| | if value_serializer is not None: |
| | rv = value_serializer(None, None, rv) |
| | elif getattr(val_type, "__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 issubclass(val_type, (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 issubclass(val_type, 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 |
| | value_type = type(v) |
| | if recurse is True: |
| | if value_type in _ATOMIC_TYPES: |
| | rv.append(v) |
| | elif has(value_type): |
| | rv.append( |
| | astuple( |
| | v, |
| | recurse=True, |
| | filter=filter, |
| | tuple_factory=tuple_factory, |
| | retain_collection_types=retain, |
| | ) |
| | ) |
| | elif issubclass(value_type, (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 issubclass(value_type, dict): |
| | df = value_type 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 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, |
| | "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 |
| |
|