Skip to content

Commit 88af007

Browse files
authored
Enable Python 3.14 (#653)
* Enable Python 3.14 * Bump orjson * Bump typing_extensions * Fix `is_generic` * Fix test_structuring_unsupported * Skip orjson on 3.14, stop skipping msgspec * Fix more tests * Conditionally import `_UnionGenericAlias` * More work on slotted dataclasses * Fix rebase * Update to attrs 25.4 * Tweak changelog * Reenable orjson
1 parent 5a1bb1a commit 88af007

File tree

16 files changed

+221
-124
lines changed

16 files changed

+221
-124
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
strategy:
1818
matrix:
19-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.10"]
19+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
2020
fail-fast: false
2121

2222
steps:

HISTORY.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
1717
This allows hashability, better immutability and is more consistent with the [`collections.abc.Set`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Set) type.
1818
See [Migrations](https://catt.rs/en/latest/migrations.html#abstract-sets-structuring-into-frozensets) for steps to restore legacy behavior.
1919
([#](https://github.com/python-attrs/cattrs/pull/))
20+
- Python 3.14 is now supported and part of the test matrix.
21+
([#653](https://github.com/python-attrs/cattrs/pull/653))
2022
- Fix unstructuring NewTypes with the {class}`BaseConverter`.
2123
([#684](https://github.com/python-attrs/cattrs/pull/684))
2224
- Make some Hypothesis tests more robust.
2325
([#684](https://github.com/python-attrs/cattrs/pull/684))
24-
- {func} `cattrs.strategies.include_subclasses` now works with generic parent classes and the tagged union strategy.
26+
- {func}`cattrs.strategies.include_subclasses` now works with generic parent classes and the tagged union strategy.
2527
([#683](https://github.com/python-attrs/cattrs/pull/683))
2628

2729
## 25.2.0 (2025-08-31)

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 { "" } }}

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ authors = [
4040
{name = "Tin Tvrtkovic", email = "tinchester@gmail.com"},
4141
]
4242
dependencies = [
43-
"attrs>=24.3.0",
44-
"typing-extensions>=4.12.2",
43+
"attrs>=25.4.0",
44+
"typing-extensions>=4.14.0",
4545
"exceptiongroup>=1.1.1; python_version < '3.11'",
4646
]
4747
requires-python = ">=3.9"
@@ -57,6 +57,7 @@ classifiers = [
5757
"Programming Language :: Python :: 3.11",
5858
"Programming Language :: Python :: 3.12",
5959
"Programming Language :: Python :: 3.13",
60+
"Programming Language :: Python :: 3.14",
6061
"Programming Language :: Python :: Implementation :: CPython",
6162
"Programming Language :: Python :: Implementation :: PyPy",
6263
"Typing :: Typed",
@@ -75,7 +76,7 @@ ujson = [
7576
"ujson>=5.10.0",
7677
]
7778
orjson = [
78-
"orjson>=3.10.7; implementation_name == \"cpython\"",
79+
"orjson>=3.11.3; implementation_name == \"cpython\"",
7980
]
8081
msgpack = [
8182
"msgpack>=1.0.5",

src/cattrs/_compat.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
_AnnotatedAlias,
3030
_GenericAlias,
3131
_SpecialGenericAlias,
32-
_UnionGenericAlias,
3332
get_args,
3433
get_origin,
3534
get_type_hints,
@@ -256,7 +255,22 @@ def is_tuple(type):
256255
)
257256

258257

259-
if sys.version_info >= (3, 10):
258+
if sys.version_info >= (3, 14):
259+
260+
def is_union_type(obj):
261+
from types import UnionType # noqa: PLC0415
262+
263+
return obj is Union or isinstance(obj, UnionType)
264+
265+
def get_newtype_base(typ: Any) -> Optional[type]:
266+
if typ is NewType or isinstance(typ, NewType):
267+
return typ.__supertype__
268+
return None
269+
270+
from typing import NotRequired, Required
271+
272+
elif sys.version_info >= (3, 10):
273+
from typing import _UnionGenericAlias
260274

261275
def is_union_type(obj):
262276
from types import UnionType # noqa: PLC0415
@@ -279,6 +293,8 @@ def get_newtype_base(typ: Any) -> Optional[type]:
279293

280294
else:
281295
# 3.9
296+
from typing import _UnionGenericAlias
297+
282298
from typing_extensions import NotRequired, Required
283299

284300
def is_union_type(obj):
@@ -411,8 +427,10 @@ def is_generic(type) -> bool:
411427
"""Whether `type` is a generic type."""
412428
# Inheriting from protocol will inject `Generic` into the MRO
413429
# without `__orig_bases__`.
414-
return isinstance(type, (_GenericAlias, GenericAlias)) or (
415-
is_subclass(type, Generic) and hasattr(type, "__orig_bases__")
430+
return (
431+
isinstance(type, (_GenericAlias, GenericAlias))
432+
or (is_subclass(type, Generic) and hasattr(type, "__orig_bases__"))
433+
or type.__class__ is Union # On 3.14, unions are no longer typing._GenericAlias
416434
)
417435

418436

src/cattrs/_generics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from collections.abc import Mapping
2-
from typing import Any
2+
from typing import Any, get_args
33

44
from attrs import NOTHING
55
from typing_extensions import Self
66

7-
from ._compat import copy_with, get_args, is_annotated, is_generic
7+
from ._compat import copy_with, is_annotated, is_generic
88

99

1010
def deep_copy_with(t, mapping: Mapping[str, Any], self_is=NOTHING):

src/cattrs/disambiguators.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from dataclasses import MISSING
88
from functools import reduce
99
from operator import or_
10-
from typing import TYPE_CHECKING, Any, Callable, Literal, Union
10+
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, get_origin
1111

1212
from attrs import NOTHING, Attribute, AttrsInstance
1313

@@ -16,7 +16,6 @@
1616
adapted_fields,
1717
fields_dict,
1818
get_args,
19-
get_origin,
2019
has,
2120
is_literal,
2221
is_union_type,

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))
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 and attrs classes.
15+
"""
16+
return [
17+
cl
18+
for cl in cls.__subclasses__()
19+
if (
20+
not (
21+
"__slots__" not in cl.__dict__
22+
and hasattr(cls, "__dataclass_params__")
23+
and cls.__dataclass_params__.slots
24+
)
25+
and not hasattr(cls, "__attrs_base_of_slotted__")
26+
)
27+
]

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)