Browse Source

fix: support Python 3.14 (#5646)

* ci: support Python 3.14

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: Python 3.14 name change

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: fix expected output to handle Python 3.14

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: tighten CLI and add color on 3.14+

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: ignore failure on 3.14.0b1

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: support Python 3.14.0b1 with interperters

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Update test_multiple_interpreters.py

* Update test_multiple_interpreters.py

* fix: new breakage for 3.14 fixed

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: handle empty annotations 3.14

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: Python 3.14 may not create the annotations dict

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: use PyUnstable_IsImmortal

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: use sys._is_immortal

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: ignore large values for refcount too

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* style: pre-commit fixes

* ci: enable all free-threaded builds

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: patch for embed

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Revert "fix: patch for embed"

This reverts commit c4226a0671.

* ci: drop new 3.xt additions

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: logic issue, also add some comments

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Update include/pybind11/pytypes.h

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
pull/5668/head
Henry Schreiner 2 months ago committed by GitHub
parent
commit
094343c74a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      .github/workflows/ci.yml
  2. 7
      include/pybind11/eval.h
  3. 3
      include/pybind11/pytypes.h
  4. 6
      pybind11/__main__.py
  5. 1
      pyproject.toml
  6. 1
      tests/pybind11_tests.cpp
  7. 20
      tests/test_class.py
  8. 5
      tests/test_methods_and_attributes.py
  9. 5
      tests/test_multiple_interpreters.py
  10. 2
      tests/test_operator_overloading.py
  11. 16
      tests/test_pickling.py
  12. 4
      tests/test_pytypes.py

4
.github/workflows/ci.yml

@ -34,6 +34,7 @@ jobs: @@ -34,6 +34,7 @@ jobs:
python:
- '3.8'
- '3.13'
- '3.14'
- 'pypy-3.10'
- 'pypy-3.11'
- 'graalpy-24.2'
@ -108,6 +109,9 @@ jobs: @@ -108,6 +109,9 @@ jobs:
# No NumPy for PyPy 3.10 ARM
- runs-on: macos-14
python: 'pypy-3.10'
# Beta 1 broken for compiling on GHA (thinks it's free-threaded)
- runs-on: windows-2022
python: '3.14'
name: "🐍 ${{ matrix.python }} • ${{ matrix.runs-on }} • x64 ${{ matrix.args }}"

7
include/pybind11/eval.h

@ -133,7 +133,12 @@ object eval_file(str fname, object global = globals(), object local = object()) @@ -133,7 +133,12 @@ object eval_file(str fname, object global = globals(), object local = object())
int closeFile = 1;
std::string fname_str = (std::string) fname;
FILE *f = _Py_fopen_obj(fname.ptr(), "r");
FILE *f =
# if PY_VERSION_HEX >= 0x030E0000
Py_fopen(fname.ptr(), "r");
# else
_Py_fopen_obj(fname.ptr(), "r");
# endif
if (!f) {
PyErr_Clear();
pybind11_fail("File \"" + fname_str + "\" could not be opened!");

3
include/pybind11/pytypes.h

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

6
pybind11/__main__.py

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
from __future__ import annotations
import argparse
import functools
import re
import sys
import sysconfig
@ -49,7 +50,10 @@ def print_includes() -> None: @@ -49,7 +50,10 @@ def print_includes() -> None:
def main() -> None:
parser = argparse.ArgumentParser()
make_parser = functools.partial(argparse.ArgumentParser, allow_abbrev=False)
if sys.version_info >= (3, 14):
make_parser = functools.partial(make_parser, color=True, suggest_on_error=True)
parser = make_parser()
parser.add_argument(
"--version",
action="version",

1
pyproject.toml

@ -22,6 +22,7 @@ classifiers = [ @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: C++",

1
tests/pybind11_tests.cpp

@ -90,7 +90,6 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) { @@ -90,7 +90,6 @@ PYBIND11_MODULE(pybind11_tests, m, py::mod_gil_not_used()) {
m.attr("cpp_std") = cpp_std();
m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID;
// Free threaded Python uses UINT32_MAX for immortal objects.
m.attr("PYBIND11_REFCNT_IMMORTAL") = UINT32_MAX;
m.attr("PYBIND11_SIMPLE_GIL_MANAGEMENT") =
#if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
true;

20
tests/test_class.py

@ -6,9 +6,17 @@ from unittest import mock @@ -6,9 +6,17 @@ from unittest import mock
import pytest
import env
from pybind11_tests import PYBIND11_REFCNT_IMMORTAL, ConstructorStats, UserType
from pybind11_tests import ConstructorStats, UserType
from pybind11_tests import class_ as m
UINT32MAX = 2**32 - 1
def refcount_immortal(ob: object) -> int:
if _is_immortal := getattr(sys, "_is_immortal", None):
return UINT32MAX if _is_immortal(ob) else sys.getrefcount(ob)
return sys.getrefcount(ob)
def test_obj_class_name():
expected_name = "UserType" if env.PYPY else "pybind11_tests.UserType"
@ -382,23 +390,23 @@ def test_brace_initialization(): @@ -382,23 +390,23 @@ def test_brace_initialization():
@pytest.mark.xfail("env.PYPY or env.GRAALPY")
def test_class_refcount():
"""Instances must correctly increase/decrease the reference count of their types (#1029)"""
from sys import getrefcount
class PyDog(m.Dog):
pass
for cls in m.Dog, PyDog:
refcount_1 = getrefcount(cls)
refcount_1 = refcount_immortal(cls)
molly = [cls("Molly") for _ in range(10)]
refcount_2 = getrefcount(cls)
refcount_2 = refcount_immortal(cls)
del molly
pytest.gc_collect()
refcount_3 = getrefcount(cls)
refcount_3 = refcount_immortal(cls)
# Python may report a large value here (above 30 bits), that's also fine
assert refcount_1 == refcount_3
assert (refcount_2 > refcount_1) or (
refcount_2 == refcount_1 == PYBIND11_REFCNT_IMMORTAL
refcount_2 == refcount_1 and refcount_1 >= 2**29
)

5
tests/test_methods_and_attributes.py

@ -305,6 +305,11 @@ def test_property_rvalue_policy(): @@ -305,6 +305,11 @@ def test_property_rvalue_policy():
# https://foss.heptapod.net/pypy/pypy/-/issues/2447
@pytest.mark.xfail("env.PYPY")
@pytest.mark.xfail(
sys.version_info == (3, 14, 0, "beta", 1),
reason="3.14.0b1 bug: https://github.com/python/cpython/issues/133912",
strict=True,
)
def test_dynamic_attributes():
instance = m.DynamicClass()
assert not hasattr(instance, "foo")

5
tests/test_multiple_interpreters.py

@ -15,7 +15,8 @@ def test_independent_subinterpreters(): @@ -15,7 +15,8 @@ def test_independent_subinterpreters():
sys.path.append(".")
if sys.version_info >= (3, 14):
# This is supposed to be added to PyPI sometime in 3.14's lifespan
if sys.version_info >= (3, 15):
import interpreters
elif sys.version_info >= (3, 13):
import _interpreters as interpreters
@ -86,7 +87,7 @@ def test_dependent_subinterpreters(): @@ -86,7 +87,7 @@ def test_dependent_subinterpreters():
sys.path.append(".")
if sys.version_info >= (3, 14):
if sys.version_info >= (3, 15):
import interpreters
elif sys.version_info >= (3, 13):
import _interpreters as interpreters

2
tests/test_operator_overloading.py

@ -158,4 +158,4 @@ def test_overriding_eq_reset_hash(): @@ -158,4 +158,4 @@ def test_overriding_eq_reset_hash():
def test_return_set_of_unhashable():
with pytest.raises(TypeError) as excinfo:
m.get_unhashable_HashMe_set()
assert str(excinfo.value.__cause__).startswith("unhashable type:")
assert "unhashable type" in str(excinfo.value.__cause__)

16
tests/test_pickling.py

@ -2,6 +2,7 @@ from __future__ import annotations @@ -2,6 +2,7 @@ from __future__ import annotations
import pickle
import re
import sys
import pytest
@ -62,7 +63,20 @@ def test_roundtrip(cls_name): @@ -62,7 +63,20 @@ def test_roundtrip(cls_name):
@pytest.mark.xfail("env.PYPY")
@pytest.mark.parametrize("cls_name", ["PickleableWithDict", "PickleableWithDictNew"])
@pytest.mark.parametrize(
"cls_name",
[
pytest.param(
"PickleableWithDict",
marks=pytest.mark.xfail(
sys.version_info == (3, 14, 0, "beta", 1),
reason="3.14.0b1 bug: https://github.com/python/cpython/issues/133912",
strict=True,
),
),
"PickleableWithDictNew",
],
)
def test_roundtrip_with_dict(cls_name):
cls = getattr(m, cls_name)
p = cls("test_value")

4
tests/test_pytypes.py

@ -1143,6 +1143,10 @@ def test_dict_ranges(tested_dict, expected): @@ -1143,6 +1143,10 @@ def test_dict_ranges(tested_dict, expected):
# https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
def get_annotations_helper(o):
if sys.version_info >= (3, 14):
import annotationlib
return annotationlib.get_annotations(o) or None
if isinstance(o, type):
return o.__dict__.get("__annotations__", None)
return getattr(o, "__annotations__", None)

Loading…
Cancel
Save