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
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Our backwards-compatibility policy can be found [here](https://github.com/python

## NEXT

- Fix converter type overrides being ignored for dataclass fields with stringified annotations.
- Dict unstructuring hook factories now inherit the converter's `omit_if_default` setting when `_cattrs_omit_if_default` is omitted. Explicit `True` or `False` values and per-attribute overrides retain their precedence.
([#238](https://github.com/python-attrs/cattrs/issues/238))
- Support the [`frozendict`](https://docs.python.org/3.15/builtins/stdtypes.html#frozendict) built-in on Python 3.15+.
Expand Down
12 changes: 3 additions & 9 deletions src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
Sequence,
Set,
TypeAlias,
adapted_fields,
fields,
frozendict,
get_final_base,
Expand All @@ -44,7 +45,6 @@
is_counter,
is_deque,
is_frozenset,
is_generic,
is_generic_attrs,
is_hetero_tuple,
is_literal,
Expand Down Expand Up @@ -1321,10 +1321,7 @@ def gen_unstructure_attrs_fromdict(
self, cl: type[T]
) -> Callable[[T], dict[str, Any]]:
origin = get_origin(cl)
attribs = fields(origin or cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved.
resolve_types(origin or cl)
attribs = adapted_fields(origin or cl)
attrib_overrides = {
a.name: self.type_overrides[a.type]
for a in attribs
Expand Down Expand Up @@ -1367,10 +1364,7 @@ def gen_structure_attrs_fromdict(
self, cl: type[T]
) -> Callable[[Mapping[str, Any], Any], T]:
origin = get_origin(cl)
attribs = fields(origin or cl if is_generic(cl) else cl)
if attrs_has(cl) and any(isinstance(a.type, str) for a in attribs):
# PEP 563 annotations - need to be resolved.
resolve_types(origin or cl)
attribs = adapted_fields(origin or cl)
attrib_overrides = {
a.name: self.type_overrides[a.type]
for a in attribs
Expand Down
26 changes: 26 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test both structuring and unstructuring."""

from collections import deque
from dataclasses import make_dataclass
from typing import (
Annotated,
Any,
Expand Down Expand Up @@ -431,6 +432,31 @@ def test_type_overrides(cl_and_vals):
assert attr.name not in unstructured


@pytest.mark.parametrize("annotation", [int, "int"])
def test_type_overrides_dataclass_omit_default(annotation):
"""Type overrides use resolved dataclass field types."""
cl = make_dataclass("Settings", [("retries", annotation, 3), ("label", str, "app")])
converter = Converter(type_overrides={int: override(omit_if_default=True)})

assert converter.unstructure(cl()) == {"label": "app"}
assert converter.unstructure(cl(5)) == {"retries": 5, "label": "app"}


@pytest.mark.parametrize("annotation", [int, "int"])
@pytest.mark.parametrize("detailed_validation", [True, False])
def test_type_overrides_dataclass_rename(annotation, detailed_validation):
"""Renamed dataclass fields work in both directions without prior unstructuring."""
cl = make_dataclass("Settings", [("retries", annotation), ("label", str)])
converter = Converter(
type_overrides={int: override(rename="attempts")},
detailed_validation=detailed_validation,
)

inst = converter.structure({"attempts": "5", "label": "app"}, cl)
assert inst == cl(5, "app")
assert converter.unstructure(inst) == {"attempts": 5, "label": "app"}


def test_calling_back():
"""
Calling unstructure_attrs_asdict from a hook should not override a manual hook.
Expand Down
Loading