From fdd78adccc3301f1caec72fef869057d6a9cae6b Mon Sep 17 00:00:00 2001 From: Mark Tareshawty Date: Mon, 24 Aug 2026 00:16:24 -0400 Subject: [PATCH 1/2] python: support free-threaded CPython builds The extension was always built against the stable ABI (`Py_LIMITED_API` / `py_limited_api=True`). The limited API is not available on free-threaded CPython 3.13+ builds, because the limited API headers omit the atomic operations that thread-safe reference counting needs, so the module failed to build there. Only request the stable ABI (and the `abi3` wheel tag) when `Py_GIL_DISABLED` is unset. Switch the module to multi-phase initialisation so it can declare `Py_MOD_GIL_NOT_USED`; the binding only returns a pointer to the static language table and holds no mutable module state, so it does not need the GIL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/python/tree_sitter_ruby/binding.c | 14 +++++++++++--- setup.py | 8 ++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/bindings/python/tree_sitter_ruby/binding.c b/bindings/python/tree_sitter_ruby/binding.c index 6a02dfd1..2d01d92d 100644 --- a/bindings/python/tree_sitter_ruby/binding.c +++ b/bindings/python/tree_sitter_ruby/binding.c @@ -8,6 +8,13 @@ static PyObject* _binding_language(PyObject *Py_UNUSED(self), PyObject *Py_UNUSE return PyCapsule_New(tree_sitter_ruby(), "tree_sitter.Language", NULL); } +static struct PyModuleDef_Slot slots[] = { +#ifdef Py_GIL_DISABLED + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL} +}; + static PyMethodDef methods[] = { {"language", _binding_language, METH_NOARGS, "Get the tree-sitter language for this grammar."}, @@ -18,10 +25,11 @@ static struct PyModuleDef module = { .m_base = PyModuleDef_HEAD_INIT, .m_name = "_binding", .m_doc = NULL, - .m_size = -1, - .m_methods = methods + .m_size = 0, + .m_methods = methods, + .m_slots = slots, }; PyMODINIT_FUNC PyInit__binding(void) { - return PyModule_Create(&module); + return PyModuleDef_Init(&module); } diff --git a/setup.py b/setup.py index 2c0384fa..e65f9b37 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ from os.path import isdir, join from platform import system +from sysconfig import get_config_var from setuptools import Extension, find_packages, setup from setuptools.command.build import build @@ -17,7 +18,7 @@ def run(self): class BdistWheel(bdist_wheel): def get_tag(self): python, abi, platform = super().get_tag() - if python.startswith("cp"): + if python.startswith("cp") and not get_config_var("Py_GIL_DISABLED"): python, abi = "cp39", "abi3" return python, abi, platform @@ -46,12 +47,11 @@ def get_tag(self): "/utf-8", ], define_macros=[ - ("Py_LIMITED_API", "0x03090000"), ("PY_SSIZE_T_CLEAN", None), ("TREE_SITTER_HIDE_SYMBOLS", None), - ], + ] + ([("Py_LIMITED_API", "0x03090000")] if not get_config_var("Py_GIL_DISABLED") else []), include_dirs=["src"], - py_limited_api=True, + py_limited_api=not get_config_var("Py_GIL_DISABLED"), ) ], cmdclass={ From ac79bb41506044d1f5fe346769d001f4c8a21b06 Mon Sep 17 00:00:00 2001 From: Mark Tareshawty Date: Mon, 24 Aug 2026 10:39:39 -0400 Subject: [PATCH 2/2] Define Py_GIL_DISABLED for free-threaded Windows builds CPython defines this macro in pyconfig.h on POSIX free-threaded builds but not on Windows, where it must be passed to the compiler. Without it the guard around the Py_mod_gil slot in binding.c is false on Windows and the module ends up re-enabling the GIL at import. Hoist the free-threading check and the macro list to module level so the three places that depend on it cannot drift apart. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac21705f-befb-4606-9c83-657e3e9ea0dc --- setup.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index e65f9b37..ef6fcde6 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,25 @@ from wheel.bdist_wheel import bdist_wheel +# True when the interpreter running setup.py is a free-threaded build. Such a +# build cannot use the stable ABI, because the module has to declare the +# Py_mod_gil slot and that slot is not part of the limited API. +FREE_THREADED = bool(get_config_var("Py_GIL_DISABLED")) + +# CPython defines Py_GIL_DISABLED in pyconfig.h on POSIX free-threaded builds, +# but not on Windows, where it has to be passed to the compiler instead. Pass it +# unconditionally on free-threaded builds so binding.c sees the same macro +# everywhere; on POSIX this repeats the existing definition with the same value, +# which is a legal redefinition. +DEFINE_MACROS = [ + ("PY_SSIZE_T_CLEAN", None), + ("TREE_SITTER_HIDE_SYMBOLS", None), +] +DEFINE_MACROS += ( + [("Py_GIL_DISABLED", "1")] if FREE_THREADED else [("Py_LIMITED_API", "0x03090000")] +) + + class Build(build): def run(self): if isdir("queries"): @@ -18,7 +37,7 @@ def run(self): class BdistWheel(bdist_wheel): def get_tag(self): python, abi, platform = super().get_tag() - if python.startswith("cp") and not get_config_var("Py_GIL_DISABLED"): + if python.startswith("cp") and not FREE_THREADED: python, abi = "cp39", "abi3" return python, abi, platform @@ -46,12 +65,9 @@ def get_tag(self): "/std:c11", "/utf-8", ], - define_macros=[ - ("PY_SSIZE_T_CLEAN", None), - ("TREE_SITTER_HIDE_SYMBOLS", None), - ] + ([("Py_LIMITED_API", "0x03090000")] if not get_config_var("Py_GIL_DISABLED") else []), + define_macros=DEFINE_MACROS, include_dirs=["src"], - py_limited_api=not get_config_var("Py_GIL_DISABLED"), + py_limited_api=not FREE_THREADED, ) ], cmdclass={