Skip to content

Commit ae6432b

Browse files
authored
fix: Python 3.13t with GIL (#5139)
* ci: try Python 3.13t Signed-off-by: Henry Schreiner <[email protected]> * fix: support Python 3.13t Signed-off-by: Henry Schreiner <[email protected]> * fix: patch PyPy Signed-off-by: Henry Schreiner <[email protected]> * tests: one more int cast Signed-off-by: Henry Schreiner <[email protected]> * tests: cleanup Signed-off-by: Henry Schreiner <[email protected]> * refactor: use named constant in tests for immortal refcounts Signed-off-by: Henry Schreiner <[email protected]> * docs: move comment about free threaded Python Signed-off-by: Henry Schreiner <[email protected]> --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent a5b9e50 commit ae6432b

File tree

8 files changed

+62
-9
lines changed

8 files changed

+62
-9
lines changed

.github/workflows/ci.yml

+29
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,35 @@ jobs:
196196
pytest tests/extra_setuptools
197197
if: "!(matrix.runs-on == 'windows-2022')"
198198

199+
manylinux:
200+
name: Manylinux on 🐍 3.13t • GIL
201+
runs-on: ubuntu-latest
202+
timeout-minutes: 40
203+
container: quay.io/pypa/musllinux_1_2_x86_64:latest
204+
steps:
205+
- uses: actions/checkout@v4
206+
with:
207+
fetch-depth: 0
208+
209+
- name: Prepare venv
210+
run: python3.13 -m venv .venv
211+
212+
- name: Install Python deps
213+
run: .venv/bin/pip install -r tests/requirements.txt
214+
215+
- name: Configure C++11
216+
run: >
217+
cmake -S. -Bbuild
218+
-DPYBIND11_WERROR=ON
219+
-DDOWNLOAD_CATCH=ON
220+
-DDOWNLOAD_EIGEN=ON
221+
-DPython_ROOT_DIR=.venv
222+
223+
- name: Build C++11
224+
run: cmake --build build -j2
225+
226+
- name: Python tests C++11
227+
run: cmake --build build --target pytest -j2
199228

200229
deadsnakes:
201230
strategy:

include/pybind11/pytypes.h

+9-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,15 @@ class object_api : public pyobject_tag {
183183
str_attr_accessor doc() const;
184184

185185
/// Return the object's current reference count
186-
int ref_count() const { return static_cast<int>(Py_REFCNT(derived().ptr())); }
186+
ssize_t ref_count() const {
187+
#ifdef PYPY_VERSION
188+
// PyPy uses the top few bits for REFCNT_FROM_PYPY & REFCNT_FROM_PYPY_LIGHT
189+
// Following pybind11 2.12.1 and older behavior and removing this part
190+
return static_cast<ssize_t>(static_cast<int>(Py_REFCNT(derived().ptr())));
191+
#else
192+
return Py_REFCNT(derived().ptr());
193+
#endif
194+
}
187195

188196
// TODO PYBIND11_DEPRECATED(
189197
// "Call py::type::handle_of(h) or py::type::of(h) instead of h.get_type()")

tests/pybind11_tests.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ PYBIND11_MODULE(pybind11_tests, m) {
8989
#endif
9090
m.attr("cpp_std") = cpp_std();
9191
m.attr("PYBIND11_INTERNALS_ID") = PYBIND11_INTERNALS_ID;
92+
// Free threaded Python uses UINT32_MAX for immortal objects.
93+
m.attr("PYBIND11_REFCNT_IMMORTAL") = UINT32_MAX;
9294
m.attr("PYBIND11_SIMPLE_GIL_MANAGEMENT") =
9395
#if defined(PYBIND11_SIMPLE_GIL_MANAGEMENT)
9496
true;

tests/test_class.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
import env
6-
from pybind11_tests import ConstructorStats, UserType
6+
from pybind11_tests import PYBIND11_REFCNT_IMMORTAL, ConstructorStats, UserType
77
from pybind11_tests import class_ as m
88

99

@@ -377,7 +377,9 @@ class PyDog(m.Dog):
377377
refcount_3 = getrefcount(cls)
378378

379379
assert refcount_1 == refcount_3
380-
assert refcount_2 > refcount_1
380+
assert (refcount_2 > refcount_1) or (
381+
refcount_2 == refcount_1 == PYBIND11_REFCNT_IMMORTAL
382+
)
381383

382384

383385
def test_reentrant_implicit_conversion_failure(msg):

tests/test_embed/CMakeLists.txt

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ if("${PYTHON_MODULE_EXTENSION}" MATCHES "pypy" OR "${Python_INTERPRETER_ID}" STR
77
return()
88
endif()
99

10+
if(TARGET Python::Module AND NOT TARGET Python::Python)
11+
message(STATUS "Skipping embed test since no embed libs found")
12+
add_custom_target(cpptest) # Dummy target since embedding is not supported.
13+
set(_suppress_unused_variable_warning "${DOWNLOAD_CATCH}")
14+
return()
15+
endif()
16+
1017
find_package(Catch 2.13.9)
1118

1219
if(CATCH_FOUND)

tests/test_kwargs_and_defaults.cpp

+5-2
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,13 @@ TEST_SUBMODULE(kwargs_and_defaults, m) {
150150

151151
// test_args_refcount
152152
// PyPy needs a garbage collection to get the reference count values to match CPython's behaviour
153+
// PyPy uses the top few bits for REFCNT_FROM_PYPY & REFCNT_FROM_PYPY_LIGHT, so truncate
153154
#ifdef PYPY_VERSION
154155
# define GC_IF_NEEDED ConstructorStats::gc()
156+
# define REFCNT(x) (int) Py_REFCNT(x)
155157
#else
156158
# define GC_IF_NEEDED
159+
# define REFCNT(x) Py_REFCNT(x)
157160
#endif
158161
m.def("arg_refcount_h", [](py::handle h) {
159162
GC_IF_NEEDED;
@@ -172,7 +175,7 @@ TEST_SUBMODULE(kwargs_and_defaults, m) {
172175
py::tuple t(a.size());
173176
for (size_t i = 0; i < a.size(); i++) {
174177
// Use raw Python API here to avoid an extra, intermediate incref on the tuple item:
175-
t[i] = (int) Py_REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast<py::ssize_t>(i)));
178+
t[i] = REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast<py::ssize_t>(i)));
176179
}
177180
return t;
178181
});
@@ -182,7 +185,7 @@ TEST_SUBMODULE(kwargs_and_defaults, m) {
182185
t[0] = o.ref_count();
183186
for (size_t i = 0; i < a.size(); i++) {
184187
// Use raw Python API here to avoid an extra, intermediate incref on the tuple item:
185-
t[i + 1] = (int) Py_REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast<py::ssize_t>(i)));
188+
t[i + 1] = REFCNT(PyTuple_GET_ITEM(a.ptr(), static_cast<py::ssize_t>(i)));
186189
}
187190
return t;
188191
});

tests/test_kwargs_and_defaults.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
from pybind11_tests import PYBIND11_REFCNT_IMMORTAL
34
from pybind11_tests import kwargs_and_defaults as m
45

56

@@ -384,7 +385,7 @@ def test_args_refcount():
384385
myval = 54321
385386
expected = refcount(myval)
386387
assert m.arg_refcount_h(myval) == expected
387-
assert m.arg_refcount_o(myval) == expected + 1
388+
assert m.arg_refcount_o(myval) in {expected + 1, PYBIND11_REFCNT_IMMORTAL}
388389
assert m.arg_refcount_h(myval) == expected
389390
assert refcount(myval) == expected
390391

@@ -420,6 +421,7 @@ def test_args_refcount():
420421
# for the `py::args`; in the previous case, we could simply inc_ref and pass on Python's input
421422
# tuple without having to inc_ref the individual elements, but here we can't, hence the extra
422423
# refs.
423-
assert m.mixed_args_refcount(myval, myval, myval) == (exp3 + 3, exp3 + 3, exp3 + 3)
424+
exp3_3 = PYBIND11_REFCNT_IMMORTAL if exp3 == PYBIND11_REFCNT_IMMORTAL else exp3 + 3
425+
assert m.mixed_args_refcount(myval, myval, myval) == (exp3_3, exp3_3, exp3_3)
424426

425427
assert m.class_default_argument() == "<class 'decimal.Decimal'>"

tests/test_pytypes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
import env
8-
from pybind11_tests import detailed_error_messages_enabled
8+
from pybind11_tests import PYBIND11_REFCNT_IMMORTAL, detailed_error_messages_enabled
99
from pybind11_tests import pytypes as m
1010

1111

@@ -635,7 +635,7 @@ def test_memoryview_refcount(method):
635635
ref_before = sys.getrefcount(buf)
636636
view = method(buf)
637637
ref_after = sys.getrefcount(buf)
638-
assert ref_before < ref_after
638+
assert ref_before < ref_after or ref_before == ref_after == PYBIND11_REFCNT_IMMORTAL
639639
assert list(view) == list(buf)
640640

641641

0 commit comments

Comments
 (0)