diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 16e74ed8..2559bca1 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -963,7 +963,6 @@ def _get_dis_func( # logic. union_types = tuple(e for e in union_types if e is not NoneType) - # TODO: technically both disambiguators could support TypedDicts too if not all(has(get_origin(e) or e) for e in union_types): raise StructureHandlerNotFoundError( "Only unions of attrs classes and dataclasses supported " diff --git a/src/cattrs/disambiguators.py b/src/cattrs/disambiguators.py index 83e8c3f1..80288024 100644 --- a/src/cattrs/disambiguators.py +++ b/src/cattrs/disambiguators.py @@ -30,7 +30,7 @@ def is_supported_union(typ: Any) -> bool: - """Whether the type is a union of attrs classes.""" + """Whether the type is a union of attrs classes or dataclasses.""" return is_union_type(typ) and all( e is NoneType or has(get_origin(e) or e) for e in typ.__args__ ) diff --git a/src/cattrs/strategies/_subclasses.py b/src/cattrs/strategies/_subclasses.py index 06a92afa..47f3e7de 100644 --- a/src/cattrs/strategies/_subclasses.py +++ b/src/cattrs/strategies/_subclasses.py @@ -84,7 +84,7 @@ def include_subclasses( def _include_subclasses_without_union_strategy( cl, converter: BaseConverter, - parent_subclass_tree: tuple[type], + parent_subclass_tree: tuple[type, ...], overrides: dict[str, AttributeOverride] | None, ): # The iteration approach is required if subclasses are more than one level deep: diff --git a/tests/strategies/test_include_subclasses.py b/tests/strategies/test_include_subclasses.py index 7b6b9861..02746305 100644 --- a/tests/strategies/test_include_subclasses.py +++ b/tests/strategies/test_include_subclasses.py @@ -1,13 +1,12 @@ import typing from copy import deepcopy from functools import partial -from typing import List, Tuple import pytest from attrs import define from cattrs import Converter, override -from cattrs.errors import ClassValidationError +from cattrs.errors import ClassValidationError, StructureHandlerNotFoundError from cattrs.strategies import configure_tagged_union, include_subclasses @@ -148,7 +147,7 @@ def conv_w_subclasses(request): "struct_unstruct", IDS_TO_STRUCT_UNSTRUCT.values(), ids=IDS_TO_STRUCT_UNSTRUCT ) def test_structuring_with_inheritance( - conv_w_subclasses: Tuple[Converter, bool], struct_unstruct + conv_w_subclasses: tuple[Converter, bool], struct_unstruct ) -> None: structured, unstructured = struct_unstruct @@ -219,7 +218,7 @@ def test_circular_reference(conv_w_subclasses): "struct_unstruct", IDS_TO_STRUCT_UNSTRUCT.values(), ids=IDS_TO_STRUCT_UNSTRUCT ) def test_unstructuring_with_inheritance( - conv_w_subclasses: Tuple[Converter, bool], struct_unstruct + conv_w_subclasses: tuple[Converter, bool], struct_unstruct ): structured, unstructured = struct_unstruct converter, included_subclasses_param = conv_w_subclasses @@ -389,5 +388,27 @@ class Derived(A): "_type": "Derived", } ], - List[A], + list[A], ) == [Derived(9, Derived(99, A(999)))] + + +def test_unsupported_class(genconverter: Converter): + """Non-attrs/dataclass classes raise proper errors.""" + + class NewParent: + """Not an attrs class.""" + + a: int + + @define + class NewChild(NewParent): + pass + + @define + class NewChild2(NewParent): + pass + + genconverter.register_structure_hook(NewParent, lambda v, _: NewParent(v)) + + with pytest.raises(StructureHandlerNotFoundError): + include_subclasses(NewParent, genconverter)