From 4f05fad65f239d17514de053d7f0786f81a498e6 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Mon, 22 Mar 2021 00:32:56 +0100 Subject: [PATCH] GenConverter: fix issue with unstructuring attrs classes with generic fields --- HISTORY.rst | 2 ++ src/cattr/_compat.py | 5 +++++ src/cattr/converters.py | 6 +++++- tests/test_generics.py | 28 +++++++++++++++++++++++++++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 72a88c85..74eeef0c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,8 @@ History 1.5.0 (UNRELEASED) ------------------ +* Fix an issue with ``GenConverter`` unstructuring ``attrs`` classes and dataclasses with generic fields. + (`#65 `_) 1.4.0 (2021-03-21) ------------------ diff --git a/src/cattr/_compat.py b/src/cattr/_compat.py index 61d63dbd..0913787d 100644 --- a/src/cattr/_compat.py +++ b/src/cattr/_compat.py @@ -48,6 +48,11 @@ def has(cls): ) +def has_with_generic(cls): + """Test whether the class if a normal or generic attrs or dataclass.""" + return has(cls) or has(get_origin(cls)) + + def fields(type): try: return type.__attrs_attrs__ diff --git a/src/cattr/converters.py b/src/cattr/converters.py index 8321ca8f..f943fc20 100644 --- a/src/cattr/converters.py +++ b/src/cattr/converters.py @@ -17,6 +17,7 @@ is_annotated, has, fields, + has_with_generic, ) from .disambiguators import create_uniq_field_dis_func from .dispatch import MultiStrategyDispatch @@ -490,7 +491,7 @@ def __init__( self._unstructure_func.register_func_list( [ ( - has, + has_with_generic, self.gen_unstructure_attrs_fromdict, True, ), @@ -541,6 +542,9 @@ def gen_structure_annotated(self, type): return h def gen_unstructure_attrs_fromdict(self, cl: Type[T]) -> Dict[str, Any]: + origin = get_origin(cl) + if origin is not None: + cl = origin attribs = fields(cl) if any(isinstance(a.type, str) for a in attribs): # PEP 563 annotations - need to be resolved. diff --git a/tests/test_generics.py b/tests/test_generics.py index c4764d28..d02b34c0 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -3,7 +3,7 @@ import pytest from attr import asdict, attrs -from cattr import Converter +from cattr import Converter, GenConverter T = TypeVar("T") T2 = TypeVar("T2") @@ -83,3 +83,29 @@ def test_raises_if_no_generic_params_supplied(converter): match="Unsupported type: ~T. Register a structure hook for it.", ): converter.structure(asdict(data), TClass) + + +def test_unstructure_generic_attrs(): + c = GenConverter() + + @attrs(auto_attribs=True) + class Inner(Generic[T]): + a: T + + @attrs(auto_attribs=True) + class Outer: + inner: Inner[int] + + initial = Outer(Inner(1)) + raw = c.unstructure(initial) + + assert raw == {"inner": {"a": 1}} + + new = c.structure(raw, Outer) + assert initial == new + + @attrs(auto_attribs=True) + class OuterStr: + inner: Inner[str] + + assert c.structure(raw, OuterStr) == OuterStr(Inner("1"))