diff --git a/HISTORY.md b/HISTORY.md index 21e223a5..519cae9f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,8 @@ - Fix a regression when unstructuring dictionary values typed as `Any`. ([#453](https://github.com/python-attrs/cattrs/issues/453) [#462](https://github.com/python-attrs/cattrs/pull/462)) +- Fix a regression when unstructuring unspecialized generic classes. + ([#465](https://github.com/python-attrs/cattrs/issues/465)) - Optimize function source code caching. ([#445](https://github.com/python-attrs/cattrs/issues/445)) - Generate unique files only in case of linecache enabled. diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 5e9e2b9c..1fd1dfe8 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -956,7 +956,7 @@ def gen_unstructure_optional(self, cl: Type[T]) -> Callable[[T], Any]: other = union_params[0] if union_params[1] is NoneType else union_params[1] # TODO: Remove this special case when we make unstructuring Any consistent. - if other is Any: + if other is Any or isinstance(other, TypeVar): handler = self.unstructure else: handler = self._unstructure_func.dispatch(other) diff --git a/tests/conftest.py b/tests/conftest.py index 69d978ca..47306491 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import sys from os import environ import pytest @@ -27,3 +28,7 @@ def converter_cls(request): settings.register_profile("fast", settings.get_profile("tests"), max_examples=10) settings.load_profile("fast" if "FAST" in environ else "tests") + +collect_ignore = [] +if sys.version_info < (3, 10): + collect_ignore.append("test_generics_604.py") diff --git a/tests/test_generics.py b/tests/test_generics.py index 9d075187..2e07342b 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -184,6 +184,9 @@ def test_unstructure_generic_attrs(genconverter): class Inner(Generic[T]): a: T + inner = Inner(Inner(1)) + assert genconverter.unstructure(inner) == {"a": {"a": 1}} + @define class Outer: inner: Inner[int] @@ -203,6 +206,16 @@ class OuterStr: assert genconverter.structure(raw, OuterStr) == OuterStr(Inner("1")) +def test_unstructure_optional(genconverter): + """Generics with optional fields work.""" + + @define + class C(Generic[T]): + a: Union[T, None] + + assert genconverter.unstructure(C(C(1))) == {"a": {"a": 1}} + + def test_unstructure_deeply_nested_generics(genconverter): @define class Inner: diff --git a/tests/test_generics_604.py b/tests/test_generics_604.py new file mode 100644 index 00000000..a224f1af --- /dev/null +++ b/tests/test_generics_604.py @@ -0,0 +1,16 @@ +"""Tests for generics under PEP 604 (unions as pipes).""" +from typing import Generic, TypeVar + +from attrs import define + +T = TypeVar("T") + + +def test_unstructure_optional(genconverter): + """Generics with optional fields work.""" + + @define + class C(Generic[T]): + a: T | None + + assert genconverter.unstructure(C(C(1))) == {"a": {"a": 1}}