Skip to content

Commit ae80674

Browse files
authored
Python 3.13 support (#543)
* Python 3.13 support * Update msgspec * Ignore msgspec on 3.13 * Update orjson * Bump typing_extensions * Fix typing.NoDefault for list unstructuring
1 parent e536258 commit ae80674

File tree

9 files changed

+134
-103
lines changed

9 files changed

+134
-103
lines changed

.github/workflows/main.yml

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

1515
strategy:
1616
matrix:
17-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"]
17+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.10"]
1818
fail-fast: false
1919

2020
steps:

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
1818
([#577](https://github.com/python-attrs/cattrs/pull/577))
1919
- Add a [Migrations](https://catt.rs/latest/migrations.html) page, with instructions on migrating changed behavior for each version.
2020
([#577](https://github.com/python-attrs/cattrs/pull/577))
21+
- Python 3.13 is now supported.
22+
([#543](https://github.com/python-attrs/cattrs/pull/543) [#547](https://github.com/python-attrs/cattrs/issues/547))
2123

2224
## 24.1.2 (2024-09-22)
2325

pdm.lock

Lines changed: 100 additions & 93 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ authors = [
4343
]
4444
dependencies = [
4545
"attrs>=23.1.0",
46-
"typing-extensions>=4.1.0, !=4.6.3; python_version < '3.11'",
46+
"typing-extensions>=4.12.2",
4747
"exceptiongroup>=1.1.1; python_version < '3.11'",
4848
]
4949
requires-python = ">=3.8"
@@ -59,6 +59,7 @@ classifiers = [
5959
"Programming Language :: Python :: 3.10",
6060
"Programming Language :: Python :: 3.11",
6161
"Programming Language :: Python :: 3.12",
62+
"Programming Language :: Python :: 3.13",
6263
"Programming Language :: Python :: Implementation :: CPython",
6364
"Programming Language :: Python :: Implementation :: PyPy",
6465
"Typing :: Typed",
@@ -77,7 +78,7 @@ ujson = [
7778
"ujson>=5.10.0",
7879
]
7980
orjson = [
80-
"orjson>=3.9.2; implementation_name == \"cpython\"",
81+
"orjson>=3.10.7; implementation_name == \"cpython\"",
8182
]
8283
msgpack = [
8384
"msgpack>=1.0.5",

src/cattrs/dispatch.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ class MultiStrategyDispatch(Generic[Hook]):
9191
MultiStrategyDispatch uses a combination of exact-match dispatch,
9292
singledispatch, and FunctionDispatch.
9393
94-
:param converter: A converter to be used for factories that require converters.
9594
:param fallback_factory: A hook factory to be called when a hook cannot be
9695
produced.
96+
:param converter: A converter to be used for factories that require converters.
9797
9898
.. versionchanged:: 23.2.0
9999
Fallbacks are now factories.
@@ -103,7 +103,6 @@ class MultiStrategyDispatch(Generic[Hook]):
103103
"""
104104

105105
_fallback_factory: HookFactory[Hook]
106-
_converter: BaseConverter
107106
_direct_dispatch: dict[TargetType, Hook]
108107
_function_dispatch: FunctionDispatch
109108
_single_dispatch: Any

src/cattrs/gen/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515

1616
from attrs import NOTHING, Attribute, Factory, resolve_types
17+
from typing_extensions import NoDefault
1718

1819
from .._compat import (
1920
ANIES,
@@ -1029,6 +1030,9 @@ def iterable_unstructure_factory(
10291030
"""A hook factory for unstructuring iterables.
10301031
10311032
:param unstructure_to: Force unstructuring to this type, if provided.
1033+
1034+
.. versionchanged:: 24.2.0
1035+
`typing.NoDefault` is now correctly handled as `Any`.
10321036
"""
10331037
handler = converter.unstructure
10341038

@@ -1039,6 +1043,8 @@ def iterable_unstructure_factory(
10391043
type_arg = cl.__args__[0]
10401044
if isinstance(type_arg, TypeVar):
10411045
type_arg = getattr(type_arg, "__default__", Any)
1046+
if type_arg is NoDefault:
1047+
type_arg = Any
10421048
handler = converter.get_unstructure_hook(type_arg, cache_result=False)
10431049
if handler == identity:
10441050
# Save ourselves the trouble of iterating over it all.

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ def converter_cls(request):
3737
collect_ignore_glob.append("*_695.py")
3838
if platform.python_implementation() == "PyPy":
3939
collect_ignore_glob.append("*_cpython.py")
40+
if sys.version_info >= (3, 13): # Remove when msgspec supports 3.13.
41+
collect_ignore_glob.append("*test_msgspec_cpython.py")

tests/test_preconf.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from json import dumps as json_dumps
55
from json import loads as json_loads
66
from platform import python_implementation
7-
from typing import Any, Dict, List, NamedTuple, NewType, Tuple, Union
7+
from typing import Any, Dict, Final, List, NamedTuple, NewType, Tuple, Union
88

99
import pytest
1010
from attrs import define
@@ -699,7 +699,10 @@ def test_cbor2_unions(union_and_val: tuple, detailed_validation: bool):
699699
assert converter.structure(val, type) == val
700700

701701

702-
@pytest.mark.skipif(python_implementation() == "PyPy", reason="no msgspec on PyPy")
702+
NO_MSGSPEC: Final = python_implementation() == "PyPy" or sys.version_info[:2] >= (3, 13)
703+
704+
705+
@pytest.mark.skipif(NO_MSGSPEC, reason="msgspec not available")
703706
@given(everythings(allow_inf=False))
704707
def test_msgspec_json_converter(everything: Everything):
705708
from cattrs.preconf.msgspec import make_converter as msgspec_make_converter
@@ -709,7 +712,7 @@ def test_msgspec_json_converter(everything: Everything):
709712
assert converter.loads(raw, Everything) == everything
710713

711714

712-
@pytest.mark.skipif(python_implementation() == "PyPy", reason="no msgspec on PyPy")
715+
@pytest.mark.skipif(NO_MSGSPEC, reason="msgspec not available")
713716
@given(everythings(allow_inf=False))
714717
def test_msgspec_json_unstruct_collection_overrides(everything: Everything):
715718
"""Ensure collection overrides work."""
@@ -724,7 +727,7 @@ def test_msgspec_json_unstruct_collection_overrides(everything: Everything):
724727
assert raw["a_frozenset"] == sorted(raw["a_frozenset"])
725728

726729

727-
@pytest.mark.skipif(python_implementation() == "PyPy", reason="no msgspec on PyPy")
730+
@pytest.mark.skipif(NO_MSGSPEC, reason="msgspec not available")
728731
@given(
729732
union_and_val=native_unions(
730733
include_datetimes=False,

tox.ini

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ python =
66
3.10: py310
77
3.11: py311, docs
88
3.12: py312, lint
9+
3.13: py313
910
pypy-3: pypy3
1011

12+
1113
[tox]
12-
envlist = pypy3, py38, py39, py310, py311, py312, lint, docs
14+
envlist = pypy3, py38, py39, py310, py311, py312, py313, lint, docs
1315
isolated_build = true
1416
skipsdist = true
1517

@@ -42,6 +44,15 @@ setenv =
4244
COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
4345
COVERAGE_CORE=sysmon
4446

47+
[testenv:py313]
48+
setenv =
49+
PDM_IGNORE_SAVED_PYTHON="1"
50+
COVERAGE_PROCESS_START={toxinidir}/pyproject.toml
51+
COVERAGE_CORE=sysmon
52+
commands_pre =
53+
pdm sync -G ujson,msgpack,pyyaml,tomlkit,cbor2,bson,orjson,test
54+
python -c 'import pathlib; pathlib.Path("{env_site_packages_dir}/cov.pth").write_text("import coverage; coverage.process_startup()")'
55+
4556
[testenv:pypy3]
4657
setenv =
4758
FAST = 1

0 commit comments

Comments
 (0)