Skip to content

Commit 60feae8

Browse files
committed
Fix include_subclasses strategy with a generic parent and tagged_union (backport)
1 parent 727aa89 commit 60feae8

File tree

3 files changed

+56
-4
lines changed

3 files changed

+56
-4
lines changed

HISTORY.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ The third number is for emergencies when we need to start branches for older rel
1111

1212
Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).
1313

14+
## 25.3.0 (unreleased)
15+
16+
- {func} `cattrs.strategies.include_subclasses` now works with generic parent classes and the tagged union strategy.
17+
1418
## 25.2.0 (2025-08-31)
1519

1620
- **Potentially breaking**: Sequences are now structured into tuples.
@@ -41,8 +45,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
4145

4246
## 25.1.1 (2025-06-04)
4347

44-
- Fixed `AttributeError: no attribute '__parameters__'` while structuring attrs classes that inherit from parametrized generic aliases from `collections.abc`.
45-
([#654](https://github.com/python-attrs/cattrs/issues/654) [#655](https://github.com/python-attrs/cattrs/pull/655))
48+
- {func} `cattrs.strategies.include_subclasses` now properly working with generic parent classes.
49+
([#649](https://github.com/python-attrs/cattrs/pull/650))
4650

4751
## 25.1.0 (2025-05-31)
4852

src/cattrs/strategies/_subclasses.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ def _make_subclasses_tree(cl: type) -> list[type]:
2323

2424
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
2525
"""Whether the given class has subclasses from `given_subclasses`."""
26-
actual = set(cl.__subclasses__())
26+
cls_origin = typing.get_origin(cl) or cl
27+
actual = set(cls_origin.__subclasses__())
2728
given = set(given_subclasses)
2829
return bool(actual & given)
2930

@@ -231,7 +232,13 @@ def cls_is_cl(cls, _cl=cl):
231232
return cls is _cl
232233

233234
converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook)
234-
subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
235+
subclasses = tuple(
236+
[
237+
c
238+
for c in union_classes
239+
if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)
240+
]
241+
)
235242
if len(subclasses) > 1:
236243
u = Union[subclasses] # type: ignore
237244
union_strategy(u, converter)

tests/strategies/test_include_subclasses.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import functools
12
import typing
23
from copy import deepcopy
34
from functools import partial
5+
from typing import Any
46

57
import pytest
68
from attrs import define
@@ -432,3 +434,42 @@ class Child1G(GenericParent[str]):
432434
assert genconverter.structure({"p": 5, "c": 5}, GenericParent[str]) == Child1G(
433435
"5", "5"
434436
)
437+
438+
439+
def test_parents_with_generics_tagged_union(genconverter: Converter):
440+
"""Ensure proper handling of generic parents with configure_tagged_union, #682."""
441+
442+
@define
443+
class GenericParent(typing.Generic[T]):
444+
p: T
445+
446+
@define
447+
class Child1G(GenericParent[str]):
448+
c: str
449+
450+
@define
451+
class Child2G(GenericParent[int]):
452+
c: str
453+
454+
union_strategy = functools.partial(
455+
configure_tagged_union,
456+
tag_generator=lambda cl: (typing.get_origin(cl) or cl).__name__,
457+
)
458+
include_subclasses(GenericParent[Any], genconverter, union_strategy=union_strategy)
459+
460+
assert genconverter.unstructure(Child1G("5", "5")) == {
461+
"p": "5",
462+
"c": "5",
463+
"_type": "Child1G",
464+
}
465+
assert genconverter.unstructure(Child2G(1, "5")) == {
466+
"p": 1,
467+
"c": "5",
468+
"_type": "Child2G",
469+
}
470+
assert genconverter.structure(
471+
{"p": "5", "c": "5", "_type": "Child1G"}, GenericParent[Any]
472+
) == Child1G("5", "5")
473+
assert genconverter.structure(
474+
{"p": 1, "c": "5", "_type": "Child2G"}, GenericParent[Any]
475+
) == Child2G(1, "5")

0 commit comments

Comments
 (0)