diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 3672d17ce11..a5af6c4f2bd 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -94,17 +94,17 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] dependencies: [latest, pre] include: - - python-version: "3.9" + - python-version: "3.10" dependencies: min exclude: # Do not test pre-releases for versions out of SPEC0 - - python-version: "3.9" - dependencies: pre - python-version: "3.10" dependencies: pre + - python-version: "3.11" + dependencies: pre env: DEPENDS: ${{ matrix.dependencies }} diff --git a/niworkflows/func/tests/test_util.py b/niworkflows/func/tests/test_util.py index 534dc8b0ae0..5710e2c2e8a 100644 --- a/niworkflows/func/tests/test_util.py +++ b/niworkflows/func/tests/test_util.py @@ -74,7 +74,7 @@ bold_datasets = [[str((datapath / p).absolute()) for p in ds] for ds in bold_datasets] - parameters = zip(bold_datasets, exp_masks) + parameters = zip(bold_datasets, exp_masks, strict=False) if not bold_datasets: raise RuntimeError( diff --git a/niworkflows/interfaces/bids.py b/niworkflows/interfaces/bids.py index 27277e4ddf3..b01841fe823 100644 --- a/niworkflows/interfaces/bids.py +++ b/niworkflows/interfaces/bids.py @@ -25,7 +25,6 @@ import os import re import shutil -import sys from collections import defaultdict from contextlib import suppress from json import dumps, loads @@ -66,15 +65,6 @@ LOGGER = logging.getLogger('nipype.interface') -if sys.version_info < (3, 10): # PY39 - builtin_zip = zip - - def zip(*args, strict=False): # noqa: A001 - if strict and any(len(args[0]) != len(arg) for arg in args): - raise ValueError('strict_zip() requires all arguments to have the same length') - return builtin_zip(*args) - - def _none(): return None @@ -629,7 +619,7 @@ def _run_interface(self, runtime): self._results['out_path'] = dest_files self._results['out_meta'] = metadata - for i, (orig_file, dest_file) in enumerate(zip(in_file, dest_files)): + for i, (orig_file, dest_file) in enumerate(zip(in_file, dest_files, strict=False)): # Set data and header iff changes need to be made. If these are # still None when it's time to write, just copy. new_data, new_header = None, None @@ -1132,7 +1122,7 @@ def _run_interface(self, runtime): f'by interpolated patterns ({len(dest_files)}).' ) - for i, (orig_file, dest_file) in enumerate(zip(in_file, dest_files)): + for i, (orig_file, dest_file) in enumerate(zip(in_file, dest_files, strict=False)): out_file = out_path / dest_file out_file.parent.mkdir(exist_ok=True, parents=True) self._results['out_file'].append(str(out_file)) diff --git a/niworkflows/interfaces/confounds.py b/niworkflows/interfaces/confounds.py index 4dc5b103a57..f0317b448c7 100644 --- a/niworkflows/interfaces/confounds.py +++ b/niworkflows/interfaces/confounds.py @@ -303,7 +303,7 @@ def spike_regressors( post_final = data.shape[0] + 1 epoch_length = np.diff(sorted(mask | {-1, post_final})) - 1 epoch_end = sorted(mask | {post_final}) - for end, length in zip(epoch_end, epoch_length): + for end, length in zip(epoch_end, epoch_length, strict=False): if length < minimum_contiguous: mask = mask | set(range(end - length, end)) mask = mask.intersection(indices) diff --git a/niworkflows/interfaces/images.py b/niworkflows/interfaces/images.py index 388dbea9bbb..d6ab3d1443a 100644 --- a/niworkflows/interfaces/images.py +++ b/niworkflows/interfaces/images.py @@ -284,7 +284,9 @@ def _run_interface(self, runtime): self._results['out_file'] = fname(suffix='_average') self._results['out_volumes'] = fname(suffix='_sliced') - sliced = nb.concat_images(i for i, t in zip(nb.four_to_three(img), t_mask) if t) + sliced = nb.concat_images( + i for i, t in zip(nb.four_to_three(img), t_mask, strict=False) if t + ) data = sliced.get_fdata(dtype='float32') # Data can come with outliers showing very high numbers - preemptively prune diff --git a/niworkflows/interfaces/itk.py b/niworkflows/interfaces/itk.py index 1c8e2c7c156..957ab8e370d 100644 --- a/niworkflows/interfaces/itk.py +++ b/niworkflows/interfaces/itk.py @@ -141,7 +141,7 @@ def _run_interface(self, runtime): if num_threads == 1: out_files = [ _applytfms((in_file, in_xfm, ifargs, i, runtime.cwd)) - for i, (in_file, in_xfm) in enumerate(zip(in_files, xfms_list)) + for i, (in_file, in_xfm) in enumerate(zip(in_files, xfms_list, strict=False)) ] else: from concurrent.futures import ThreadPoolExecutor @@ -152,7 +152,9 @@ def _run_interface(self, runtime): _applytfms, [ (in_file, in_xfm, ifargs, i, runtime.cwd) - for i, (in_file, in_xfm) in enumerate(zip(in_files, xfms_list)) + for i, (in_file, in_xfm) in enumerate( + zip(in_files, xfms_list, strict=False) + ) ], ) ) @@ -264,4 +266,4 @@ def _arrange_xfms(transforms, num_files, tmp_folder): xfms_T.append(split_xfms) # Transpose back (only Python 3) - return list(map(list, zip(*xfms_T))) + return list(map(list, zip(*xfms_T, strict=False))) diff --git a/niworkflows/interfaces/surf.py b/niworkflows/interfaces/surf.py index a4dd2e91753..6834185ea37 100644 --- a/niworkflows/interfaces/surf.py +++ b/niworkflows/interfaces/surf.py @@ -743,6 +743,7 @@ def ply2gii(in_file, metadata, out_file=None): zip( ('SurfaceCenterX', 'SurfaceCenterY', 'SurfaceCenterZ'), [f'{c:.4f}' for c in surf.centroid], + strict=False, ) ) diff --git a/niworkflows/interfaces/tests/test_bids.py b/niworkflows/interfaces/tests/test_bids.py index 9d6b16ef15a..f1e8116eb61 100644 --- a/niworkflows/interfaces/tests/test_bids.py +++ b/niworkflows/interfaces/tests/test_bids.py @@ -361,10 +361,10 @@ def test_DerivativesDataSink_build_path( ] base = (out_path_base or 'niworkflows') if interface == bintfs.DerivativesDataSink else '' - for out, exp in zip(output, expectation): + for out, exp in zip(output, expectation, strict=False): assert Path(out).relative_to(base_directory) == Path(base) / exp - for out, exp in zip(output, expectation): + for out, exp in zip(output, expectation, strict=False): assert Path(out).relative_to(base_directory) == Path(base) / exp # Regression - some images were given nan scale factors if out.endswith(('.nii', '.nii.gz')): @@ -374,7 +374,7 @@ def test_DerivativesDataSink_build_path( hdr = img.header.from_fileobj(fobj) assert not np.isnan(hdr['scl_slope']) assert not np.isnan(hdr['scl_inter']) - for out, chksum in zip(output, checksum): + for out, chksum in zip(output, checksum, strict=False): if chksum == '335f1394ce90b58bbf27026b6eeec4d2124c11da': if Version(nb.__version__) < Version('5.3'): # Nibabel 5.3 avoids unnecessary roundtrips for Cifti2Headers diff --git a/niworkflows/interfaces/tests/test_images.py b/niworkflows/interfaces/tests/test_images.py index 4f261bcf485..5b0409d97cf 100644 --- a/niworkflows/interfaces/tests/test_images.py +++ b/niworkflows/interfaces/tests/test_images.py @@ -195,7 +195,7 @@ def test_TemplateDimensions(tmp_path): (0.9, 0.9, 0.9), ] - for i, (shape, zoom) in enumerate(zip(shapes, zooms)): + for i, (shape, zoom) in enumerate(zip(shapes, zooms, strict=False)): img = nb.Nifti1Image(np.ones(shape, dtype='float32'), np.eye(4)) img.header.set_zooms(zoom) img.to_filename(tmp_path / f'test{i}.nii') diff --git a/niworkflows/interfaces/utility.py b/niworkflows/interfaces/utility.py index 4a3dd60108c..1c5999f3f3a 100644 --- a/niworkflows/interfaces/utility.py +++ b/niworkflows/interfaces/utility.py @@ -371,7 +371,7 @@ def _run_interface(self, runtime): raise ValueError('Number of columns in datasets do not match') merged = [] - for d, j in zip(data, join): + for d, j in zip(data, join, strict=False): line = '%s\t%s' % ((j, d) if self.inputs.side == 'left' else (d, j)) merged.append(line) diff --git a/niworkflows/reports/core.py b/niworkflows/reports/core.py index 5c7a2926d05..1805695f97f 100644 --- a/niworkflows/reports/core.py +++ b/niworkflows/reports/core.py @@ -534,7 +534,9 @@ def generate_reports( logger = logging.getLogger('cli') error_list = ', '.join( - f'{subid} ({err})' for subid, err in zip(subject_list, report_errors) if err + f'{subid} ({err})' + for subid, err in zip(subject_list, report_errors, strict=False) + if err ) logger.error( 'Preprocessing did not finish successfully. Errors occurred while processing ' diff --git a/niworkflows/tests/test_viz.py b/niworkflows/tests/test_viz.py index fe1818a9a02..1497c2f7ae0 100644 --- a/niworkflows/tests/test_viz.py +++ b/niworkflows/tests/test_viz.py @@ -72,7 +72,7 @@ def test_carpetplot(tr, sorting): rng.shuffle(indexes) segments = {} start = 0 - for group, size in zip(labels, sizes): + for group, size in zip(labels, sizes, strict=False): segments[group] = indexes[start : start + size] data[indexes[start : start + size]] = rng.normal( rng.standard_normal(1) * 100, rng.normal(20, 5, size=1), size=(size, 300) @@ -100,7 +100,7 @@ def test_carpetplot(tr, sorting): rng.shuffle(indexes) segments = {} start = 0 - for i, (group, size) in enumerate(zip(labels, sizes)): + for i, (group, size) in enumerate(zip(labels, sizes, strict=False)): segments[group] = indexes[start : start + size] data[indexes[start : start + size]] = i start += size diff --git a/niworkflows/utils/spaces.py b/niworkflows/utils/spaces.py index 344b2ae9bd8..fc5e93e4dff 100644 --- a/niworkflows/utils/spaces.py +++ b/niworkflows/utils/spaces.py @@ -805,4 +805,4 @@ def _expand_entities(entities): """ keys = list(entities.keys()) values = list(product(*[entities[k] for k in keys])) - return [dict(zip(keys, combs)) for combs in values] + return [dict(zip(keys, combs, strict=False)) for combs in values] diff --git a/niworkflows/utils/tests/test_spaces.py b/niworkflows/utils/tests/test_spaces.py index 8b6e35f6d93..1f37c56b778 100644 --- a/niworkflows/utils/tests/test_spaces.py +++ b/niworkflows/utils/tests/test_spaces.py @@ -95,7 +95,7 @@ def test_space_action(parser, spaces, expected): 'Every element must be a `Reference`' ) assert len(parsed_spaces.references) == len(expected) - for ref, expected_ref in zip(parsed_spaces.references, expected): + for ref, expected_ref in zip(parsed_spaces.references, expected, strict=False): assert str(ref) == expected_ref diff --git a/niworkflows/viz/plots.py b/niworkflows/viz/plots.py index d415327cb41..6b74d0e920d 100644 --- a/niworkflows/viz/plots.py +++ b/niworkflows/viz/plots.py @@ -758,7 +758,7 @@ def compcor_variance_plot( metadata_sources = ['CompCor'] else: metadata_sources = [f'Decomposition {i:d}' for i in range(len(metadata_files))] - for file, source in zip(metadata_files, metadata_sources): + for file, source in zip(metadata_files, metadata_sources, strict=False): metadata[source] = pd.read_csv(str(file), sep=r'\s+') metadata[source]['source'] = source metadata = pd.concat(list(metadata.values())) diff --git a/niworkflows/viz/utils.py b/niworkflows/viz/utils.py index 60883cfb203..7694e6141f1 100644 --- a/niworkflows/viz/utils.py +++ b/niworkflows/viz/utils.py @@ -185,7 +185,7 @@ def cuts_from_bbox(mask_nii, cuts=3): vox_coords = np.zeros((4, cuts), dtype=np.float32) vox_coords[-1, :] = 1.0 - for ax, (c, th) in enumerate(zip(ijk_counts, ijk_th)): + for ax, (c, th) in enumerate(zip(ijk_counts, ijk_th, strict=False)): # Start with full plane if mask is seemingly empty smin, smax = (0, mask_data.shape[ax] - 1) @@ -198,7 +198,7 @@ def cuts_from_bbox(mask_nii, cuts=3): vox_coords[ax, :] = np.linspace(smin, smax, num=cuts + 2)[1:-1] ras_coords = mask_nii.affine.dot(vox_coords)[:3, ...] - return {k: list(v) for k, v in zip(['x', 'y', 'z'], np.around(ras_coords, 3))} + return {k: list(v) for k, v in zip(['x', 'y', 'z'], np.around(ras_coords, 3), strict=False)} def _3d_in_file(in_file): diff --git a/pyproject.toml b/pyproject.toml index 5ecdbe3e34e..323d2fad6b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,13 @@ description = "NeuroImaging Workflows provides processing tools for magnetic res readme = "README.rst" license = "Apache-2.0" license-files = ["LICENSE"] -requires-python = ">= 3.9" +requires-python = ">= 3.10" authors = [ { name = "The NiPreps Developers", email = "nipreps@gmail.com" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -24,23 +23,23 @@ classifiers = [ "Topic :: Scientific/Engineering :: Image Recognition", ] dependencies = [ - "acres", - "attrs >=20.1", + "acres >=0.5", + "attrs >=23.1", "jinja2 >=3", - "looseversion", - "matplotlib >= 3.5", - "nibabel >= 3.0", - "nilearn >= 0.8", - "nipype >= 1.8.5", - "nitransforms >= 22.0.0", - "numpy >= 1.20", + "looseversion >=1.3", + "matplotlib >= 3.7", + "nibabel >= 5.0", + "nilearn >= 0.10", + "nipype >= 1.9.1", + "nitransforms >= 23.0.0", + "numpy >= 1.24", "packaging", - "pandas >= 1.2", - "pybids >= 0.15.1", - "PyYAML >= 5.4", - "scikit-image >= 0.18", - "scipy >= 1.8", - "seaborn >= 0.11", + "pandas >= 2.0", + "pybids >= 0.16", + "pyyaml >= 6.0", + "scikit-image >= 0.20", + "scipy >= 1.10", + "seaborn >= 0.13", "svgutils >= 0.3.4", "templateflow >= 23.1", "transforms3d >= 0.4", @@ -62,11 +61,11 @@ style = [ "flake8 >= 3.7.0", ] tests = [ - "coverage[toml] >=5.2.1", - "pytest >= 6", - "pytest-cov >= 2.11", + "coverage[toml] >=7", + "pytest >= 8", + "pytest-cov >= 7", "pytest-env", - "pytest-xdist >= 2.5", + "pytest-xdist >= 3.8", "pytest-xvfb >= 2", ] # Aliases @@ -226,3 +225,45 @@ source = [ [tool.codespell] skip = "*/data/*,*/docs/_build/*,./examples/viz-report.*" ignore-words-list = "objekt,nd" + +[tool.pixi.workspace] +channels = ["https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/public", "conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.feature.niworkflows.activation.env] +FSLDIR = "$CONDA_PREFIX" + +[tool.pixi.pypi-dependencies] +niworkflows = { path = ".", editable = true } + +[tool.pixi.environments] +default = { features = ["editable"], solve-group = "default" } +test = { features = ["editable", "tests"], solve-group = "default" } +niworkflows = { features = ["production"], solve-group = "production" } + +[tool.pixi.feature.editable.pypi-dependencies] +niworkflows = { path = ".", editable = true } +[tool.pixi.feature.production.pypi-dependencies] +niworkflows = { path = ".", editable = false } + +[tool.pixi.tasks] + +[tool.pixi.dependencies] +python = "3.12.*" +mkl = "2024.2.2.*" +mkl-service = "2.4.2.*" +numpy = "1.26.*" +scipy = "1.15.*" +matplotlib = "3.9.*" +pandas = "2.2.*" +h5py = "3.13.*" +scikit-image = "0.25.*" +scikit-learn = "1.6.*" +graphviz = "11.0.*" +ants = "2.5.*" +libitk = "5.4.0.*" +fsl-bet2 = "2111.8.*" +fsl-flirt = "2111.2.*" +fsl-fast4 = "2111.3.*" +fsl-mcflirt = "2111.0.*" +fsl-miscmaths = "2203.2.*" diff --git a/tox.ini b/tox.ini index f06c9414ba9..650ff8bdc66 100644 --- a/tox.ini +++ b/tox.ini @@ -2,19 +2,18 @@ requires = tox>=4 envlist = - py3{9,10,11,12,13}-latest - py39-min - py3{10,11,12,13}-pre + py3{10,11,12,13,14}-{latest,pre} + py310-min skip_missing_interpreters = true # Configuration that allows us to split tests across GitHub runners effectively [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 3.13: py313 + 3.14: py314 [gh-actions:env] DEPENDS = @@ -54,8 +53,6 @@ pass_env = CLICOLOR CLICOLOR_FORCE PYTHON_GIL -deps = - py313: traits @ git+https://github.com/enthought/traits.git@10954eb extras = tests setenv = pre: PIP_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple