From 97396c615d429c1593370d54b5247278755ff325 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 15 Oct 2025 00:45:47 -0500 Subject: [PATCH 1/2] Fix structuring of nested generics, add test --- src/cattrs/converters.py | 5 +++-- tests/test_generics_nested.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/test_generics_nested.py diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 08d7fc56..13f63ee1 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -1303,10 +1303,11 @@ def gen_structure_typeddict(self, cl: Any) -> Callable[[dict, Any], dict]: def gen_structure_attrs_fromdict( self, cl: type[T] ) -> Callable[[Mapping[str, Any], Any], T]: - attribs = fields(get_origin(cl) or cl if is_generic(cl) else cl) + 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(cl) + resolve_types(origin or cl) attrib_overrides = { a.name: self.type_overrides[a.type] for a in attribs diff --git a/tests/test_generics_nested.py b/tests/test_generics_nested.py new file mode 100644 index 00000000..a98cc8c1 --- /dev/null +++ b/tests/test_generics_nested.py @@ -0,0 +1,24 @@ +"""Tests un/structure of nested generics""" + +from __future__ import annotations + +from typing import Generic, TypeVar + +from attrs import define + +T = TypeVar('T') + + +def test_structure_nested_roundtrip(genconverter): + @define(auto_attribs=True) + class Inner: + value: int + + @define(auto_attribs=True) + class Container(Generic[T]): + data: T + + + raw = {'data': {'value': 42}} + structured = genconverter.structure(raw, Container[Inner]) + assert genconverter.unstructure(structured, Container[Inner]) == raw From 0d095603c208cf014bcc5334a9272474e863676d Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 15 Oct 2025 08:39:33 -0500 Subject: [PATCH 2/2] Black lint, changelog entry, better test docstring --- HISTORY.md | 5 +++++ tests/test_generics_nested.py | 7 +++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 4768967a..2eed1dce 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,11 @@ 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) + +- Fix structuring of nested generic classes with stringified annotations. + ([#688](https://github.com/python-attrs/cattrs/pull/688)) + ## 25.3.0 (2025-10-07) - **Potentially breaking**: [Abstract sets](https://docs.python.org/3/library/collections.abc.html#collections.abc.Set) are now structured into frozensets. diff --git a/tests/test_generics_nested.py b/tests/test_generics_nested.py index a98cc8c1..1e0da3a8 100644 --- a/tests/test_generics_nested.py +++ b/tests/test_generics_nested.py @@ -1,4 +1,4 @@ -"""Tests un/structure of nested generics""" +"""Tests un/structure of nested generic classes (stringified only)""" from __future__ import annotations @@ -6,7 +6,7 @@ from attrs import define -T = TypeVar('T') +T = TypeVar("T") def test_structure_nested_roundtrip(genconverter): @@ -18,7 +18,6 @@ class Inner: class Container(Generic[T]): data: T - - raw = {'data': {'value': 42}} + raw = {"data": {"value": 42}} structured = genconverter.structure(raw, Container[Inner]) assert genconverter.unstructure(structured, Container[Inner]) == raw