diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 365829ce18..c9f7ae9617 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: # 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 }}" diff --git a/include/pybind11/eval.h b/include/pybind11/eval.h index 3ed1b5a4a9..a588729182 100644 --- a/include/pybind11/eval.h +++ b/include/pybind11/eval.h @@ -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!"); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 32b3d2575d..fb78c0f2b3 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -2583,7 +2583,8 @@ str_attr_accessor object_api::doc() const { template object object_api::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()); diff --git a/pybind11/__main__.py b/pybind11/__main__.py index 28be9f165b..21c3cd9abc 100644 --- a/pybind11/__main__.py +++ b/pybind11/__main__.py @@ -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: 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", diff --git a/pyproject.toml b/pyproject.toml index 41f26ffb21..93f73748c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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++", diff --git a/tests/pybind11_tests.cpp b/tests/pybind11_tests.cpp index 0b92d15871..4317925737 100644 --- a/tests/pybind11_tests.cpp +++ b/tests/pybind11_tests.cpp @@ -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; diff --git a/tests/test_class.py b/tests/test_class.py index 00e939a9ed..1e82930361 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -6,9 +6,17 @@ 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(): @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 ) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 9dc28ffdaa..f0085c871b 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -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") diff --git a/tests/test_multiple_interpreters.py b/tests/test_multiple_interpreters.py index d7321171bd..b1eca50585 100644 --- a/tests/test_multiple_interpreters.py +++ b/tests/test_multiple_interpreters.py @@ -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(): 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 diff --git a/tests/test_operator_overloading.py b/tests/test_operator_overloading.py index 0230e72c5a..19c4bbbb56 100644 --- a/tests/test_operator_overloading.py +++ b/tests/test_operator_overloading.py @@ -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__) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 98381d9bdc..bd06292f4e 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -2,6 +2,7 @@ import pickle import re +import sys import pytest @@ -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") diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index d1044d995d..64e56b4bfd 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -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)