Skip to content

Commit ea7afcb

Browse files
authored
Fix GIL being re-enabled by C++ extensions (#8059)
Towards fixing #7464 Currently, running the scikit-image tests fails on the free-threaded build because the GIL is enabled at runtime: ``` _______________________________________________ ERROR collecting build-install/usr/lib/python3.14t/site-packages/skimage/feature/censure.py ________________________________________________ build-install/usr/lib/python3.14t/site-packages/skimage/feature/censure.py:6: in <module> from ..morphology import octagon, star build-install/usr/lib/python3.14t/site-packages/skimage/morphology/__init__.py:28: in <module> from ._skeletonize import medial_axis, skeletonize, thin build-install/usr/lib/python3.14t/site-packages/skimage/morphology/_skeletonize.py:10: in <module> from ._skeletonize_lee_cy import _compute_thin_image E RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'skimage.morphology._skeletonize_lee_cy', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0. ``` This is happening because the `cython_args` aren't being passed to the C++ codegen function in the project's meson configuration. I changed the name of the variable to `cython_codegen_args` to make it a little clearer it has nothing to do with `cython_c_args` and `cython_cpp_args`, which are passed to e.g. clang and not Cython. I also made `cython_gen_cpp` use the same arguments as `cython_gen`. `c_undefined_ok` is unused, so I deleted it. Finally, I added a regression test for this in the project's `conftest.py`, following a similar check I added to numpy's `conftest.py` a while back. I also added a 3.14t testing job and deleted the 3.13t job and special testing logic. All of that is unnecessary now. I will follow up after this to make the 3.14t testing job use pytest-run-parallel, superseding #7678.
1 parent 89009eb commit ea7afcb

3 files changed

Lines changed: 26 additions & 56 deletions

File tree

.github/workflows/test-linux.yaml

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
# Ensure that a wheel builder finishes even if another fails
2323
fail-fast: false
2424
matrix:
25-
python-version: ["3.11", "3.13", "3.14"]
25+
python-version: ["3.11", "3.13", "3.14", "3.14t"]
2626

2727
# Dependency options for setup-test-env.sh
2828
MINIMUM_REQUIREMENTS: [0]
@@ -131,55 +131,9 @@ jobs:
131131
run: |
132132
asv check -v -E existing
133133
134-
test_skimage_linux_free_threaded:
135-
name: linux-cp313t-default
136-
runs-on: ubuntu-latest
137-
138-
steps:
139-
- name: Checkout scikit-image
140-
uses: actions/checkout@v5
141-
with:
142-
persist-credentials: false
143-
144-
# TODO: replace with setup-python when there is support
145-
- uses: deadsnakes/action@e640ac8743173a67cca4d7d77cd837e514bf98e8 # v3.2.0
146-
with:
147-
python-version: "3.13"
148-
nogil: true
149-
150-
- name: Install build dependencies
151-
# See special clause inside setup-build-env.sh that detects when
152-
# free threaded build is used, and then installs dependencies
153-
# from nightly wheels
154-
env:
155-
PIP_FLAGS: "--pre"
156-
run: |
157-
source .github/scripts/setup-build-env.sh
158-
159-
- name: Build and install
160-
run: |
161-
pip install -v --no-build-isolation .
162-
163-
- name: Install test dependencies
164-
# See special clause inside setup-test-env.sh that detects when
165-
# free threaded build is used, and then installs dependencies
166-
# from nightly wheels
167-
env:
168-
PIP_FLAGS: "--pre"
169-
run: |
170-
source .github/scripts/setup-test-env.sh
171-
172-
- name: Run tests
173-
env:
174-
PYTHON_GIL: 0
175-
# A lazy loader configuration parameter
176-
EAGER_IMPORT: ${{ matrix.EAGER_IMPORT }}
177-
run: |
178-
$PYTEST --doctest-plus --showlocals --pyargs skimage ./tests
179-
180134
report-failures:
181135
runs-on: ubuntu-latest
182-
needs: [test_skimage_linux, test_skimage_linux_free_threaded]
136+
needs: test_skimage_linux
183137
if: ${{ always() && github.ref == 'refs/heads/main' && contains(needs.*.result, 'failure') }}
184138
permissions:
185139
issues: write

src/skimage/meson.build

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,23 +128,20 @@ py3.install_sources(
128128
cython_cli = find_program('_build_utils/cythoner.py')
129129
cy = meson.get_compiler('cython')
130130

131-
cython_args = ['@INPUT@', '@OUTPUT@']
131+
cython_codegen_args = ['@INPUT@', '@OUTPUT@']
132+
132133
if cy.version().version_compare('>=3.1.0')
133-
cython_args += ['-Xfreethreading_compatible=True']
134+
cython_codegen_args += ['-Xfreethreading_compatible=True']
134135
endif
135136

136-
cython_cpp_args = cython_args + ['--cplus']
137-
138137
cython_gen = generator(cython_cli,
139-
arguments : cython_args,
138+
arguments : cython_codegen_args,
140139
output : '@BASENAME@.c')
141140

142141
cython_gen_cpp = generator(cython_cli,
143-
arguments : ['@INPUT@', '@OUTPUT@', '--cplus'],
142+
arguments : cython_codegen_args + ['--cplus'],
144143
output : '@BASENAME@.cpp')
145144

146-
c_undefined_ok = ['-Wno-maybe-uninitialized']
147-
148145
# Suppress warning for deprecated Numpy API.
149146
# (Suppress warning messages emitted by #warning directives).
150147
# Replace with numpy_nodepr_api after Cython 3.0 is out

tests/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
from datetime import timedelta
66
import os
77
import sys
8+
import sysconfig
89
from pytest_pretty import CustomTerminalReporter
910

1011
from _pytest.terminal import TerminalReporter
1112
from _pytest.pathlib import bestrelpath
1213

1314

15+
FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
16+
GIL_ENABLED_AT_START = getattr(sys, "_is_gil_enabled", lambda: True)()
17+
18+
1419
class SKTerminalReporter(CustomTerminalReporter):
1520
"""Custom terminal reporter to display test runtimes.
1621
@@ -59,3 +64,17 @@ def pytest_configure(config: pytest.Config) -> None:
5964
custom_reporter._session = standard_reporter._session
6065
config.pluginmanager.unregister(standard_reporter)
6166
config.pluginmanager.register(custom_reporter, 'terminalreporter')
67+
68+
69+
def pytest_terminal_summary(terminalreporter, exitstatus, config):
70+
if FREE_THREADED_BUILD and not GIL_ENABLED_AT_START and sys._is_gil_enabled():
71+
tr = terminalreporter
72+
tr.ensure_newline()
73+
tr.section("GIL re-enabled", sep="=", red=True, bold=True)
74+
tr.line("The GIL was re-enabled at runtime during the tests.")
75+
tr.line("This can happen with no test failures if the RuntimeWarning")
76+
tr.line("raised by Python when this happens is filtered by a test.")
77+
tr.line("")
78+
tr.line("Please ensure all new C and C++ extensions declare support")
79+
tr.line("for running without the GIL.")
80+
pytest.exit("GIL re-enabled during tests", returncode=1)

0 commit comments

Comments
 (0)