From 15bc5f3303cd51bcad3745a28bc7710fd2e1e4d9 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 May 2024 17:08:38 -0400 Subject: [PATCH 1/7] ci: try Python 3.13t Signed-off-by: Henry Schreiner --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ tests/test_embed/CMakeLists.txt | 7 +++++++ 2 files changed, 39 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb6ee3bad6..9b59c3d8f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,38 @@ jobs: pytest tests/extra_setuptools if: "!(matrix.runs-on == 'windows-2022')" + manylinux: + name: Manylinux on 🐍 ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.13t", "3.13"] + timeout-minutes: 40 + container: quay.io/pypa/musllinux_1_2_x86_64:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Prepare venv + run: python${{ matrix.python }} -m venv .venv + + - name: Install Python deps + run: .venv/bin/pip install -r tests/requirements.txt + + - name: Configure C++11 + run: > + cmake -S. -Bbuild + -DPYBIND11_WERROR=ON + -DDOWNLOAD_CATCH=ON + -DDOWNLOAD_EIGEN=ON + -DPython_ROOT_DIR=.venv + + - name: Build C++11 + run: cmake --build build -j2 + + - name: Python tests C++11 + run: cmake --build build --target pytest -j2 deadsnakes: strategy: diff --git a/tests/test_embed/CMakeLists.txt b/tests/test_embed/CMakeLists.txt index 09a3693999..9b539cd42d 100644 --- a/tests/test_embed/CMakeLists.txt +++ b/tests/test_embed/CMakeLists.txt @@ -7,6 +7,13 @@ if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy" OR "${Python_INTERPRETER_ID}" STR return() endif() +if(TARGET Python::Module AND NOT TARGET Python::Python) + message(STATUS "Skipping embed test since no embed libs found") + add_custom_target(cpptest) # Dummy target since embedding is not supported. + set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}") + return() +endif() + find_package(Catch 2.13.9) if(CATCH_FOUND) From c57a84092402486695c85c4fb930d3bd1f7a71d9 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 May 2024 18:56:17 -0400 Subject: [PATCH 2/7] fix: support Python 3.13t Signed-off-by: Henry Schreiner --- include/pybind11/pytypes.h | 2 +- tests/test_class.py | 3 ++- tests/test_pytypes.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index d5f6af8e02..cbf325be29 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -183,7 +183,7 @@ class object_api : public pyobject_tag { str_attr_accessor doc() const; /// Return the object's current reference count - int ref_count() const { return static_cast(Py_REFCNT(derived().ptr())); } + ssize_t ref_count() const { return Py_REFCNT(derived().ptr()); } // TODO PYBIND11_DEPRECATED( // "Call py::type::handle_of(h) or py::type::of(h) instead of h.get_type()") diff --git a/tests/test_class.py b/tests/test_class.py index 73a48309e8..199b936db0 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -377,7 +377,8 @@ class PyDog(m.Dog): refcount_3 = getrefcount(cls) assert refcount_1 == refcount_3 - assert refcount_2 > refcount_1 + # Free-threaded Python uses UINT32_MAX for immortal objects + assert (refcount_2 > refcount_1) or (refcount_2 == refcount_1 == 2**32 - 1) def test_reentrant_implicit_conversion_failure(msg): diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index f6271a628a..212a492a89 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -635,7 +635,8 @@ def test_memoryview_refcount(method): ref_before = sys.getrefcount(buf) view = method(buf) ref_after = sys.getrefcount(buf) - assert ref_before < ref_after + # Free threaded Python uses UINT32_MAX + assert ref_before < ref_after or ref_before == ref_after == 2**32 - 1 assert list(view) == list(buf) From f74d23af99d4d089b8d6ee16ccf16ceed57fbdb0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 May 2024 20:10:09 -0400 Subject: [PATCH 3/7] fix: patch PyPy Signed-off-by: Henry Schreiner --- include/pybind11/pytypes.h | 10 +++++++++- tests/test_kwargs_and_defaults.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index cbf325be29..a27d6aed09 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -183,7 +183,15 @@ class object_api : public pyobject_tag { str_attr_accessor doc() const; /// Return the object's current reference count - ssize_t ref_count() const { return Py_REFCNT(derived().ptr()); } + ssize_t ref_count() const { +#ifdef PYPY_VERSION + // PyPy uses the top few bits for REFCNT_FROM_PYPY & REFCNT_FROM_PYPY_LIGHT + // Following pybind11 2.12.1 and older behavior and removing this part + return static_cast(static_cast(Py_REFCNT(derived().ptr()))); +#else + return Py_REFCNT(derived().ptr()); +#endif + } // TODO PYBIND11_DEPRECATED( // "Call py::type::handle_of(h) or py::type::of(h) instead of h.get_type()") diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 0e3bbdb765..7053a5d43f 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -384,7 +384,7 @@ def test_args_refcount(): myval = 54321 expected = refcount(myval) assert m.arg_refcount_h(myval) == expected - assert m.arg_refcount_o(myval) == expected + 1 + assert m.arg_refcount_o(myval) in {expected + 1, 2**32 - 1} assert m.arg_refcount_h(myval) == expected assert refcount(myval) == expected From 0d958462d47c7bebfbb52e93eab1a2c56311ead3 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 May 2024 22:21:58 -0400 Subject: [PATCH 4/7] tests: one more int cast Signed-off-by: Henry Schreiner --- tests/test_kwargs_and_defaults.cpp | 6 ++++-- tests/test_kwargs_and_defaults.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_kwargs_and_defaults.cpp b/tests/test_kwargs_and_defaults.cpp index 2f40aca6af..87bb164821 100644 --- a/tests/test_kwargs_and_defaults.cpp +++ b/tests/test_kwargs_and_defaults.cpp @@ -152,8 +152,10 @@ TEST_SUBMODULE(kwargs_and_defaults, m) { // PyPy needs a garbage collection to get the reference count values to match CPython's behaviour #ifdef PYPY_VERSION # define GC_IF_NEEDED ConstructorStats::gc() +# define REFCNT(x) (int) Py_REFCNT(x) #else # define GC_IF_NEEDED +# define REFCNT(x) Py_REFCNT(x) #endif m.def("arg_refcount_h", [](py::handle h) { GC_IF_NEEDED; @@ -172,7 +174,7 @@ TEST_SUBMODULE(kwargs_and_defaults, m) { py::tuple t(a.size()); for (size_t i = 0; i < a.size(); i++) { // Use raw Python API here to avoid an extra, intermediate incref on the tuple item: - t[i] = (int) Py_REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast(i))); + t[i] = REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast(i))); } return t; }); @@ -182,7 +184,7 @@ TEST_SUBMODULE(kwargs_and_defaults, m) { t[0] = o.ref_count(); for (size_t i = 0; i < a.size(); i++) { // Use raw Python API here to avoid an extra, intermediate incref on the tuple item: - t[i + 1] = (int) Py_REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast(i))); + t[i + 1] = REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast(i))); } return t; }); diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 7053a5d43f..2ae783a599 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -420,6 +420,7 @@ def test_args_refcount(): # for the `py::args`; in the previous case, we could simply inc_ref and pass on Python's input # tuple without having to inc_ref the individual elements, but here we can't, hence the extra # refs. - assert m.mixed_args_refcount(myval, myval, myval) == (exp3 + 3, exp3 + 3, exp3 + 3) + exp3_3 = 2**32 - 1 if exp3 == 2**32 - 1 else exp3 + 3 + assert m.mixed_args_refcount(myval, myval, myval) == (exp3_3, exp3_3, exp3_3) assert m.class_default_argument() == "" From a7c337792fb078cafea6801055e8b33f82437732 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 24 May 2024 23:49:45 -0400 Subject: [PATCH 5/7] tests: cleanup Signed-off-by: Henry Schreiner --- .github/workflows/ci.yml | 7 ++----- tests/test_class.py | 2 +- tests/test_kwargs_and_defaults.cpp | 1 + tests/test_kwargs_and_defaults.py | 2 ++ tests/test_pytypes.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b59c3d8f4..177eac46d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,11 +197,8 @@ jobs: if: "!(matrix.runs-on == 'windows-2022')" manylinux: - name: Manylinux on 🐍 ${{ matrix.python }} + name: Manylinux on 🐍 3.13t • GIL runs-on: ubuntu-latest - strategy: - matrix: - python: ["3.13t", "3.13"] timeout-minutes: 40 container: quay.io/pypa/musllinux_1_2_x86_64:latest steps: @@ -210,7 +207,7 @@ jobs: fetch-depth: 0 - name: Prepare venv - run: python${{ matrix.python }} -m venv .venv + run: python3.13 -m venv .venv - name: Install Python deps run: .venv/bin/pip install -r tests/requirements.txt diff --git a/tests/test_class.py b/tests/test_class.py index 199b936db0..7194d1bd61 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -377,7 +377,7 @@ class PyDog(m.Dog): refcount_3 = getrefcount(cls) assert refcount_1 == refcount_3 - # Free-threaded Python uses UINT32_MAX for immortal objects + # Free-threaded Python uses UINT32_MAX for immortal objects. assert (refcount_2 > refcount_1) or (refcount_2 == refcount_1 == 2**32 - 1) diff --git a/tests/test_kwargs_and_defaults.cpp b/tests/test_kwargs_and_defaults.cpp index 87bb164821..bc76ec7c2d 100644 --- a/tests/test_kwargs_and_defaults.cpp +++ b/tests/test_kwargs_and_defaults.cpp @@ -150,6 +150,7 @@ TEST_SUBMODULE(kwargs_and_defaults, m) { // test_args_refcount // PyPy needs a garbage collection to get the reference count values to match CPython's behaviour +// PyPy uses the top few bits for REFCNT_FROM_PYPY & REFCNT_FROM_PYPY_LIGHT, so truncate #ifdef PYPY_VERSION # define GC_IF_NEEDED ConstructorStats::gc() # define REFCNT(x) (int) Py_REFCNT(x) diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 2ae783a599..123911a717 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -384,6 +384,7 @@ def test_args_refcount(): myval = 54321 expected = refcount(myval) assert m.arg_refcount_h(myval) == expected + # Free threaded Python uses UINT32_MAX for immortal objects. assert m.arg_refcount_o(myval) in {expected + 1, 2**32 - 1} assert m.arg_refcount_h(myval) == expected assert refcount(myval) == expected @@ -420,6 +421,7 @@ def test_args_refcount(): # for the `py::args`; in the previous case, we could simply inc_ref and pass on Python's input # tuple without having to inc_ref the individual elements, but here we can't, hence the extra # refs. + # Free threaded Python uses UINT32_MAX for immortal objects. exp3_3 = 2**32 - 1 if exp3 == 2**32 - 1 else exp3 + 3 assert m.mixed_args_refcount(myval, myval, myval) == (exp3_3, exp3_3, exp3_3) diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 212a492a89..c6362d642d 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -635,7 +635,7 @@ def test_memoryview_refcount(method): ref_before = sys.getrefcount(buf) view = method(buf) ref_after = sys.getrefcount(buf) - # Free threaded Python uses UINT32_MAX + # Free threaded Python uses UINT32_MAX for immortal objects. assert ref_before < ref_after or ref_before == ref_after == 2**32 - 1 assert list(view) == list(buf) From 884524c8b25c32ab4f3e385985ef53d83a588de5 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 28 May 2024 01:04:30 -0400 Subject: [PATCH 6/7] refactor: use named constant in tests for immortal refcounts Signed-off-by: Henry Schreiner --- tests/pybind11_tests.cpp | 1 + tests/test_class.py | 6 ++++-- tests/test_kwargs_and_defaults.py | 5 +++-- tests/test_pytypes.py | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/pybind11_tests.cpp b/tests/pybind11_tests.cpp index 81869ebe21..241469b426 100644 --- a/tests/pybind11_tests.cpp +++ b/tests/pybind11_tests.cpp @@ -89,6 +89,7 @@ PYBIND11_MODULE(pybind11_tests, m) { #endif m.attr("cpp_std") = cpp_std(); m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID; + 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 7194d1bd61..bf77206251 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -3,7 +3,7 @@ import pytest import env -from pybind11_tests import ConstructorStats, UserType +from pybind11_tests import PYBIND11_REFCNT_IMMORTAL, ConstructorStats, UserType from pybind11_tests import class_ as m @@ -378,7 +378,9 @@ class PyDog(m.Dog): assert refcount_1 == refcount_3 # Free-threaded Python uses UINT32_MAX for immortal objects. - assert (refcount_2 > refcount_1) or (refcount_2 == refcount_1 == 2**32 - 1) + assert (refcount_2 > refcount_1) or ( + refcount_2 == refcount_1 == PYBIND11_REFCNT_IMMORTAL + ) def test_reentrant_implicit_conversion_failure(msg): diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 123911a717..8eda4a418a 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -1,5 +1,6 @@ import pytest +from pybind11_tests import PYBIND11_REFCNT_IMMORTAL from pybind11_tests import kwargs_and_defaults as m @@ -385,7 +386,7 @@ def test_args_refcount(): expected = refcount(myval) assert m.arg_refcount_h(myval) == expected # Free threaded Python uses UINT32_MAX for immortal objects. - assert m.arg_refcount_o(myval) in {expected + 1, 2**32 - 1} + assert m.arg_refcount_o(myval) in {expected + 1, PYBIND11_REFCNT_IMMORTAL} assert m.arg_refcount_h(myval) == expected assert refcount(myval) == expected @@ -422,7 +423,7 @@ def test_args_refcount(): # tuple without having to inc_ref the individual elements, but here we can't, hence the extra # refs. # Free threaded Python uses UINT32_MAX for immortal objects. - exp3_3 = 2**32 - 1 if exp3 == 2**32 - 1 else exp3 + 3 + exp3_3 = PYBIND11_REFCNT_IMMORTAL if exp3 == PYBIND11_REFCNT_IMMORTAL else exp3 + 3 assert m.mixed_args_refcount(myval, myval, myval) == (exp3_3, exp3_3, exp3_3) assert m.class_default_argument() == "" diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index c6362d642d..277ac54da9 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -5,7 +5,7 @@ import pytest import env -from pybind11_tests import detailed_error_messages_enabled +from pybind11_tests import PYBIND11_REFCNT_IMMORTAL, detailed_error_messages_enabled from pybind11_tests import pytypes as m @@ -636,7 +636,7 @@ def test_memoryview_refcount(method): view = method(buf) ref_after = sys.getrefcount(buf) # Free threaded Python uses UINT32_MAX for immortal objects. - assert ref_before < ref_after or ref_before == ref_after == 2**32 - 1 + assert ref_before < ref_after or ref_before == ref_after == PYBIND11_REFCNT_IMMORTAL assert list(view) == list(buf) From 72a6337885dc723449c0d7e8432ba152e42d1002 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 28 May 2024 08:25:37 -0400 Subject: [PATCH 7/7] docs: move comment about free threaded Python Signed-off-by: Henry Schreiner --- tests/pybind11_tests.cpp | 1 + tests/test_class.py | 1 - tests/test_kwargs_and_defaults.py | 2 -- tests/test_pytypes.py | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/pybind11_tests.cpp b/tests/pybind11_tests.cpp index 241469b426..15cbf74545 100644 --- a/tests/pybind11_tests.cpp +++ b/tests/pybind11_tests.cpp @@ -89,6 +89,7 @@ PYBIND11_MODULE(pybind11_tests, m) { #endif 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) diff --git a/tests/test_class.py b/tests/test_class.py index bf77206251..edaa5b3ca4 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -377,7 +377,6 @@ class PyDog(m.Dog): refcount_3 = getrefcount(cls) assert refcount_1 == refcount_3 - # Free-threaded Python uses UINT32_MAX for immortal objects. assert (refcount_2 > refcount_1) or ( refcount_2 == refcount_1 == PYBIND11_REFCNT_IMMORTAL ) diff --git a/tests/test_kwargs_and_defaults.py b/tests/test_kwargs_and_defaults.py index 8eda4a418a..12d30ecae8 100644 --- a/tests/test_kwargs_and_defaults.py +++ b/tests/test_kwargs_and_defaults.py @@ -385,7 +385,6 @@ def test_args_refcount(): myval = 54321 expected = refcount(myval) assert m.arg_refcount_h(myval) == expected - # Free threaded Python uses UINT32_MAX for immortal objects. assert m.arg_refcount_o(myval) in {expected + 1, PYBIND11_REFCNT_IMMORTAL} assert m.arg_refcount_h(myval) == expected assert refcount(myval) == expected @@ -422,7 +421,6 @@ def test_args_refcount(): # for the `py::args`; in the previous case, we could simply inc_ref and pass on Python's input # tuple without having to inc_ref the individual elements, but here we can't, hence the extra # refs. - # Free threaded Python uses UINT32_MAX for immortal objects. exp3_3 = PYBIND11_REFCNT_IMMORTAL if exp3 == PYBIND11_REFCNT_IMMORTAL else exp3 + 3 assert m.mixed_args_refcount(myval, myval, myval) == (exp3_3, exp3_3, exp3_3) diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 277ac54da9..f941e7ac31 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -635,7 +635,6 @@ def test_memoryview_refcount(method): ref_before = sys.getrefcount(buf) view = method(buf) ref_after = sys.getrefcount(buf) - # Free threaded Python uses UINT32_MAX for immortal objects. assert ref_before < ref_after or ref_before == ref_after == PYBIND11_REFCNT_IMMORTAL assert list(view) == list(buf)