Skip to content

Commit 48cb3d0

Browse files
authored
Fix unstructuring underspecified generics (#466)
* Fix unstructuring underspecified generics * Fix check * Fix ignore maybe?
1 parent 88d4758 commit 48cb3d0

5 files changed

Lines changed: 37 additions & 1 deletion

File tree

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Fix a regression when unstructuring dictionary values typed as `Any`.
66
([#453](https://github.com/python-attrs/cattrs/issues/453) [#462](https://github.com/python-attrs/cattrs/pull/462))
7+
- Fix a regression when unstructuring unspecialized generic classes.
8+
([#465](https://github.com/python-attrs/cattrs/issues/465))
79
- Optimize function source code caching.
810
([#445](https://github.com/python-attrs/cattrs/issues/445))
911
- Generate unique files only in case of linecache enabled.

src/cattrs/converters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ def gen_unstructure_optional(self, cl: Type[T]) -> Callable[[T], Any]:
956956
other = union_params[0] if union_params[1] is NoneType else union_params[1]
957957

958958
# TODO: Remove this special case when we make unstructuring Any consistent.
959-
if other is Any:
959+
if other is Any or isinstance(other, TypeVar):
960960
handler = self.unstructure
961961
else:
962962
handler = self._unstructure_func.dispatch(other)

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
from os import environ
23

34
import pytest
@@ -27,3 +28,7 @@ def converter_cls(request):
2728
settings.register_profile("fast", settings.get_profile("tests"), max_examples=10)
2829

2930
settings.load_profile("fast" if "FAST" in environ else "tests")
31+
32+
collect_ignore = []
33+
if sys.version_info < (3, 10):
34+
collect_ignore.append("test_generics_604.py")

tests/test_generics.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ def test_unstructure_generic_attrs(genconverter):
184184
class Inner(Generic[T]):
185185
a: T
186186

187+
inner = Inner(Inner(1))
188+
assert genconverter.unstructure(inner) == {"a": {"a": 1}}
189+
187190
@define
188191
class Outer:
189192
inner: Inner[int]
@@ -203,6 +206,16 @@ class OuterStr:
203206
assert genconverter.structure(raw, OuterStr) == OuterStr(Inner("1"))
204207

205208

209+
def test_unstructure_optional(genconverter):
210+
"""Generics with optional fields work."""
211+
212+
@define
213+
class C(Generic[T]):
214+
a: Union[T, None]
215+
216+
assert genconverter.unstructure(C(C(1))) == {"a": {"a": 1}}
217+
218+
206219
def test_unstructure_deeply_nested_generics(genconverter):
207220
@define
208221
class Inner:

tests/test_generics_604.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Tests for generics under PEP 604 (unions as pipes)."""
2+
from typing import Generic, TypeVar
3+
4+
from attrs import define
5+
6+
T = TypeVar("T")
7+
8+
9+
def test_unstructure_optional(genconverter):
10+
"""Generics with optional fields work."""
11+
12+
@define
13+
class C(Generic[T]):
14+
a: T | None
15+
16+
assert genconverter.unstructure(C(C(1))) == {"a": {"a": 1}}

0 commit comments

Comments
 (0)