From a198b33771b6224e3a15158c8aae2012aab03a4f Mon Sep 17 00:00:00 2001 From: danarmak Date: Tue, 2 Sep 2025 21:06:09 +0300 Subject: [PATCH] Fix include_subclasses strategy with a generic parent and tagged_union (backport) --- HISTORY.md | 2 + src/cattrs/strategies/_subclasses.py | 11 +++++- tests/strategies/test_include_subclasses.py | 41 +++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index c9daa47b..676f1849 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ([#684](https://github.com/python-attrs/cattrs/pull/684)) - Make some Hypothesis tests more robust. ([#684](https://github.com/python-attrs/cattrs/pull/684)) +- {func} `cattrs.strategies.include_subclasses` now works with generic parent classes and the tagged union strategy. + ([#683](https://github.com/python-attrs/cattrs/pull/683)) ## 25.2.0 (2025-08-31) diff --git a/src/cattrs/strategies/_subclasses.py b/src/cattrs/strategies/_subclasses.py index 483a226e..55585c40 100644 --- a/src/cattrs/strategies/_subclasses.py +++ b/src/cattrs/strategies/_subclasses.py @@ -23,7 +23,8 @@ def _make_subclasses_tree(cl: type) -> list[type]: def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool: """Whether the given class has subclasses from `given_subclasses`.""" - actual = set(cl.__subclasses__()) + cls_origin = typing.get_origin(cl) or cl + actual = set(cls_origin.__subclasses__()) given = set(given_subclasses) return bool(actual & given) @@ -231,7 +232,13 @@ def cls_is_cl(cls, _cl=cl): return cls is _cl converter.register_unstructure_hook_func(cls_is_cl, unstruct_hook) - subclasses = tuple([c for c in union_classes if issubclass(c, cl)]) + subclasses = tuple( + [ + c + for c in union_classes + if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl) + ] + ) if len(subclasses) > 1: u = Union[subclasses] # type: ignore union_strategy(u, converter) diff --git a/tests/strategies/test_include_subclasses.py b/tests/strategies/test_include_subclasses.py index 42507c3c..bf3d60ad 100644 --- a/tests/strategies/test_include_subclasses.py +++ b/tests/strategies/test_include_subclasses.py @@ -1,6 +1,8 @@ +import functools import typing from copy import deepcopy from functools import partial +from typing import Any import pytest from attrs import define @@ -432,3 +434,42 @@ class Child1G(GenericParent[str]): assert genconverter.structure({"p": 5, "c": 5}, GenericParent[str]) == Child1G( "5", "5" ) + + +def test_parents_with_generics_tagged_union(genconverter: Converter): + """Ensure proper handling of generic parents with configure_tagged_union, #682.""" + + @define + class GenericParent(typing.Generic[T]): + p: T + + @define + class Child1G(GenericParent[str]): + c: str + + @define + class Child2G(GenericParent[int]): + c: str + + union_strategy = functools.partial( + configure_tagged_union, + tag_generator=lambda cl: (typing.get_origin(cl) or cl).__name__, + ) + include_subclasses(GenericParent[Any], genconverter, union_strategy=union_strategy) + + assert genconverter.unstructure(Child1G("5", "5")) == { + "p": "5", + "c": "5", + "_type": "Child1G", + } + assert genconverter.unstructure(Child2G(1, "5")) == { + "p": 1, + "c": "5", + "_type": "Child2G", + } + assert genconverter.structure( + {"p": "5", "c": "5", "_type": "Child1G"}, GenericParent[Any] + ) == Child1G("5", "5") + assert genconverter.structure( + {"p": 1, "c": "5", "_type": "Child2G"}, GenericParent[Any] + ) == Child2G(1, "5")