ENH: Free threading support
Closes #198. Basically:
- Uses a critical section for both the
globalvariables in Cython- With the slow-and-fast path structuring recommended in the ft-guide
- Signals to Cython that the code is indeed safe for
ftbuilds - Splits the
globalsections out into their own files since earliercythonversions cannot handlecritical_section
Draft until I get the CI right for
- [ ] testing and
- [ ] building wheels.
The testing bit is mostly taken from this recent NumPy PR, but since there aren't any tests which use threading (nor AFAIK should there be, the NDIndex objects seem pretty immutable), might not even need the TSAN run.
Leaving the thread sanitizer race warnings here for posterity..
Races
==================
WARNING: ThreadSanitizer: data race (pid=933)
Read of size 8 at 0x7f0890502298 by main thread:
#0 PyUnstable_InterpreterFrame_GetLine /tmp/python-build.20250221213836.92/Python-3.13.2/Python/frame.c:152:16 (libpython3.13t.so.1.0+0x3769a8) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#1 PyFrame_GetLineNumber /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/frameobject.c:901:12 (libpython3.13t.so.1.0+0x16368d) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#2 frame_getlineno /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/frameobject.c:907:18 (libpython3.13t.so.1.0+0x16368d)
#3 getset_get /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/descrobject.c:193:16 (libpython3.13t.so.1.0+0x136dc8) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#4 _PyObject_GenericGetAttrWithDict /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/object.c:1665:19 (libpython3.13t.so.1.0+0x1cd525) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#5 PyObject_GenericGetAttr /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/object.c:1747:12 (libpython3.13t.so.1.0+0x1cd3[72](https://github.com/Quansight-Labs/ndindex/actions/runs/14653425582/job/41124094812?pr=199#step:8:73)) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#6 PyObject_GetAttr /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/object.c:1261:18 (libpython3.13t.so.1.0+0x1cbc97) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#7 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:3766:28 (libpython3.13t.so.1.0+0x3225d3) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#8 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x152aa8) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#9 gen_send_ex2 /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/genobject.c:229:24 (libpython3.13t.so.1.0+0x152aa8)
#10 gen_iternext /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/genobject.c:589:9 (libpython3.13t.so.1.0+0x150299) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#11 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:2786:24 (libpython3.13t.so.1.0+0x31f0ef) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#12 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x152aa8) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#13 gen_send_ex2 /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/genobject.c:229:24 (libpython3.13t.so.1.0+0x152aa8)
#14 gen_iternext /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/genobject.c:589:9 (libpython3.13t.so.1.0+0x150299) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#15 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:2786:24 (libpython3.13t.so.1.0+0x31f0ef) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#16 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x314599) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#17 _PyEval_Vector /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval.c:1812:12 (libpython3.13t.so.1.0+0x314599)
#18 _PyFunction_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c (libpython3.13t.so.1.0+0x122b51) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#19 _PyVectorcall_Call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:2[73](https://github.com/Quansight-Labs/ndindex/actions/runs/14653425582/job/41124094812?pr=199#step:8:74):16 (libpython3.13t.so.1.0+0x1227c3) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#20 _PyObject_Call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:348:16 (libpython3.13t.so.1.0+0x1227c3)
#21 _PyErr_CheckSignalsTstate /tmp/python-build.20250221213836.92/Python-3.13.2/./Modules/signalmodule.c:1855:22 (libpython3.13t.so.1.0+0x42dbe3) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#22 handle_signals /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval_gil.c:828:9 (libpython3.13t.so.1.0+0x385f87) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#23 _PyEval_MakePendingCalls /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval_gil.c:1045:15 (libpython3.13t.so.1.0+0x385f87)
#24 Py_MakePendingCalls /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval_gil.c:1073:12 (libpython3.13t.so.1.0+0x385f87)
#25 ThreadHandle_join /tmp/python-build.20250221213836.92/Python-3.13.2/./Modules/_threadmodule.c:515:17 (libpython3.13t.so.1.0+0x499198) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#26 PyThreadHandleObject_join /tmp/python-build.20250221213836.92/Python-3.13.2/./Modules/_threadmodule.c:643:9 (libpython3.13t.so.1.0+0x4999b3) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#37 _PyObject_VectorcallTstate /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_call.h:166:16 (libpython3.13t.so.1.0+0x12258a) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#38 PyObject_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:327:12 (libpython3.13t.so.1.0+0x12258a)
#39 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:1502:19 (libpython3.13t.so.1.0+0x31a98b) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#40 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x314599) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#41 _PyEval_Vector /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval.c:1812:12 (libpython3.13t.so.1.0+0x314599)
#42 _PyFunction_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c (libpython3.13t.so.1.0+0x122b51) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#43 _PyObject_VectorcallDictTstate /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:146:15 (libpython3.13t.so.1.0+0x121680) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#44 _PyObject_Call_Prepend /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:504:24 (libpython3.13t.so.1.0+0x1231a6) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#45 slot_tp_call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/typeobject.c:9539:15 (libpython3.13t.so.1.0+0x22f76a) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#46 _PyObject_MakeTpCall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:242:18 (libpython3.13t.so.1.0+0x12192e) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#47 _PyObject_VectorcallTstate /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_call.h:166:16 (libpython3.13t.so.1.0+0x12258a) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#48 PyObject_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:327:12 (libpython3.13t.so.1.0+0x12258a)
#49 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:1502:19 (libpython3.13t.so.1.0+0x31a98b) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#50 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x314599) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#51 _PyEval_Vector /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval.c:1812:12 (libpython3.13t.so.1.0+0x314599)
#52 _PyFunction_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c (libpython3.13t.so.1.0+0x122b51) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#53 _PyObject_VectorcallDictTstate /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:146:15 (libpython3.13t.so.1.0+0x121680) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#54 _PyObject_Call_Prepend /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:504:24 (libpython3.13t.so.1.0+0x1231a6) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#55 slot_tp_call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/typeobject.c:9539:15 (libpython3.13t.so.1.0+0x22f76a) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#56 _PyObject_MakeTpCall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:242:18 (libpython3.13t.so.1.0+0x12192e) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#57 _PyObject_VectorcallTstate /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_call.h:166:16 (libpython3.13t.so.1.0+0x12258a) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#58 PyObject_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:327:12 (libpython3.13t.so.1.0+0x12258a)
#59 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:1502:19 (libpython3.13t.so.1.0+0x31a98b) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#60 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x314284) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#61 _PyEval_Vector /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval.c:1812:12 (libpython3.13t.so.1.0+0x314284)
#62 PyEval_EvalCode /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval.c:602:21 (libpython3.13t.so.1.0+0x314284)
#63 builtin_exec_impl /tmp/python-build.20250221213836.92/Python-3.13.2/Python/bltinmodule.c:1145:17 (libpython3.13t.so.1.0+0x30eb96) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#64 builtin_exec /tmp/python-build.20250221213836.92/Python-3.13.2/Python/clinic/bltinmodule.c.h:556:20 (libpython3.13t.so.1.0+0x30eb96)
#65 cfunction_vectorcall_FASTCALL_KEYWORDS /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/methodobject.c:441:24 (libpython3.13t.so.1.0+0x1c1853) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#66 _PyObject_VectorcallTstate /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_call.h:168:11 (libpython3.13t.so.1.0+0x1224cc) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#67 PyObject_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:327:12 (libpython3.13t.so.1.0+0x1224cc)
#68 _PyEval_EvalFrameDefault /tmp/python-build.20250221213836.92/Python-3.13.2/Python/generated_cases.c.h:813:23 (libpython3.13t.so.1.0+0x317d5b) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#69 _PyEval_EvalFrame /tmp/python-build.20250221213836.92/Python-3.13.2/./Include/internal/pycore_ceval.h:119:16 (libpython3.13t.so.1.0+0x314599) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#70 _PyEval_Vector /tmp/python-build.20250221213836.92/Python-3.13.2/Python/ceval.c:1812:12 (libpython3.13t.so.1.0+0x314599)
#71 _PyFunction_Vectorcall /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c (libpython3.13t.so.1.0+0x122b51) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#72 _PyVectorcall_Call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:273:16 (libpython3.13t.so.1.0+0x1227c3) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#73 _PyObject_Call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:348:16 (libpython3.13t.so.1.0+0x1227c3)
#[74](https://github.com/Quansight-Labs/ndindex/actions/runs/14653425582/job/41124094812?pr=199#step:8:75) PyObject_Call /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/call.c:373:12 (libpython3.13t.so.1.0+0x122847) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#75 pymain_run_module /tmp/python-build.20250221213836.92/Python-3.13.2/Modules/main.c:349:14 (libpython3.13t.so.1.0+0x40be9a) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#76 pymain_run_python /tmp/python-build.20250221213836.92/Python-3.13.2/Modules/main.c:691:21 (libpython3.13t.so.1.0+0x40b5bb) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#77 Py_RunMain /tmp/python-build.20250221213836.92/Python-3.13.2/Modules/main.c:7[76](https://github.com/Quansight-Labs/ndindex/actions/runs/14653425582/job/41124094812?pr=199#step:8:77):5 (libpython3.13t.so.1.0+0x40b5bb)
#78 pymain_main /tmp/python-build.20250221213836.92/Python-3.13.2/Modules/main.c:806:12 (libpython3.13t.so.1.0+0x40bc68) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#79 Py_BytesMain /tmp/python-build.20250221213836.92/Python-3.13.2/Modules/main.c:830:12 (libpython3.13t.so.1.0+0x40bcec) (BuildId: 9359595b9a1895ecc20bf063638092944065b5c6)
#80 main /tmp/python-build.20250221213836.92/Python-3.13.2/./Programs/python.c:15:12 (python3.13+0xe8c9b) (BuildId: 3cc7488ffc292c8dd5400212ea3a107a04537e72)
SUMMARY: ThreadSanitizer: data race /tmp/python-build.20250221213836.92/Python-3.13.2/Objects/object.c:2465:23 in new_reference
==================
Rather baffling doc building error, also in CircleCI:
Doc build errors
/home/runner/work/ndindex/ndindex/docs/indexing-guide/multidimensional-indices/boolean-arrays.md:176: ERROR: Error in "plot" directive:
unknown option: "output-base-name".
.. plot::
:context: reset
:include-source: True
:output-base-name: plot-{counter}
:alt: A plot of 4*x*np.sin(x) - x**2/4 - 2*x from -10 to 10. The curve crosses the x-axis several times at irregular intervals.
:caption: Plot of :math:`y = 4x\sin(x) - \frac{x^2}{4} - 2x`
>>> import matplotlib.pyplot as plt
>>> x = np.linspace(-10, 10, 10000) # 10000 evenly spaced points between -10 and 10
>>> y = 4*x*np.sin(x) - x**2/4 - 2*x # our function
>>> plt.scatter(x, y, marker=',', s=1)
<matplotlib.collections.PathCollection object at ...> [docutils]
/home/runner/work/ndindex/ndindex/docs/indexing-guide/multidimensional-indices/boolean-arrays.md:194: ERROR: Error in "plot" directive:
unknown option: "output-base-name".
.. plot::
:context: close-figs
:include-source: True
:output-base-name: plot-{counter}
:alt: A plot of only the parts of 4*x*np.sin(x) - x**2/4 - 2*x that are above the x-axis.
:caption: Plot of :math:`y = 4x\sin(x) - \frac{x^2}{4} - 2x` where :math:`y > 0`
>>> plt.scatter(x[y > 0], y[y > 0], marker=',', s=1)
<matplotlib.collections.PathCollection object at ...> [docutils]
/home/runner/work/ndindex/ndindex/docs/indexing-guide/multidimensional-indices/boolean-arrays.md:387: ERROR: Error in "plot" directive:
unknown option: "output-base-name".
.. plot::
:context: reset
:include-source: True
:output-base-name: astronaut-{counter}
:alt: An image of an astronaut, which is represented as a shape (512, 512, 3) array.
>>> def imshow(image, title):
... import matplotlib.pyplot as plt
... plt.axis('off')
... plt.title(title)
... plt.imshow(image)
>>> from skimage.data import astronaut
>>> image = astronaut()
>>> image.shape
(512, 512, 3)
>>> imshow(image, "Original Image") [docutils]
/home/runner/work/ndindex/ndindex/docs/indexing-guide/multidimensional-indices/boolean-arrays.md:[41](https://github.com/Quansight-Labs/ndindex/actions/runs/14654158449/job/41126132114?pr=199#step:6:42)0: ERROR: Error in "plot" directive:
unknown option: "output-base-name".
Test failures are actually coverage issues so far (TSAN aside).
The docs build error is because the docs require https://github.com/matplotlib/matplotlib/pull/28187. I thought I had the CI set up to use that PR when building.
the NDIndex objects seem pretty immutable
NDIndex objects are all designed to be 100% immutable. Even the objects that wrap arrays (which aren't yet Cythonized) copy them and set them to be read-only so that they can be immutable and hashable.
Oh, regarding the docs thing, I guess I changed the option name in the matplotlib PR and never updated it here.
I'm not sure whether the TSAN tests are hanging or if they're just really slow. I would avoid TSAN if it adds a ton of overhead to the tests.
ASAN will likely be a lot faster, although it won't catch as many things. It will catch serious issues, though.
The docs build should be fixed on main.
A sanity check for the CI approach here: I did not expect a TSan job to be added, or ASan. We haven't done that in any project except for NumPy (and maybe PyO3?). It's probably only going to catch things in other projects (CPython, NumPy, Cython) and be worth way more trouble than it's worth - all I'd expect is future maintenance headaches. The way I'd deal with this is to run it once in a Docker container, which is quick and easy with https://github.com/nascheme/cpython_sanity, and if it turns up nothing then check the box that it was clean in a comment or the PR description here.
The standard approach to testing is to add a single cp313t job with pytest-run-parallel, which is a lot easier to do and maintain.
This is starting to look close. call_once is new in Cython 3.1 so needs protecting based on version number.
I'd also suggest putting the unrelated minor changes like removing the classifier from setup.py and the .gitignore/.coveragerc tweaks in a separate PR that can be merged straight away.
@HaoZeke I thought the Cython code was already fixed, with a nice and small diff. Why is it now completely duplicated again? I think all you needed was:
if Version(cython_version) >= Version("3.1.0a0"):
compiler_directives["freethreading_compatible"] = True
The comment-directives at the top of the .pyx files are also unnecessary.
@HaoZeke I thought the Cython code was already fixed, with a nice and small diff. Why is it now completely duplicated again? I think all you needed was:
if Version(cython_version) >= Version("3.1.0a0"): compiler_directives["freethreading_compatible"] = TrueThe comment-directives at the top of the .pyx files are also unnecessary.
The diff for working with free threading is nice and small. The duplication is for BC because the resulting file cannot be compiled with older Cython versions.
i.e.:
Successfully installed cython-3.0.12 setuptools-80.3.1
Installing build dependencies: finished with status 'done'
Getting requirements to build wheel: started
Running command Getting requirements to build wheel
Error compiling Cython file:
------------------------------------------------------------
...
# distutils: language = c++
import sys
from _import_cache import _NUMPY_IMPORTED, _NUMPY_BOOL, _NUMPY_NDARRAY
from libcpp.mutex cimport once_flag, call_once
^
------------------------------------------------------------
ndindex/_tuple.pyx:7:0: 'libcpp/mutex.pxd' not found
Error compiling Cython file:
------------------------------------------------------------
...
# distutils: language = c++
import sys
from _import_cache import _NUMPY_IMPORTED, _NUMPY_BOOL, _NUMPY_NDARRAY
from libcpp.mutex cimport once_flag, call_once
^
------------------------------------------------------------
ndindex/_tuple.pyx:7:0: 'libcpp/mutex/once_flag.pxd' not found
Error compiling Cython file:
------------------------------------------------------------
...
# distutils: language = c++
import sys
from _import_cache import _NUMPY_IMPORTED, _NUMPY_BOOL, _NUMPY_NDARRAY
from libcpp.mutex cimport once_flag, call_once
^
------------------------------------------------------------
ndindex/_tuple.pyx:7:0: 'libcpp/mutex/call_once.pxd' not found
Error compiling Cython file:
------------------------------------------------------------
...
# We cannot just add these imports to the top because of circular import
# issues. We can put them inside the constructor, but then they create a big
# performance bottleneck.
# Uses C++ primitives for preventing races
cdef once_flag _lazy_import_lock
^
------------------------------------------------------------
ndindex/_tuple.pyx:[61](https://github.com/Quansight-Labs/ndindex/actions/runs/14826373995/job/41619982673#step:5:62):5: 'once_flag' is not a type identifier
Error compiling Cython file:
------------------------------------------------------------
...
_broadcast_shapes = broadcast_shapes
_BroadcastError = BroadcastError
cdef int _is_boolean_scalar(object idx):
cdef object BooleanArray
call_once(_lazy_import_lock, _lazy_import)
^
------------------------------------------------------------
ndindex/_tuple.pyx:83:4: 'call_once' is not a constant, variable or function identifier
Error compiling Cython file:
------------------------------------------------------------
...
int array_block_stop = 0
int has_array = 0
int has_boolean_scalar = 0
object arg, newarg
call_once(_lazy_import_lock, _lazy_import)
^
------------------------------------------------------------
ndindex/_tuple.pyx:103:8: 'call_once' is not a constant, variable or function identifier
and similar from the CI.
The fix (which unfortunately duplicates the code) is to have conditional compilation based on the Cython version, which is done in this commit with:
if Version(cython_version) >= Version("3.1.0a0"):
compiler_directives["freethreading_compatible"] = True
_tuple = Extension(
"ndindex._tuple", ["ndindex/_tuple_ft.pyx"],
define_macros=define_macros,
)
else:
_tuple = Extension(
"ndindex._tuple", ["ndindex/_tuple_gil.pyx"],
define_macros=define_macros,
)
The fix (which unfortunately duplicates the code) is to have conditional compilation based on the Cython version, which is done in this commit with:
The difference between the files is:
1c1,3
< # cython: freethreading_compatible = False
---
> # cython: freethreading_compatible = True
> # distutils: language = c++
>
3a6
> from libcpp.mutex cimport once_flag, call_once
13c16,18
< cdef void _lazy_import():
---
> # Uses C++ primitives for preventing races
> cdef once_flag _lazy_import_lock
> cdef void _lazy_import() noexcept with gil:
34c39,40
< _lazy_import()
---
> with nogil:
> call_once(_lazy_import_lock, _lazy_import)
54c60,61
< _lazy_import()
---
> with nogil:
> call_once(_lazy_import_lock, _lazy_import)
The # cython don't need to be different (use c++ mode always, and the free-threading directives are unnecessary because it's handled in setup.py already). So you're left with only a couple of lines. Duplicating everything for that feels wrong - a couple of ideas:
- Put the common code in a
_tuple.pxiandincludethat (see https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#the-include-statement-and-include-files) - Use
IF(see https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#conditional-statements). Yes it's deprecated, and no that isn't important - it can be removed again in a few months when requiring Cython>=3.1.0, and it won't go away any time soon. Example from pandas: https://github.com/pandas-dev/pandas/pull/60540).
- Put the common code in a
_tuple.pxiandincludethat (see https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#the-include-statement-and-include-files)- Use
IF(see
Good point, I managed to clean it up to minimize differences to main. AFAIK .pxi isn't good here since the code to be swapped out is part of a member function. The IF conditional compilation directive seems to be the only way through here since normal try-catch won't take cdef, might be something to point out to https://github.com/cython/cython/issues/4310.
Cython 3.1 came out today so all the special handling for that can be deleted.
I'm a bit flummoxed as to why the changes since https://github.com/Quansight-Labs/ndindex/pull/199/commits/24ae26c980196cf5713982562b509598ebcf7534 are causing so many coverage failures.
Half of this PR is now # pragma: no cover but other than that I guess its ready to go.
For reference, before this set of skips : https://github.com/Quansight-Labs/ndindex/pull/199/commits/3967b5d26da0db49b32ce6a77769d1dcef96b8c3
The only skips needed for https://github.com/Quansight-Labs/ndindex/pull/199/commits/24ae26c980196cf5713982562b509598ebcf7534 were https://github.com/Quansight-Labs/ndindex/pull/199/commits/f91f81e9d6fda37218084db227b6432f610566b1 and https://github.com/Quansight-Labs/ndindex/pull/199/commits/e8c9574226942c7149c7dc36007ba66f260fce59
Half of this PR is now
# pragma: no cover
It's happening on main as well (see gh-202), so it's unrelated to your changes it looks like. Adding a ton of pragma's can't be the right solution, it has to be some dependency that changed. Both hypothesis and Cython did releases on May 8th, so right before things started failing.
We should replace 3.13-dev with just 3.13 in the CI (we could also add 3.14-dev, but that can be done in a separate PR).
We should replace 3.13-dev with just 3.13 in the CI (we could also add 3.14-dev, but that can be done in a separate PR).
I'll take this along too.
It's too early for 3.14-dev by the way: that attempts to build numpy, scikit-image and maybe more things from source.
Oh actually, it needs some history rewriting since there's no squash-merge enabled. Let me just do that right now and get this over the finish line.