Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/test_with_free_threaded_python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
# Runs tests suite with free threaded python.
#
###
name: test free threaded python

on:
pull_request:
workflow_dispatch:
push:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
FORCE_COLOR: true

jobs:

free_threaded:
name: 'Test with ${{ matrix.py }} on ${{ matrix.os }}'

runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
py: [3.14t]
os: [macos-latest, ubuntu-latest]
env: [free_threaded]

steps:
- uses: actions/checkout@v6

- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7

- name: Setup python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.py }}
allow-prereleases: true

- name: Install dependencies
run: |
uv venv -p 3.14t
source .venv/bin/activate
uv pip install -e '.[test,dicomfs,dicom,viewers,indexed_gzip,spm,zstd]'
uv pip install pytest-run-parallel

- name: Run test suite
# run on several threads
# this may take a bit longer with fewer threads
# but as some tests can be flaky,
# they will more systematically fail with more threads
id: test_free_threaded
run: |
source .venv/bin/activate
PYTEST_RUN_PARALLEL_VERBOSE=1
N_THREADS=16
pytest -ra --strict-config --strict-markers --showlocals --capture=no --parallel-threads $N_THREADS nibabel
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,15 @@ for_testing/
# Generated by setuptools_scm #
###############################
_version.py

# files generated by tests #
############################
another_image.nii
another_image.npy
image.hdr
stream
streamlines.trk
test.dtseries.nii
test.hdr
test.img
test.nii
Comment on lines +85 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with this, but it would be good to figure out why they are being generated in the CWD and fix that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

want me to do this in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struggling to actually "regenerate" those files: not sure which tests are supposed to fail to get them

2 changes: 2 additions & 0 deletions nibabel/cifti2/tests/test_cifti2io_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ def test_pixdim_checks(self):
hdr['pixdim'][i] = -1
assert self._dxer(hdr) == self._pixdim_message

@pytest.mark.thread_unsafe
def test_nifti_qfac_checks(self):
# Test qfac is 1 or -1 or 0
hdr = self.header_class()
Expand All @@ -438,6 +439,7 @@ def test_nifti_qfac_checks(self):
assert fhdr['pixdim'][0] == 1
assert message == 'pixdim[0] (qfac) should be 1 (default) or 0 or -1; setting qfac to 1'

@pytest.mark.thread_unsafe
def test_pixdim_log_checks(self):
# pixdim can be zero or positive
HC = self.header_class
Expand Down
12 changes: 12 additions & 0 deletions nibabel/cifti2/tests/test_new_cifti2.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def check_series_map(mapping):
assert mapping.series_unit == 'SECOND'


@pytest.mark.thread_unsafe
def test_dtseries():
series_map = create_series_map((0,))
geometry_map = create_geometry_map((1,))
Expand All @@ -308,6 +309,7 @@ def test_dtseries():
del img2


@pytest.mark.thread_unsafe
def test_dscalar():
scalar_map = create_scalar_map((0,))
geometry_map = create_geometry_map((1,))
Expand All @@ -329,6 +331,7 @@ def test_dscalar():
del img2


@pytest.mark.thread_unsafe
def test_dlabel():
label_map = create_label_map((0,))
geometry_map = create_geometry_map((1,))
Expand All @@ -350,6 +353,7 @@ def test_dlabel():
del img2


@pytest.mark.thread_unsafe
def test_dconn():
mapping = create_geometry_map((0, 1))
matrix = ci.Cifti2Matrix()
Expand All @@ -370,6 +374,7 @@ def test_dconn():
del img2


@pytest.mark.thread_unsafe
def test_ptseries():
series_map = create_series_map((0,))
parcel_map = create_parcel_map((1,))
Expand All @@ -391,6 +396,7 @@ def test_ptseries():
del img2


@pytest.mark.thread_unsafe
def test_pscalar():
scalar_map = create_scalar_map((0,))
parcel_map = create_parcel_map((1,))
Expand All @@ -412,6 +418,7 @@ def test_pscalar():
del img2


@pytest.mark.thread_unsafe
def test_pdconn():
geometry_map = create_geometry_map((0,))
parcel_map = create_parcel_map((1,))
Expand All @@ -433,6 +440,7 @@ def test_pdconn():
del img2


@pytest.mark.thread_unsafe
def test_dpconn():
parcel_map = create_parcel_map((0,))
geometry_map = create_geometry_map((1,))
Expand All @@ -454,6 +462,7 @@ def test_dpconn():
del img2


@pytest.mark.thread_unsafe
def test_plabel():
label_map = create_label_map((0,))
parcel_map = create_parcel_map((1,))
Expand All @@ -474,6 +483,7 @@ def test_plabel():
del img2


@pytest.mark.thread_unsafe
def test_pconn():
mapping = create_parcel_map((0, 1))
matrix = ci.Cifti2Matrix()
Expand All @@ -494,6 +504,7 @@ def test_pconn():
del img2


@pytest.mark.thread_unsafe
def test_pconnseries():
parcel_map = create_parcel_map((0, 1))
series_map = create_series_map((2,))
Expand All @@ -517,6 +528,7 @@ def test_pconnseries():
del img2


@pytest.mark.thread_unsafe
def test_pconnscalar():
parcel_map = create_parcel_map((0, 1))
scalar_map = create_scalar_map((2,))
Expand Down
5 changes: 5 additions & 0 deletions nibabel/freesurfer/tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def test_morph_data():
assert np.array_equal(curv2, curv)


@pytest.mark.thread_unsafe
def test_write_morph_data():
"""Test write_morph_data edge cases"""
values = np.arange(20, dtype='>f4')
Expand Down Expand Up @@ -211,6 +212,7 @@ def test_annot():
assert names == names2


@pytest.mark.thread_unsafe
def test_read_write_annot():
"""Test generating .annot file and reading it back."""
# This annot file will store a LUT for a mesh made of 10 vertices, with
Expand Down Expand Up @@ -244,6 +246,7 @@ def test_read_write_annot():
assert names2 == names


@pytest.mark.thread_unsafe
def test_write_annot_fill_ctab():
"""Test the `fill_ctab` parameter to :func:`.write_annot`."""
nvertices = 10
Expand Down Expand Up @@ -291,6 +294,7 @@ def test_write_annot_fill_ctab():
assert names2 == names


@pytest.mark.thread_unsafe
def test_read_annot_old_format():
"""Test reading an old-style .annot file."""

Expand Down Expand Up @@ -353,6 +357,7 @@ def test_label():
assert len(labels) == len(scalars)


@pytest.mark.thread_unsafe
def test_write_annot_maxstruct():
"""Test writing ANNOT files with repeated labels"""
with InTemporaryDirectory():
Expand Down
3 changes: 3 additions & 0 deletions nibabel/freesurfer/tests/test_mghformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def test_read_mgh():
assert_almost_equal(v[1, 2, 3, 1], 0.0018, 4)


@pytest.mark.thread_unsafe
def test_write_mgh():
# write our data to a tmp file
v = np.arange(120)
Expand Down Expand Up @@ -121,6 +122,7 @@ def test_write_mgh():
assert_almost_equal(dat, v, 7)


@pytest.mark.thread_unsafe
def test_write_noaffine_mgh():
# now just save the image without the vox2ras transform
# and see if it uses the default values to save
Expand Down Expand Up @@ -188,6 +190,7 @@ def test_bad_dtype_mgh():
bad_dtype_mgh()


@pytest.mark.thread_unsafe
def test_filename_exts():
# Test acceptable filename extensions
v = np.ones((7, 13, 3, 22), np.uint8)
Expand Down
1 change: 1 addition & 0 deletions nibabel/gifti/tests/test_gifti.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ def test_darray_dtype_coercion_failures():
assert_array_equal(da_copy.data, da.data)


@pytest.mark.thread_unsafe
def test_gifti_file_close(recwarn):
gii = load(get_test_data('gifti', 'ascii.gii'))
with InTemporaryDirectory():
Expand Down
9 changes: 9 additions & 0 deletions nibabel/gifti/tests/test_parse_gifti_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def test_load_metadata():
assert img.version == '1.0'


@pytest.mark.thread_unsafe
def test_load_dataarray1():
img1 = load(DATA_FILE1)
# Round trip
Expand All @@ -247,6 +248,7 @@ def test_load_dataarray1():
assert xform_codes.niistring[img.darrays[0].coordsys.xformspace] == 'NIFTI_XFORM_TALAIRACH'


@pytest.mark.thread_unsafe
def test_load_dataarray2():
img2 = load(DATA_FILE2)
# Round trip
Expand All @@ -257,6 +259,7 @@ def test_load_dataarray2():
assert_array_almost_equal(img.darrays[0].data[:10], DATA_FILE2_darr1)


@pytest.mark.thread_unsafe
def test_load_dataarray3():
img3 = load(DATA_FILE3)
with InTemporaryDirectory():
Expand All @@ -266,6 +269,7 @@ def test_load_dataarray3():
assert_array_almost_equal(img.darrays[0].data[30:50], DATA_FILE3_darr1)


@pytest.mark.thread_unsafe
def test_load_dataarray4():
img4 = load(DATA_FILE4)
# Round trip
Expand All @@ -285,6 +289,7 @@ def test_dataarray5():
# Round trip tested below


@pytest.mark.thread_unsafe
def test_base64_written():
with InTemporaryDirectory():
with open(DATA_FILE5, 'rb') as fobj:
Expand Down Expand Up @@ -314,6 +319,7 @@ def test_base64_written():
assert_array_almost_equal(darrays[1].data, DATA_FILE5_darr2)


@pytest.mark.thread_unsafe
def test_readwritedata():
img = load(DATA_FILE2)
with InTemporaryDirectory():
Expand Down Expand Up @@ -357,6 +363,7 @@ def test_load_getbyintent():
assert da == []


@pytest.mark.thread_unsafe
def test_load_labeltable():
img6 = load(DATA_FILE6)
# Round trip
Expand All @@ -376,6 +383,7 @@ def test_load_labeltable():
assert img.labeltable.labels[1].alpha == 1


@pytest.mark.thread_unsafe
def test_parse_dataarrays():
fn = 'bad_daa.gii'
img = gi.GiftiImage()
Expand Down Expand Up @@ -441,6 +449,7 @@ def test_parse_with_memmap_fallback():
assert_array_almost_equal(img2.darrays[1].data, DATA_FILE7_darr2)


@pytest.mark.thread_unsafe
def test_external_file_failure_cases():
# external file cannot be found
with InTemporaryDirectory() as tmpdir:
Expand Down
1 change: 1 addition & 0 deletions nibabel/nicom/tests/test_csareader.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def test_csa_len0():
assert len(tags) == 44


@pytest.mark.thread_unsafe
def test_csa_nitem():
# testing csa.read's ability to raise an error when n_items >= 200
with pytest.raises(csa.CSAReadError):
Expand Down
1 change: 1 addition & 0 deletions nibabel/nicom/tests/test_dicomwrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ class TestMultiFrameWrapper(TestCase):
WRAPCLASS = didw.MultiframeWrapper

@dicom_test
@pytest.mark.thread_unsafe
def test_shape(self):
# Check the shape algorithm
fake_mf = deepcopy(self.MINIMAL_MF)
Expand Down
6 changes: 6 additions & 0 deletions nibabel/streamlines/tests/test_streamlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def test_load_complex_file(self):
with pytest.warns(Warning) if lazy_load else error_warnings():
assert_tractogram_equal(tfile.tractogram, tractogram)

@pytest.mark.thread_unsafe
def test_save_tractogram_file(self):
tractogram = Tractogram(DATA['streamlines'], affine_to_rasmm=np.eye(4))
trk_file = trk.TrkFile(tractogram)
Expand All @@ -232,6 +233,7 @@ def test_save_tractogram_file(self):
tfile = nib.streamlines.load('dummy.trk', lazy_load=False)
assert_tractogram_equal(tfile.tractogram, tractogram)

@pytest.mark.thread_unsafe
def test_save_empty_file(self):
tractogram = Tractogram(affine_to_rasmm=np.eye(4))
for ext in FORMATS:
Expand All @@ -241,6 +243,7 @@ def test_save_empty_file(self):
tfile = nib.streamlines.load(filename, lazy_load=False)
assert_tractogram_equal(tfile.tractogram, tractogram)

@pytest.mark.thread_unsafe
def test_save_simple_file(self):
tractogram = Tractogram(DATA['streamlines'], affine_to_rasmm=np.eye(4))
for ext in FORMATS:
Expand All @@ -250,6 +253,7 @@ def test_save_simple_file(self):
tfile = nib.streamlines.load(filename, lazy_load=False)
assert_tractogram_equal(tfile.tractogram, tractogram)

@pytest.mark.thread_unsafe
def test_save_complex_file(self):
complex_tractogram = Tractogram(
DATA['streamlines'],
Expand Down Expand Up @@ -286,6 +290,7 @@ def test_save_complex_file(self):
tfile = nib.streamlines.load(filename, lazy_load=False)
assert_tractogram_equal(tfile.tractogram, tractogram)

@pytest.mark.thread_unsafe
def test_save_sliced_tractogram(self):
tractogram = Tractogram(DATA['streamlines'], affine_to_rasmm=np.eye(4))
original_tractogram = tractogram.copy()
Expand All @@ -306,6 +311,7 @@ def test_save_unknown_format(self):
with pytest.raises(ValueError):
nib.streamlines.save(Tractogram(), '')

@pytest.mark.thread_unsafe
def test_save_from_generator(self):
tractogram = Tractogram(DATA['streamlines'], affine_to_rasmm=np.eye(4))

Expand Down
Loading
Loading