Skip to content

Commit 94ac927

Browse files
authored
Add support for Python 3.14 (#1714)
1 parent ef455b4 commit 94ac927

19 files changed

+114
-80
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ jobs:
5757

5858
- uses: codecov/test-results-action@v1
5959

60-
# See https://github.com/PyO3/pyo3/discussions/2781
61-
# tests intermittently segfault with pypy and cpython 3.7 when using `coverage run ...`, hence separate job
6260
test-python:
6361
name: test ${{ matrix.python-version }}
6462
strategy:
@@ -71,6 +69,8 @@ jobs:
7169
- '3.12'
7270
- '3.13'
7371
- '3.13t'
72+
- '3.14'
73+
- '3.14t'
7474
- 'pypy3.9'
7575
- 'pypy3.10'
7676

@@ -412,15 +412,15 @@ jobs:
412412
- os: linux
413413
manylinux: auto
414414
target: armv7
415-
interpreter: 3.9 3.10 3.11 3.12 3.13
415+
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
416416
- os: linux
417417
manylinux: auto
418418
target: ppc64le
419-
interpreter: 3.9 3.10 3.11 3.12 3.13
419+
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
420420
- os: linux
421421
manylinux: auto
422422
target: s390x
423-
interpreter: 3.9 3.10 3.11 3.12 3.13
423+
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
424424
- os: linux
425425
manylinux: auto
426426
target: x86_64
@@ -456,10 +456,10 @@ jobs:
456456
- os: windows
457457
target: i686
458458
python-architecture: x86
459-
interpreter: 3.9 3.10 3.11 3.12 3.13
459+
interpreter: 3.9 3.10 3.11 3.12 3.13 3.14
460460
- os: windows
461461
target: aarch64
462-
interpreter: 3.11 3.12 3.13
462+
interpreter: 3.11 3.12 3.13 3.14
463463

464464
exclude:
465465
# See above; disabled for now.
@@ -483,7 +483,7 @@ jobs:
483483
with:
484484
target: ${{ matrix.target }}
485485
manylinux: ${{ matrix.manylinux }}
486-
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 pypy3.9 pypy3.10 pypy3.11' }}
486+
args: --release --out dist --interpreter ${{ matrix.interpreter || '3.9 3.10 3.11 3.12 3.13 3.14 pypy3.9 pypy3.10 pypy3.11' }}
487487
rust-toolchain: stable
488488
docker-options: -e CI
489489

@@ -504,7 +504,7 @@ jobs:
504504
fail-fast: false
505505
matrix:
506506
os: [linux, windows, macos]
507-
interpreter: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.13t']
507+
interpreter: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.13t', '3.14', '3.14t']
508508
include:
509509
# standard runners with override for macos arm
510510
- os: linux

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ rust-version = "1.75"
2727
[dependencies]
2828
# TODO it would be very nice to remove the "py-clone" feature as it can panic,
2929
# but needs a bit of work to make sure it's not used in the codebase
30-
pyo3 = { version = "0.24", features = ["generate-import-lib", "num-bigint", "py-clone"] }
30+
pyo3 = { version = "0.25", features = ["generate-import-lib", "num-bigint", "py-clone"] }
3131
regex = "1.11.1"
3232
strum = { version = "0.26.3", features = ["derive"] }
3333
strum_macros = "0.26.4"
@@ -44,7 +44,7 @@ base64 = "0.22.1"
4444
num-bigint = "0.4.6"
4545
num-traits = "0.2.19"
4646
uuid = "1.16.0"
47-
jiter = { version = "0.9.1", features = ["python"] }
47+
jiter = { version = "0.10.0", features = ["python"] }
4848
hex = "0.4.3"
4949

5050
[lib]
@@ -72,12 +72,12 @@ debug = true
7272
strip = false
7373

7474
[dev-dependencies]
75-
pyo3 = { version = "0.24", features = ["auto-initialize"] }
75+
pyo3 = { version = "0.25", features = ["auto-initialize"] }
7676

7777
[build-dependencies]
7878
version_check = "0.9.5"
7979
# used where logic has to be version/distribution specific, e.g. pypy
80-
pyo3-build-config = { version = "0.24" }
80+
pyo3-build-config = { version = "0.25" }
8181

8282
[lints.clippy]
8383
dbg_macro = "warn"

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ classifiers = [
2424
'Programming Language :: Python :: 3.11',
2525
'Programming Language :: Python :: 3.12',
2626
'Programming Language :: Python :: 3.13',
27+
'Programming Language :: Python :: 3.14',
2728
'Programming Language :: Rust',
2829
'Framework :: Pydantic',
2930
'Intended Audience :: Developers',
@@ -34,7 +35,9 @@ classifiers = [
3435
'Operating System :: MacOS',
3536
'Typing :: Typed',
3637
]
37-
dependencies = ['typing-extensions>=4.6.0,!=4.7.0']
38+
dependencies = [
39+
'typing-extensions>=4.13.0',
40+
]
3841
dynamic = ['description', 'license', 'readme', 'version']
3942

4043
[project.urls]
@@ -65,10 +68,10 @@ testing = [
6568
'numpy; python_version < "3.13" and implementation_name == "cpython" and platform_machine == "x86_64"',
6669
'exceptiongroup; python_version < "3.11"',
6770
'tzdata',
68-
'typing_extensions',
71+
'typing-inspection>=0.4.1',
6972
]
7073
linting = [{ include-group = "dev" }, 'griffe', 'pyright', 'ruff', 'mypy']
71-
wasm = [{ include-group = "dev" }, 'typing_extensions', 'ruff']
74+
wasm = [{ include-group = "dev" }, 'ruff']
7275
codspeed = [
7376
# codspeed is only run on CI, with latest version of CPython
7477
'pytest-codspeed; python_version == "3.13" and implementation_name == "cpython"',

src/py_gc.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@ use std::sync::Arc;
22

33
use ahash::AHashMap;
44
use enum_dispatch::enum_dispatch;
5-
use pyo3::{AsPyPointer, Py, PyTraverseError, PyVisit};
5+
use pyo3::{Py, PyTraverseError, PyVisit};
66

77
/// Trait implemented by types which can be traversed by the Python GC.
88
#[enum_dispatch]
99
pub trait PyGcTraverse {
1010
fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError>;
1111
}
1212

13-
impl<T> PyGcTraverse for Py<T>
14-
where
15-
Py<T>: AsPyPointer,
16-
{
13+
impl<T> PyGcTraverse for Py<T> {
1714
fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> {
1815
visit.call(self)
1916
}

src/serializers/filter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ where
320320

321321
/// detect both ellipsis and `True` to be compatible with pydantic V1
322322
fn is_ellipsis_like(v: &Bound<'_, PyAny>) -> bool {
323-
v.is(&v.py().Ellipsis())
323+
v.is(v.py().Ellipsis())
324324
|| match v.downcast::<PyBool>() {
325325
Ok(b) => b.is_true(),
326326
Err(_) => false,

src/tools.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use pyo3::types::{PyDict, PyString};
88
use pyo3::{intern, FromPyObject};
99

1010
use crate::input::Int;
11-
use jiter::{cached_py_string, pystring_fast_new, StringCacheMode};
11+
use jiter::{cached_py_string, StringCacheMode};
1212

1313
pub trait SchemaDict<'py> {
1414
fn get_as<T>(&self, key: &Bound<'py, PyString>) -> PyResult<Option<T>>
@@ -148,11 +148,10 @@ pub fn extract_int(v: &Bound<'_, PyAny>) -> Option<Int> {
148148

149149
pub(crate) fn new_py_string<'py>(py: Python<'py>, s: &str, cache_str: StringCacheMode) -> Bound<'py, PyString> {
150150
// we could use `bytecount::num_chars(s.as_bytes()) == s.len()` as orjson does, but it doesn't appear to be faster
151-
let ascii_only = false;
152151
if matches!(cache_str, StringCacheMode::All) {
153-
cached_py_string(py, s, ascii_only)
152+
cached_py_string(py, s)
154153
} else {
155-
pystring_fast_new(py, s, ascii_only)
154+
PyString::new(py, s)
156155
}
157156
}
158157

src/validators/dataclass.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ impl Validator for DataclassArgsValidator {
401401
let data_dict = dict.copy()?;
402402
if let Err(err) = data_dict.del_item(field_name) {
403403
// KeyError is fine here as the field might not be in the dict
404-
if !err.get_type(py).is(&PyType::new::<PyKeyError>(py)) {
404+
if !err.get_type(py).is(PyType::new::<PyKeyError>(py)) {
405405
return Err(err.into());
406406
}
407407
}

src/validators/enum_.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ impl<T: EnumValidateValue> Validator for EnumValidator<T> {
138138
// https://github.com/python/cpython/blob/v3.12.2/Lib/enum.py#L1148
139139
if enum_value.is_instance(class)? {
140140
return Ok(enum_value.into());
141-
} else if !enum_value.is(&py.None()) {
141+
} else if !enum_value.is(py.None()) {
142142
let type_error = PyTypeError::new_err(format!(
143143
"error in {}._missing_: returned {} instead of None or a valid member",
144144
class

src/validators/model_fields.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ impl Validator for ModelFieldsValidator {
409409
let data_dict = dict.copy()?;
410410
if let Err(err) = data_dict.del_item(field_name) {
411411
// KeyError is fine here as the field might not be in the dict
412-
if !err.get_type(py).is(&PyType::new::<PyKeyError>(py)) {
412+
if !err.get_type(py).is(PyType::new::<PyKeyError>(py)) {
413413
return Err(err.into());
414414
}
415415
}

tests/emscripten_runner.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ await micropip.install([
106106
'tzdata',
107107
'file:${wheel_path}',
108108
'typing-extensions',
109+
'typing-inspection',
109110
])
110111
importlib.invalidate_caches()
111112

tests/test_garbage_collection.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import platform
2+
import sys
23
from collections.abc import Iterable
34
from typing import Any
45
from weakref import WeakValueDictionary
@@ -20,7 +21,7 @@
2021
)
2122

2223

23-
@pytest.mark.xfail(is_free_threaded, reason='GC leaks on free-threaded')
24+
@pytest.mark.xfail(is_free_threaded and sys.version_info < (3, 14), reason='GC leaks on free-threaded (<3.14)')
2425
@pytest.mark.xfail(
2526
condition=platform.python_implementation() == 'PyPy', reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
2627
)
@@ -48,7 +49,7 @@ class MyModel(BaseModel):
4849
assert_gc(lambda: len(cache) == 0)
4950

5051

51-
@pytest.mark.xfail(is_free_threaded, reason='GC leaks on free-threaded')
52+
@pytest.mark.xfail(is_free_threaded and sys.version_info < (3, 14), reason='GC leaks on free-threaded (<3.14)')
5253
@pytest.mark.xfail(
5354
condition=platform.python_implementation() == 'PyPy', reason='https://foss.heptapod.net/pypy/pypy/-/issues/3899'
5455
)

tests/test_misc.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import copy
22
import pickle
3-
import re
43

54
import pytest
6-
from typing_extensions import get_args
5+
from typing_extensions import ( # noqa: UP035 (https://github.com/astral-sh/ruff/pull/18476)
6+
get_args,
7+
get_origin,
8+
get_type_hints,
9+
)
10+
from typing_inspection import typing_objects
11+
from typing_inspection.introspection import UNKNOWN, AnnotationSource, inspect_annotation
712

813
from pydantic_core import CoreConfig, CoreSchema, CoreSchemaType, PydanticUndefined, core_schema
914
from pydantic_core._pydantic_core import (
@@ -159,13 +164,26 @@ class MyModel:
159164

160165

161166
def test_core_schema_type_literal():
162-
def get_type_value(schema):
163-
type_ = schema.__annotations__['type']
164-
m = re.search(r"Literal\['(.+?)']", type_.__forward_arg__)
165-
assert m, f'Unknown schema type: {type_}'
166-
return m.group(1)
167+
def get_type_value(schema_typeddict) -> str:
168+
annotation = get_type_hints(schema_typeddict, include_extras=True)['type']
169+
inspected_ann = inspect_annotation(annotation, annotation_source=AnnotationSource.TYPED_DICT)
170+
annotation = inspected_ann.type
171+
assert annotation is not UNKNOWN
172+
assert typing_objects.is_literal(get_origin(annotation)), (
173+
f"The 'type' key of core schemas must be a Literal form, got {get_origin(annotation)}"
174+
)
175+
args = get_args(annotation)
176+
assert len(args) == 1, (
177+
f"The 'type' key of core schemas must be a Literal form with a single element, got {len(args)} elements"
178+
)
179+
type_ = args[0]
180+
assert isinstance(type_, str), (
181+
f"The 'type' key of core schemas must be a Literal form with a single string element, got element of type {type(type_)}"
182+
)
183+
184+
return type_
167185

168-
schema_types = tuple(get_type_value(x) for x in CoreSchema.__args__)
186+
schema_types = (get_type_value(x) for x in CoreSchema.__args__)
169187
schema_types = tuple(dict.fromkeys(schema_types)) # remove duplicates while preserving order
170188
if get_args(CoreSchemaType) != schema_types:
171189
literal = ''.join(f'\n {e!r},' for e in schema_types)

0 commit comments

Comments
 (0)