diff --git a/.circleci/config.yml b/.circleci/config.yml index 0dfa7a6d..cd1217da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: build: executor: name: python/default - tag: '3.12' + tag: '3.13' # docker: # - auth: @@ -38,7 +38,7 @@ jobs: deploy_pypi: executor: name: python/default - tag: '3.12' + tag: '3.13' # docker: # - auth: @@ -60,7 +60,7 @@ jobs: tests: executor: name: python/default - tag: '3.12' + tag: '3.13' # docker: # - auth: @@ -70,37 +70,19 @@ jobs: environment: - OSF_MIRROR_PATH: /tmp/data/templateflow steps: - - restore_cache: - keys: - - annex-v1-{{ epoch }} - - annex-v1- - run: - name: Install git and git-annex + name: Configure git command: | - if [[ ! -e "/tmp/cache/git-annex-standalone.tar.gz" ]]; then - wget -O- http://neuro.debian.net/lists/focal.us-ca.full | sudo tee /etc/apt/sources.list.d/neurodebian.sources.list - sudo apt-key add .neurodebian/neurodebian.gpg - sudo apt-key adv --recv-keys --keyserver hkps://keys.openpgp.org 0xA5D32F012649A5A9 || true - sudo apt update && sudo apt-get install -y --no-install-recommends git-annex-standalone - mkdir -p /tmp/cache - tar czvf /tmp/cache/git-annex-standalone.tar.gz /usr/bin/git-annex /usr/bin/git-annex-shell /usr/lib/git-annex.linux - else - sudo tar xzfv /tmp/cache/git-annex-standalone.tar.gz -C / - fi git config --global user.name "First Last" git config --global user.email "email@domain.com" - - save_cache: - key: annex-v1-{{ epoch }} - paths: - - "/tmp/cache" - - attach_workspace: at: ~/project - run: command: | python .maint/update_requirements.py + echo git-annex >> dev-requirements.txt name: Generate requirements.txt - python/install-packages: @@ -126,8 +108,14 @@ jobs: mkdir -p ~/tests/ ~/coverage/ export TEMPLATEFLOW_USE_DATALAD=on python -m pytest \ - --junit-xml=~/tests/datalad.xml --cov templateflow --cov-report xml:~/coverage/cov_api_dl.xml \ - --doctest-modules templateflow/api.py + --junit-xml=~/tests/datalad.xml --cov templateflow --doctest-modules \ + templateflow/api.py + + coverage run --append -m templateflow.cli config + coverage run --append -m templateflow.cli ls MNI152NLin2009cAsym --suffix T1w + coverage run --append -m templateflow.cli get MNI152NLin2009cAsym --suffix mask + coverage run --append -m templateflow.cli update + coverage xml -o ~/coverage/cov_api_dl.xml - codecov/upload: file: ~/coverage/cov_api_dl.xml @@ -140,8 +128,13 @@ jobs: export TEMPLATEFLOW_USE_DATALAD=off export TEMPLATEFLOW_HOME=$HOME/templateflow-s3 python -m pytest \ - --junit-xml=~/tests/s3.xml --cov templateflow --cov-report xml:~/coverage/cov_api_s3.xml \ - --doctest-modules templateflow/api.py + --junit-xml=~/tests/s3.xml --cov templateflow --doctest-modules templateflow/api.py + + coverage run --append -m templateflow.cli config + coverage run --append -m templateflow.cli ls MNI152NLin2009cAsym --suffix T1w + coverage run --append -m templateflow.cli get MNI152NLin2009cAsym --suffix mask + coverage run --append -m templateflow.cli update + coverage xml -o ~/coverage/cov_api_s3.xml - codecov/upload: file: ~/coverage/cov_api_s3.xml @@ -273,6 +266,11 @@ workflows: only: - master jobs: + - build: + context: + - nipreps-common - tests: context: - nipreps-common + requires: + - build diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index b988f68f..06a143a8 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -63,7 +63,7 @@ jobs: needs: build strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] mode: ['wheel'] include: - {python-version: '3.11', mode: 'repo'} diff --git a/CHANGES.rst b/CHANGES.rst index 2b79878a..99b9e103 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +25.0.0 (August 12, 2025) +======================== +Minor release including updated templates. + +* FIX: Fix crash in templateflow get when matching one file (#140) +* MAINT: Update Python support, datalad and git-annex deps (#143) + 24.2.2 (September 14, 2024) =========================== Patch release containing one bugfix and updating the default skeleton. diff --git a/docs/conf.py b/docs/conf.py index 6d778bcb..a9d8b88d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,8 +24,7 @@ # The short X.Y version version = ( - __version__ if Version(release).public == release - else f"dev ({release.partition('+')[0]})" + __version__ if Version(release).public == release else f'dev ({release.partition("+")[0]})' ) # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 51e999f9..59f87c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,14 +12,14 @@ classifiers = [ "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering :: Image Recognition", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "pybids >= 0.15.2", "importlib_resources >= 5.7; python_version < '3.11'", @@ -45,7 +45,7 @@ test = [ "toml", ] datalad = [ - "datalad ~= 1.0.0" + "datalad >= 1.0.0" ] doc = [ "nbsphinx", diff --git a/templateflow/api.py b/templateflow/api.py index 6d1d51f0..b1aa8ca5 100644 --- a/templateflow/api.py +++ b/templateflow/api.py @@ -176,22 +176,18 @@ def get(template, raise_empty=False, **kwargs): if not_fetched: msg = 'Could not fetch template files: {}.'.format(', '.join(not_fetched)) if dl_missing and not TF_USE_DATALAD: - msg += ( - f"""\ + msg += f"""\ The $TEMPLATEFLOW_HOME folder {TF_LAYOUT.root} seems to contain an initiated DataLad \ dataset, but the environment variable $TEMPLATEFLOW_USE_DATALAD is not \ set or set to one of (false, off, 0). Please set $TEMPLATEFLOW_USE_DATALAD \ on (possible values: true, on, 1).""" - ) if s3_missing and TF_USE_DATALAD: - msg += ( - f"""\ + msg += f"""\ The $TEMPLATEFLOW_HOME folder {TF_LAYOUT.root} seems to contain an plain \ dataset, but the environment variable $TEMPLATEFLOW_USE_DATALAD is \ set to one of (true, on, 1). Please set $TEMPLATEFLOW_USE_DATALAD \ off (possible values: false, off, 0).""" - ) raise RuntimeError(msg) @@ -394,5 +390,5 @@ def _normalize_ext(value): return value if isinstance(value, str): - return f"{'' if value.startswith('.') else '.'}{value}" + return f'{"" if value.startswith(".") else "."}{value}' return [_normalize_ext(v) for v in value] diff --git a/templateflow/cli.py b/templateflow/cli.py index f536eed1..59453ced 100644 --- a/templateflow/cli.py +++ b/templateflow/cli.py @@ -61,7 +61,7 @@ def entity_opts(): entities = json.loads(Path(load_data('conf/config.json')).read_text())['entities'] args = [ - (f"--{e['name']}", *ENTITY_SHORTHANDS.get(e['name'], ())) + (f'--{e["name"]}', *ENTITY_SHORTHANDS.get(e['name'], ())) for e in entities if e['name'] not in ENTITY_EXCLUDE ] @@ -141,7 +141,9 @@ def ls(template, **kwargs): def get(template, **kwargs): """Fetch the assets corresponding to template and optional filters.""" entities = {k: _nulls(v) for k, v in kwargs.items() if v != ''} - click.echo('\n'.join(f'{match}' for match in api.get(template, **entities))) + paths = api.get(template, **entities) + filenames = [str(paths)] if isinstance(paths, Path) else [str(file) for file in paths] + click.echo('\n'.join(filenames)) if __name__ == '__main__': diff --git a/templateflow/conf/templateflow-skel.md5 b/templateflow/conf/templateflow-skel.md5 index d6259748..cc9f6de2 100644 --- a/templateflow/conf/templateflow-skel.md5 +++ b/templateflow/conf/templateflow-skel.md5 @@ -1 +1 @@ -2c49f134299f66f514faa5dd8cf4b64f - +0817bbb0e75461d87e389427470b48e8 - diff --git a/templateflow/conf/templateflow-skel.zip b/templateflow/conf/templateflow-skel.zip index 8727b9c0..60c1eacf 100644 Binary files a/templateflow/conf/templateflow-skel.zip and b/templateflow/conf/templateflow-skel.zip differ diff --git a/templateflow/tests/test_api.py b/templateflow/tests/test_api.py index fe2aea3b..6cf83eb6 100644 --- a/templateflow/tests/test_api.py +++ b/templateflow/tests/test_api.py @@ -85,9 +85,9 @@ def assert_same(self, other): assert self.citekey == other.citekey, 'Mismatched citekeys' for key in self.pairs.keys(): assert key in other.pairs, f'Key ({key}) missing from other' - assert ( - self.pairs[key] == other.pairs[key] - ), f'Key ({key}) mismatched\n\n{self.pairs[key]}\n\n{other.pairs[key]}' + assert self.pairs[key] == other.pairs[key], ( + f'Key ({key}) mismatched\n\n{self.pairs[key]}\n\n{other.pairs[key]}' + ) for key in other.pairs.keys(): assert key in self.pairs, f'Key ({key}) missing from pairs' diff --git a/templateflow/tests/test_cli.py b/templateflow/tests/test_cli.py new file mode 100644 index 00000000..141b70db --- /dev/null +++ b/templateflow/tests/test_cli.py @@ -0,0 +1,69 @@ +from pathlib import Path + +import click.testing +import pytest + +from .. import cli + + +@pytest.fixture +def runner(): + return click.testing.CliRunner() + + +def test_ls_one(runner): + result = runner.invoke(cli.main, ['ls', 'MNI152Lin', '--res', '1', '-s', 'T1w']) + + # One result + lines = result.stdout.strip().splitlines() + assert len(lines) == 1 + + assert 'tpl-MNI152Lin/tpl-MNI152Lin_res-01_T1w.nii.gz' in lines[0] + + path = Path(lines[0]) + + assert path.exists() + + +def test_ls_multi(runner): + result = runner.invoke(cli.main, ['ls', 'MNI152Lin', '--res', '1', '-s', 'T1w', '-s', 'T2w']) + + # Two results + lines = result.stdout.strip().splitlines() + assert len(lines) == 2 + + assert 'tpl-MNI152Lin/tpl-MNI152Lin_res-01_T1w.nii.gz' in lines[0] + assert 'tpl-MNI152Lin/tpl-MNI152Lin_res-01_T2w.nii.gz' in lines[1] + + paths = [Path(line) for line in lines] + + assert all(path.exists() for path in paths) + + +def test_get_one(runner): + result = runner.invoke(cli.main, ['get', 'MNI152Lin', '--res', '1', '-s', 'T1w']) + + # One result, possible download status before + lines = result.stdout.strip().splitlines()[-2:] + + assert 'tpl-MNI152Lin/tpl-MNI152Lin_res-01_T1w.nii.gz' in lines[0] + + path = Path(lines[0]) + + stat_res = path.stat() + assert stat_res.st_size == 10669511 + + +def test_get_multi(runner): + result = runner.invoke(cli.main, ['get', 'MNI152Lin', '--res', '1', '-s', 'T1w', '-s', 'T2w']) + + # Two result, possible download status before + lines = result.stdout.strip().splitlines()[-3:] + + assert 'tpl-MNI152Lin/tpl-MNI152Lin_res-01_T1w.nii.gz' in lines[0] + assert 'tpl-MNI152Lin/tpl-MNI152Lin_res-01_T2w.nii.gz' in lines[1] + + paths = [Path(line) for line in lines] + + stats = [path.stat() for path in paths] + assert [stat_res.st_size for stat_res in stats] == [10669511, 10096230]