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 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} }; diff --git a/tests/test_circular.py b/tests/test_circular.py index 89e15b3..68a321d 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,11 @@ def test_max_recursion_depth(dumps): sys.setrecursionlimit(rl) +@pytest.mark.skipif( + # 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): rl = sys.getrecursionlimit() diff --git a/tests/test_memory_leaks.py b/tests/test_memory_leaks.py index 51b5a55..863c80b 100644 --- a/tests/test_memory_leaks.py +++ b/tests/test_memory_leaks.py @@ -9,10 +9,16 @@ import io import datetime import gc +import sys import pytest import rapidjson as rj +pytestmark = pytest.mark.skipif( + # 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")