Skip to content

Commit 094343c

Browse files
fix: support Python 3.14 (#5646)
* ci: support Python 3.14 Signed-off-by: Henry Schreiner <[email protected]> * fix: Python 3.14 name change Signed-off-by: Henry Schreiner <[email protected]> * tests: fix expected output to handle Python 3.14 Signed-off-by: Henry Schreiner <[email protected]> * fix: tighten CLI and add color on 3.14+ Signed-off-by: Henry Schreiner <[email protected]> * tests: ignore failure on 3.14.0b1 Signed-off-by: Henry Schreiner <[email protected]> * fix: support Python 3.14.0b1 with interperters Signed-off-by: Henry Schreiner <[email protected]> * Update test_multiple_interpreters.py * Update test_multiple_interpreters.py * fix: new breakage for 3.14 fixed Signed-off-by: Henry Schreiner <[email protected]> * fix: handle empty annotations 3.14 Signed-off-by: Henry Schreiner <[email protected]> * fix: Python 3.14 may not create the annotations dict Signed-off-by: Henry Schreiner <[email protected]> * fix: use PyUnstable_IsImmortal Signed-off-by: Henry Schreiner <[email protected]> * fix: use sys._is_immortal Signed-off-by: Henry Schreiner <[email protected]> * tests: ignore large values for refcount too Signed-off-by: Henry Schreiner <[email protected]> * style: pre-commit fixes * ci: enable all free-threaded builds Signed-off-by: Henry Schreiner <[email protected]> * fix: patch for embed Signed-off-by: Henry Schreiner <[email protected]> * Revert "fix: patch for embed" This reverts commit c4226a0. * ci: drop new 3.xt additions Signed-off-by: Henry Schreiner <[email protected]> * fix: logic issue, also add some comments Signed-off-by: Henry Schreiner <[email protected]> * Update include/pybind11/pytypes.h --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 1107c09 commit 094343c

12 files changed

+60
-14
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
python:
3535
- '3.8'
3636
- '3.13'
37+
- '3.14'
3738
- 'pypy-3.10'
3839
- 'pypy-3.11'
3940
- 'graalpy-24.2'
@@ -108,6 +109,9 @@ jobs:
108109
# No NumPy for PyPy 3.10 ARM
109110
- runs-on: macos-14
110111
python: 'pypy-3.10'
112+
# Beta 1 broken for compiling on GHA (thinks it's free-threaded)
113+
- runs-on: windows-2022
114+
python: '3.14'
111115

112116

113117
name: "🐍 ${{ matrix.python }} • ${{ matrix.runs-on }} • x64 ${{ matrix.args }}"

include/pybind11/eval.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,12 @@ object eval_file(str fname, object global = globals(), object local = object())
133133

134134
int closeFile = 1;
135135
std::string fname_str = (std::string) fname;
136-
FILE *f = _Py_fopen_obj(fname.ptr(), "r");
136+
FILE *f =
137+
# if PY_VERSION_HEX >= 0x030E0000
138+
Py_fopen(fname.ptr(), "r");
139+
# else
140+
_Py_fopen_obj(fname.ptr(), "r");
141+
# endif
137142
if (!f) {
138143
PyErr_Clear();
139144
pybind11_fail("File \"" + fname_str + "\" could not be opened!");

include/pybind11/pytypes.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2583,7 +2583,8 @@ str_attr_accessor object_api<D>::doc() const {
25832583

25842584
template <typename D>
25852585
object object_api<D>::annotations() const {
2586-
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION <= 9
2586+
// This is needed again because of the lazy annotations added in 3.14+
2587+
#if PY_VERSION_HEX < 0x030A0000 || PY_VERSION_HEX >= 0x030E0000
25872588
// https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
25882589
if (!hasattr(derived(), "__annotations__")) {
25892590
setattr(derived(), "__annotations__", dict());

pybind11/__main__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import argparse
5+
import functools
56
import re
67
import sys
78
import sysconfig
@@ -49,7 +50,10 @@ def print_includes() -> None:
4950

5051

5152
def main() -> None:
52-
parser = argparse.ArgumentParser()
53+
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
54+
if sys.version_info >= (3, 14):
55+
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
56+
parser = make_parser()
5357
parser.add_argument(
5458
"--version",
5559
action="version",

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classifiers = [
2222
"Programming Language :: Python :: 3.11",
2323
"Programming Language :: Python :: 3.12",
2424
"Programming Language :: Python :: 3.13",
25+
"Programming Language :: Python :: 3.14",
2526
"Programming Language :: Python :: Implementation :: PyPy",
2627
"Programming Language :: Python :: Implementation :: CPython",
2728
"Programming Language :: C++",

tests/pybind11_tests.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) {
9090
m.attr("cpp_std") = cpp_std();
9191
m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID;
9292
// Free threaded Python uses UINT32_MAX for immortal objects.
93-
m.attr("PYBIND11_REFCNT_IMMORTAL") = UINT32_MAX;
9493
m.attr("PYBIND11_SIMPLE_GIL_MANAGEMENT") =
9594
#if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
9695
true;

tests/test_class.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@
66
import pytest
77

88
import env
9-
from pybind11_tests import PYBIND11_REFCNT_IMMORTAL, ConstructorStats, UserType
9+
from pybind11_tests import ConstructorStats, UserType
1010
from pybind11_tests import class_ as m
1111

12+
UINT32MAX = 2**32 - 1
13+
14+
15+
def refcount_immortal(ob: object) -> int:
16+
if _is_immortal := getattr(sys, "_is_immortal", None):
17+
return UINT32MAX if _is_immortal(ob) else sys.getrefcount(ob)
18+
return sys.getrefcount(ob)
19+
1220

1321
def test_obj_class_name():
1422
expected_name = "UserType" if env.PYPY else "pybind11_tests.UserType"
@@ -382,23 +390,23 @@ def test_brace_initialization():
382390
@pytest.mark.xfail("env.PYPY or env.GRAALPY")
383391
def test_class_refcount():
384392
"""Instances must correctly increase/decrease the reference count of their types (#1029)"""
385-
from sys import getrefcount
386393

387394
class PyDog(m.Dog):
388395
pass
389396

390397
for cls in m.Dog, PyDog:
391-
refcount_1 = getrefcount(cls)
398+
refcount_1 = refcount_immortal(cls)
392399
molly = [cls("Molly") for _ in range(10)]
393-
refcount_2 = getrefcount(cls)
400+
refcount_2 = refcount_immortal(cls)
394401

395402
del molly
396403
pytest.gc_collect()
397-
refcount_3 = getrefcount(cls)
404+
refcount_3 = refcount_immortal(cls)
398405

406+
# Python may report a large value here (above 30 bits), that's also fine
399407
assert refcount_1 == refcount_3
400408
assert (refcount_2 > refcount_1) or (
401-
refcount_2 == refcount_1 == PYBIND11_REFCNT_IMMORTAL
409+
refcount_2 == refcount_1 and refcount_1 >= 2**29
402410
)
403411

404412

tests/test_methods_and_attributes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ def test_property_rvalue_policy():
305305

306306
# https://foss.heptapod.net/pypy/pypy/-/issues/2447
307307
@pytest.mark.xfail("env.PYPY")
308+
@pytest.mark.xfail(
309+
sys.version_info == (3, 14, 0, "beta", 1),
310+
reason="3.14.0b1 bug: https://github.com/python/cpython/issues/133912",
311+
strict=True,
312+
)
308313
def test_dynamic_attributes():
309314
instance = m.DynamicClass()
310315
assert not hasattr(instance, "foo")

tests/test_multiple_interpreters.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def test_independent_subinterpreters():
1515

1616
sys.path.append(".")
1717

18-
if sys.version_info >= (3, 14):
18+
# This is supposed to be added to PyPI sometime in 3.14's lifespan
19+
if sys.version_info >= (3, 15):
1920
import interpreters
2021
elif sys.version_info >= (3, 13):
2122
import _interpreters as interpreters
@@ -86,7 +87,7 @@ def test_dependent_subinterpreters():
8687

8788
sys.path.append(".")
8889

89-
if sys.version_info >= (3, 14):
90+
if sys.version_info >= (3, 15):
9091
import interpreters
9192
elif sys.version_info >= (3, 13):
9293
import _interpreters as interpreters

tests/test_operator_overloading.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,4 @@ def test_overriding_eq_reset_hash():
158158
def test_return_set_of_unhashable():
159159
with pytest.raises(TypeError) as excinfo:
160160
m.get_unhashable_HashMe_set()
161-
assert str(excinfo.value.__cause__).startswith("unhashable type:")
161+
assert "unhashable type" in str(excinfo.value.__cause__)

tests/test_pickling.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pickle
44
import re
5+
import sys
56

67
import pytest
78

@@ -62,7 +63,20 @@ def test_roundtrip(cls_name):
6263

6364

6465
@pytest.mark.xfail("env.PYPY")
65-
@pytest.mark.parametrize("cls_name", ["PickleableWithDict", "PickleableWithDictNew"])
66+
@pytest.mark.parametrize(
67+
"cls_name",
68+
[
69+
pytest.param(
70+
"PickleableWithDict",
71+
marks=pytest.mark.xfail(
72+
sys.version_info == (3, 14, 0, "beta", 1),
73+
reason="3.14.0b1 bug: https://github.com/python/cpython/issues/133912",
74+
strict=True,
75+
),
76+
),
77+
"PickleableWithDictNew",
78+
],
79+
)
6680
def test_roundtrip_with_dict(cls_name):
6781
cls = getattr(m, cls_name)
6882
p = cls("test_value")

tests/test_pytypes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,10 @@ def test_dict_ranges(tested_dict, expected):
11431143

11441144
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
11451145
def get_annotations_helper(o):
1146+
if sys.version_info >= (3, 14):
1147+
import annotationlib
1148+
1149+
return annotationlib.get_annotations(o) or None
11461150
if isinstance(o, type):
11471151
return o.__dict__.get("__annotations__", None)
11481152
return getattr(o, "__annotations__", None)

0 commit comments

Comments
 (0)