Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
([#707](https://github.com/python-attrs/cattrs/issues/707) [#708](https://github.com/python-attrs/cattrs/pull/708))
- Enum handling has been optimized by switching to hook factories, improving performance especially for plain enums.
([#705](https://github.com/python-attrs/cattrs/pull/705))
- Fix `include_subclasses` when used with `configure_tagged_union` and classes using diamond inheritance.
([#685](https://github.com/python-attrs/cattrs/issues/685) [#713](https://github.com/python-attrs/cattrs/pull/713))

## 25.3.0 (2025-10-07)

Expand Down
10 changes: 7 additions & 3 deletions src/cattrs/strategies/_subclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
def _make_subclasses_tree(cl: type) -> list[type]:
# get class origin for accessing subclasses (see #648 for more info)
cls_origin = typing.get_origin(cl) or cl
return [cl] + [
sscl for scl in subclasses(cls_origin) for sscl in _make_subclasses_tree(scl)
]

# Use a dict to deduplicate and keep insertion order.
seen = {cl: None}
for scl in subclasses(cls_origin):
for sscl in _make_subclasses_tree(scl):
seen[sscl] = None
return list(seen)


def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
Expand Down
27 changes: 27 additions & 0 deletions tests/strategies/test_include_subclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,30 @@ class ChildDC1(ParentDC):
include_subclasses(ParentDC, genconverter)

assert genconverter.structure({"a": 1, "b": "a"}, ParentDC) == ChildDC1(1, "a")


def test_diamond_inheritance(genconverter: Converter):
"""Diamond inheritance is handled correctly (issue #685)."""

@define
class Base:
pass

@define
class Mid1(Base):
pass

@define
class Mid2(Base):
pass

@define
class Sub(Mid1, Mid2):
pass

# This should not raise an error
include_subclasses(Base, genconverter, union_strategy=configure_tagged_union)

assert genconverter.structure({"_type": "Sub"}, Base) == Sub()
assert genconverter.structure({"_type": "Mid1"}, Base) == Mid1()
assert genconverter.structure({"_type": "Mid2"}, Base) == Mid2()
Loading