Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ The third number is for emergencies when we need to start branches for older rel

Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).

## 25.4.0 (UNRELEASED)
## NEXT (UNRELEASED)

- Add the {mod}`tomllib <cattrs.preconf.tomllib>` preconf converter.
See [here](https://catt.rs/en/latest/preconf.html#tomllib) for details.
([#716](https://github.com/python-attrs/cattrs/pull/716))
- Customizing un/structuring of _attrs_ classes, dataclasses, TypedDicts and dict NamedTuples is now possible by using `Annotated[T, override()]` on fields.
See [here](https://catt.rs/en/stable/customizing.html#using-typing-annotated-t-override) for more details.
([#717](https://github.com/python-attrs/cattrs/pull/717))
- Fix structuring of nested generic classes with stringified annotations.
([#688](https://github.com/python-attrs/cattrs/pull/688))
- Python 3.9 is no longer supported, as it is end-of-life. Use previous versions on this Python version.
Expand Down
31 changes: 31 additions & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,37 @@ ClassWithInitFalse(number=2)

```

## Using `typing.Annotated[T, override(...)]`

The un/structuring process for _attrs_ classes, dataclasses, TypedDicts and dict NamedTuples can be customized by annotating the fields using `typing.Annotated[T, override()]`.

```{doctest}
>>> from typing import Annotated

>>> @define
... class ExampleClass:
... klass: Annotated[int, cattrs.override(rename="class")]

>>> cattrs.unstructure(ExampleClass(1))
{'class': 1}
>>> cattrs.structure({'class': 1}, ExampleClass)
ExampleClass(klass=1)
```

These customizations are automatically recognized by every {class}`Converter <cattrs.Converter>`.
They can still be overriden explicitly, see [](#custom-un-structuring-hooks).

```{attention}
One of the fundamental [design decisions](why.md#design-decisions) of _cattrs_ is that serialization rules should be separate from the models themselves;
by using this feature you're going against the spirit of this design decision.

However, software is written in many different context and with different constraints; and practicality (sometimes) beats purity.
```

```{versionadded} NEXT

```

## Customizing Collections

```{currentmodule} cattrs.cols
Expand Down
10 changes: 8 additions & 2 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,11 @@ Tuples can be structured into classes using {meth}`structure_attrs_fromtuple() <
A(a='string', b=2)
```

Loading from tuples can be made the default by creating a new {class}`Converter <cattrs.Converter>` with `unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE`.
Loading from tuples can be made the default by creating a new {class}`Converter <cattrs.Converter>` with `unstruct_strat=cattrs.UnstructureStrategy.AS_TUPLE`.

```{doctest}

>>> converter = cattrs.Converter(unstruct_strat=cattr.UnstructureStrategy.AS_TUPLE)
>>> converter = cattrs.Converter(unstruct_strat=cattrs.UnstructureStrategy.AS_TUPLE)
>>> @define
... class A:
... a: str
Expand Down Expand Up @@ -620,6 +620,12 @@ The {mod}`cattrs.cols` module contains hook factories for un/structuring named t

[PEP 593](https://www.python.org/dev/peps/pep-0593/) annotations (`typing.Annotated[type, ...]`) are supported and are handled using the first type present in the annotated type.

Additionally, `typing.Annotated` types containing `cattrs.override()` are recognized and used by the _attrs_, dataclass, TypedDict and dict NamedTuple hook factories.

```{versionchanged} NEXT
`Annotated[T, override()]` is now used by the _attrs_, dataclass, TypedDict and dict NamedTuple hook factories.
```

```{versionadded} 1.4.0

```
Expand Down
13 changes: 3 additions & 10 deletions src/cattrs/cols.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,15 @@
from collections import defaultdict
from collections.abc import Callable, Iterable
from functools import partial
from typing import (
TYPE_CHECKING,
Any,
DefaultDict,
Literal,
NamedTuple,
TypeVar,
get_type_hints,
)
from typing import TYPE_CHECKING, Any, DefaultDict, Literal, NamedTuple, TypeVar

from attrs import NOTHING, Attribute, NothingType

from ._compat import (
ANIES,
AbcSet,
get_args,
get_full_type_hints,
get_origin,
is_bare,
is_frozenset,
Expand Down Expand Up @@ -246,7 +239,7 @@ def _namedtuple_to_attrs(cl: type[tuple]) -> list[Attribute]:
type=a,
alias=name,
)
for name, a in get_type_hints(cl).items()
for name, a in get_full_type_hints(cl).items()
]


Expand Down
66 changes: 50 additions & 16 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from ._consts import AttributeOverride, already_generating, neutral
from ._generics import generate_mapping
from ._lc import generate_unique_filename
from ._shared import find_structure_handler
from ._shared import _annotated_override_or_default, find_structure_handler

if TYPE_CHECKING:
from ..converters import BaseConverter
Expand Down Expand Up @@ -95,10 +95,13 @@ def make_dict_unstructure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.

.. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
.. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: NEXT
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
.. versionchanged:: NEXT
When `_cattrs_omit_if_default` is true and the attribute has an attrs converter
specified, the converter is applied to the default value before checking if it
Expand All @@ -117,7 +120,13 @@ def make_dict_unstructure_fn_from_attrs(

for a in attrs:
attr_name = a.name
override = kwargs.get(attr_name, neutral)
if attr_name in kwargs:
override = kwargs[attr_name]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[attr_name] = override

if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
Expand Down Expand Up @@ -264,11 +273,14 @@ def make_dict_unstructure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.

.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 25.2.0
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: NEXT
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""
origin = get_origin(cl)
attrs = adapted_fields(origin or cl) # type: ignore
Expand Down Expand Up @@ -349,10 +361,13 @@ def make_dict_structure_fn_from_attrs(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.

.. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
.. versionadded:: 24.1.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: NEXT
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""

cl_name = cl.__name__
Expand Down Expand Up @@ -408,7 +423,13 @@ def make_dict_structure_fn_from_attrs(
internal_arg_parts["__c_avn"] = AttributeValidationNote
for a in attrs:
an = a.name
override = kwargs.get(an, neutral)
if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override

if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
Expand Down Expand Up @@ -539,14 +560,24 @@ def make_dict_structure_fn_from_attrs(
# The first loop deals with required args.
for a in attrs:
an = a.name
override = kwargs.get(an, neutral)

if an in kwargs:
override = kwargs[an]
else:
override = _annotated_override_or_default(a.type, neutral)
if override != neutral:
kwargs[an] = override

if override.omit:
continue
if override.omit is None and not a.init and not _cattrs_include_init_false:
continue

if a.default is not NOTHING:
non_required.append(a)
# The next loop will handle it.
continue

t = a.type
if isinstance(t, TypeVar):
t = typevar_map.get(t.__name__, t)
Expand Down Expand Up @@ -753,17 +784,20 @@ def make_dict_structure_fn(
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
will be included.

.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0
.. versionadded:: 23.2.0 *_cattrs_use_alias*
.. versionadded:: 23.2.0 *_cattrs_include_init_false*
.. versionchanged:: 23.2.0
The `_cattrs_forbid_extra_keys` and `_cattrs_detailed_validation` parameters
take their values from the given converter by default.
.. versionchanged:: 24.1.0
.. versionchanged:: 24.1.0
The `_cattrs_prefer_attrib_converters` parameter takes its value from the given
converter by default.
.. versionchanged:: 25.2.0
.. versionchanged:: 25.2.0
The `_cattrs_use_alias` parameter takes its value from the given converter
by default.
.. versionchanged:: NEXT
`typing.Annotated[T, override()]` is now recognized and can be used to customize
unstructuring.
"""

mapping = {}
Expand Down
18 changes: 17 additions & 1 deletion src/cattrs/gen/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,31 @@

from attrs import NOTHING, Attribute, Factory

from .._compat import is_bare_final
from .._compat import get_args, is_annotated, is_bare_final
from ..dispatch import StructureHook
from ..errors import StructureHandlerNotFoundError
from ..fns import raise_error
from ._consts import AttributeOverride

if TYPE_CHECKING:
from ..converters import BaseConverter


def _annotated_override_or_default(
type: Any, default: AttributeOverride
) -> AttributeOverride:
"""
If the type is Annotated containing an AttributeOverride, return it.
Otherwise, return the default.
"""
if is_annotated(type):
for arg in get_args(type):
if isinstance(arg, AttributeOverride):
return arg

return default


def find_structure_handler(
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
) -> StructureHook | None:
Expand Down
Loading