Skip to content

Commit ce2e28b

Browse files
committed
More work on slotted dataclasses
1 parent 3a80714 commit ce2e28b

File tree

5 files changed

+70
-6
lines changed

5 files changed

+70
-6
lines changed

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
3030
This allows hashability, better immutability and is more consistent with the [`collections.abc.Sequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence) type.
3131
See [Migrations](https://catt.rs/en/latest/migrations.html#sequences-structuring-into-tuples) for steps to restore legacy behavior.
3232
([#663](https://github.com/python-attrs/cattrs/pull/663))
33+
- Python 3.14 is now supported and part of the test matrix.
34+
([#653](https://github.com/python-attrs/cattrs/pull/653))
3335
- Add a `use_alias` parameter to {class}`cattrs.Converter`.
3436
{func}`cattrs.gen.make_dict_unstructure_fn_from_attrs`, {func}`cattrs.gen.make_dict_unstructure_fn`,
3537
{func}`cattrs.gen.make_dict_structure_fn_from_attrs`, {func}`cattrs.gen.make_dict_structure_fn`

Justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ lint:
1010
uv run -p python3.13 --group lint black --check src tests docs/conf.py
1111

1212
test *args="-x --ff -n auto tests":
13-
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test pytest {{args}}
13+
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test --group lint pytest {{args}}
1414

1515
testall:
1616
just python=python3.9 test
@@ -21,7 +21,7 @@ testall:
2121
just python=python3.13 test
2222

2323
cov *args="-x --ff -n auto tests":
24-
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test coverage run -m pytest {{args}}
24+
uv run {{ if python != '' { '-p ' + python } else { '' } }} --all-extras --group test --group lint coverage run -m pytest {{args}}
2525
{{ if covcleanup == "true" { "uv run coverage combine" } else { "" } }}
2626
{{ if covcleanup == "true" { "uv run coverage report" } else { "" } }}
2727
{{ if covcleanup == "true" { "@rm .coverage*" } else { "" } }}

src/cattrs/strategies/_subclasses.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,21 @@
99
from ..converters import BaseConverter
1010
from ..gen import AttributeOverride, make_dict_structure_fn, make_dict_unstructure_fn
1111
from ..gen._consts import already_generating
12+
from ..subclasses import subclasses
1213

1314

1415
def _make_subclasses_tree(cl: type) -> list[type]:
1516
# get class origin for accessing subclasses (see #648 for more info)
1617
cls_origin = typing.get_origin(cl) or cl
1718
return [cl] + [
18-
sscl
19-
for scl in cls_origin.__subclasses__()
20-
for sscl in _make_subclasses_tree(scl)
19+
sscl for scl in subclasses(cls_origin) for sscl in _make_subclasses_tree(scl)
2120
]
2221

2322

2423
def _has_subclasses(cl: type, given_subclasses: tuple[type, ...]) -> bool:
2524
"""Whether the given class has subclasses from `given_subclasses`."""
2625
cls_origin = typing.get_origin(cl) or cl
27-
actual = set(cls_origin.__subclasses__())
26+
actual = set(subclasses(cls_origin.__subclasses__()))
2827
given = set(given_subclasses)
2928
return bool(actual & given)
3029

@@ -69,6 +68,9 @@ def include_subclasses(
6968
.. versionchanged:: 24.1.0
7069
When overrides are not provided, hooks for individual classes are retrieved from
7170
the converter instead of generated with no overrides, using converter defaults.
71+
.. versionchanged:: 25.2.0
72+
Slotted dataclasses work on Python 3.14 via :func:`cattrs.subclasses.subclasses`,
73+
which filters out duplicate classes caused by slotting.
7274
"""
7375
# Due to https://github.com/python-attrs/attrs/issues/1047
7476
collect()

src/cattrs/subclasses.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import sys
2+
3+
if sys.version_info <= (3, 13):
4+
5+
def subclasses(cls: type) -> list[type]:
6+
"""A proxy for `cls.__subclasses__()` on older Pythons."""
7+
return cls.__subclasses__()
8+
9+
else:
10+
11+
def subclasses(cls: type) -> list[type]:
12+
"""A helper for getting subclasses of a class.
13+
14+
Filters out duplicate subclasses of slot dataclasses.
15+
"""
16+
return [
17+
cl
18+
for cl in cls.__subclasses__()
19+
if not (
20+
"__slots__" not in cl.__dict__
21+
and hasattr(cls, "__dataclass_params__")
22+
and cls.__dataclass_params__.slots
23+
)
24+
]

tests/strategies/test_include_subclasses.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import functools
22
import typing
33
from copy import deepcopy
4+
from dataclasses import dataclass
45
from functools import partial
56
from typing import Any
67

@@ -11,6 +12,8 @@
1112
from cattrs.errors import ClassValidationError, StructureHandlerNotFoundError
1213
from cattrs.strategies import configure_tagged_union, include_subclasses
1314

15+
from .._compat import is_py311_plus
16+
1417
T = typing.TypeVar("T")
1518

1619

@@ -473,3 +476,36 @@ class Child2G(GenericParent[int]):
473476
assert genconverter.structure(
474477
{"p": 1, "c": "5", "_type": "Child2G"}, GenericParent[Any]
475478
) == Child2G(1, "5")
479+
480+
481+
def test_dataclasses(genconverter: Converter):
482+
"""Dict dataclasses work."""
483+
484+
@dataclass
485+
class ParentDC:
486+
a: int
487+
488+
@dataclass
489+
class ChildDC1(ParentDC):
490+
b: str
491+
492+
include_subclasses(ParentDC, genconverter)
493+
494+
assert genconverter.structure({"a": 1, "b": "a"}, ParentDC) == ChildDC1(1, "a")
495+
496+
497+
@pytest.mark.skipif(not is_py311_plus, reason="slotted dataclasses supported on 3.11+")
498+
def test_dataclasses_slots(genconverter: Converter):
499+
"""Slotted dataclasses work."""
500+
501+
@dataclass(slots=True)
502+
class ParentDC:
503+
a: int
504+
505+
@dataclass(slots=True)
506+
class ChildDC1(ParentDC):
507+
b: str
508+
509+
include_subclasses(ParentDC, genconverter)
510+
511+
assert genconverter.structure({"a": 1, "b": "a"}, ParentDC) == ChildDC1(1, "a")

0 commit comments

Comments
 (0)