Skip to content

include_subclasses with configure_tagged_union fails for generic classes #682

@danarmak

Description

@danarmak

This is the same as #648, except I'm using the tagged union strategy. test_parents_with_generics fails with this strategy:

>       cattrs.strategies.include_subclasses(GenericParent[str], converter, union_strategy=cattrs.strategies.configure_tagged_union)
[snip]
E       AttributeError: __subclasses__. Did you mean: '__subclasscheck__'?

The problem is that _include_subclasses_with_union_strategy -> _has_subclasses still calls cl.__subclasses__(), and typing.get_origin(cl) needs to be used here too.

Fixing _has_subclasses leads to a further error:

  File "test_cattrutil.py", line 111, in test_test
    cattrs.strategies.include_subclasses(GenericParent[str], converter, union_strategy=cattrs.strategies.configure_tagged_union)
  File "cattrs/strategies/_subclasses.py", line 84, in include_subclasses
    _include_subclasses_with_union_strategy(
  File "cattrs/strategies/_subclasses.py", line 234, in _include_subclasses_with_union_strategy
    subclasses = tuple([c for c in union_classes if issubclass(c, cl)])
                                                    ^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/typing.py", line 1225, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

Changing this to read subclasses = tuple([c for c in union_classes if issubclass(typing.get_origin(c) or c, typing.get_origin(cl) or cl)]) works. This extended test succeeds - note that there is no reason to specify Parent[str] anymore; we don't have to know what type arguments Parent can ever get, as long as every child class has no open type parameters left:

converter = cattrs.Converter()

@define
class GenericParent[T]:
    p: T

@define
class Child1G(GenericParent[str]):
    c: str

@define
class Child2G(GenericParent[int]):
    c: str

cattrs.strategies.include_subclasses(GenericParent[Any], converter, union_strategy=cattrs.strategies.configure_tagged_union)

assert converter.structure(converter.unstructure(Child1G('a', 'b')), GenericParent[Any]) == Child1G('a', 'b')
assert converter.structure(converter.unstructure(Child2G(1, 'b')), GenericParent[Any]) == Child2G(1, 'b')

I will submit this as a PR, but I'm not sure about the limitations of this code (intended or otherwise), there may well be reasons not to merge it as is.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions