From 85e9cda72b5f045e8444f0bce21513e487ef4f7e Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 22 Aug 2025 11:15:23 +0200 Subject: [PATCH 1/5] Support the "free-threaded" variant of Python (PEP-703) DISCLAIMER: at this point, this is just an experiment, so don't rely on this branch unless you know what you are doing. --- rapidjson.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rapidjson.cpp b/rapidjson.cpp index 40ce8fa..e0b6b27 100644 --- a/rapidjson.cpp +++ b/rapidjson.cpp @@ -3,7 +3,7 @@ // :Author: Ken Robbins // :License: MIT License // :Copyright: © 2015 Ken Robbins -// :Copyright: © 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Lele Gaifax +// :Copyright: © 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025 Lele Gaifax // #include @@ -4117,6 +4117,9 @@ module_exec(PyObject* m) static struct PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*) module_exec}, +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif {0, NULL} }; From fe16103e15cd987420845dda4e114699473775f8 Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 22 Aug 2025 11:23:34 +0200 Subject: [PATCH 2/5] Skip the recursion limit test under free-threaded variant --- tests/test_circular.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_circular.py b/tests/test_circular.py index 89e15b3..1a64935 100644 --- a/tests/test_circular.py +++ b/tests/test_circular.py @@ -3,7 +3,7 @@ # :Author: John Anderson # :License: MIT License # :Copyright: © 2015 John Anderson -# :Copyright: © 2017, 2018, 2020, 2023, 2024 Lele Gaifax +# :Copyright: © 2017, 2018, 2020, 2023, 2024, 2025 Lele Gaifax # import sys @@ -74,6 +74,10 @@ def test_max_recursion_depth(dumps): sys.setrecursionlimit(rl) +@pytest.mark.skipif( + not sys._is_gil_enabled(), + reason="Crashes under free-threaded variant, perhaps getrecursionlimit() works differently there?" +) def test_parse_respects_recursion_limit(loads): rl = sys.getrecursionlimit() From 87984832afc482de6ba22c97676a352876bcf88e Mon Sep 17 00:00:00 2001 From: Lele Gaifax Date: Fri, 22 Aug 2025 11:24:29 +0200 Subject: [PATCH 3/5] Skip the tracemalloc-based tests under free-threaded variant --- tests/test_memory_leaks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_memory_leaks.py b/tests/test_memory_leaks.py index 51b5a55..1205fd7 100644 --- a/tests/test_memory_leaks.py +++ b/tests/test_memory_leaks.py @@ -9,10 +9,15 @@ import io import datetime import gc +import sys import pytest import rapidjson as rj +pytestmark = pytest.mark.skipif( + not sys._is_gil_enabled(), + reason="Crashes under free-threaded variant, perhaps tracemalloc does not work there?" +) tracemalloc = pytest.importorskip("tracemalloc") From e5f3e0d54b57afeca146a0fc9efbb1acd5bf8cbd Mon Sep 17 00:00:00 2001 From: espressolee <70549809+espressolee@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:57:46 +0900 Subject: [PATCH 4/5] Run CI on the free-threaded branch, and on a free-threaded interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow only triggers on master, so nothing on this branch — including pull requests into it — has ever been exercised by CI. And no job uses a free-threaded interpreter, so the branch that exists in order to be free-threaded is not tested as one. This adds `free-threaded` to the push and pull_request filters, and one job that runs the test suite on 3.14t. The job asserts `sys._is_gil_enabled()` is False before it installs anything. Without that check the job can go green while proving nothing: if the runner ever resolves 3.14t to a GIL-enabled interpreter, every test below still passes, and the tick would mean "the default build works", which the existing job already covers. Deliberately minimal — build and pytest only. Doctests and stubtest are left to the existing job rather than duplicated here. --- .github/workflows/main.yml | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7452ecd..2048d00 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,11 +16,45 @@ name: Build, test and upload to PyPI on: push: - branches: [ master ] + branches: [ master, free-threaded ] pull_request: - branches: [ master ] + branches: [ master, free-threaded ] jobs: + free-threaded-tests: + name: Tests on the free-threaded interpreter + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ['3.14t'] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + # Without this the job can pass while proving nothing: if the runner ever + # falls back to a GIL-enabled interpreter, every test below still passes and + # the green tick means "the GIL build works", which is already covered above. + - name: Confirm the interpreter really is free-threaded + run: | + python -c "import sys; assert hasattr(sys, '_is_gil_enabled'), sys.version; assert not sys._is_gil_enabled(), 'GIL is enabled: ' + sys.version; print(sys.version)" + + - name: Install dependencies + run: | + pip install -r requirements-test.txt + pip install . + + - name: Tests + run: | + pytest tests + tests: name: All tests, on current Python runs-on: ubuntu-latest From 851369b658a7ddc3dbf01c21eb9c9a8ace1d4404 Mon Sep 17 00:00:00 2001 From: espressolee <70549809+espressolee@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:02:39 +0900 Subject: [PATCH 5/5] Guard the free-threading skips so they import on Python < 3.13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both skip conditions call sys._is_gil_enabled() at module scope, and that attribute only exists on 3.13+. On anything older the call raises AttributeError during collection, so pytest aborts before running a single test: ERROR collecting tests/test_circular.py not sys._is_gil_enabled(), E AttributeError: module 'sys' has no attribute '_is_gil_enabled' That is not hypothetical on this branch. Its CI pins 3.11, and cibuildwheel runs the suite against every wheel it builds, so the same error takes out the test job, the debug job and the macOS wheel job — one cause, three red jobs. It went unseen because the workflow only triggers on master, so CI has never run on this branch at all; the previous commit fixes that half. getattr(..., lambda: True) keeps the behaviour identical on 3.13+ and treats older interpreters as GIL-enabled, which they are. --- tests/test_circular.py | 3 ++- tests/test_memory_leaks.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_circular.py b/tests/test_circular.py index 1a64935..68a321d 100644 --- a/tests/test_circular.py +++ b/tests/test_circular.py @@ -75,7 +75,8 @@ def test_max_recursion_depth(dumps): @pytest.mark.skipif( - not sys._is_gil_enabled(), + # sys._is_gil_enabled() exists only on 3.13+; everywhere else the GIL is on. + not getattr(sys, "_is_gil_enabled", lambda: True)(), reason="Crashes under free-threaded variant, perhaps getrecursionlimit() works differently there?" ) def test_parse_respects_recursion_limit(loads): diff --git a/tests/test_memory_leaks.py b/tests/test_memory_leaks.py index 1205fd7..863c80b 100644 --- a/tests/test_memory_leaks.py +++ b/tests/test_memory_leaks.py @@ -15,7 +15,8 @@ import rapidjson as rj pytestmark = pytest.mark.skipif( - not sys._is_gil_enabled(), + # sys._is_gil_enabled() exists only on 3.13+; everywhere else the GIL is on. + not getattr(sys, "_is_gil_enabled", lambda: True)(), reason="Crashes under free-threaded variant, perhaps tracemalloc does not work there?" ) tracemalloc = pytest.importorskip("tracemalloc")