From 3fcdf34b154ba4691232ecd5b6d7c2f493951f6d Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Thu, 30 Jul 2026 13:24:00 +0100 Subject: [PATCH 1/3] lint: codacy --- README.ipynb | 4 ++-- README.md | 2 +- brainweb/utils.py | 23 ++++++++++++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.ipynb b/README.ipynb index 44e4e7a..a2274b2 100644 --- a/README.ipynb +++ b/README.ipynb @@ -37,8 +37,8 @@ "[PyPI-target]: https://pypi.org/project/brainweb\n", "[CI]: https://img.shields.io/github/actions/workflow/status/casperdcl/brainweb/test.yml?branch=main&label=brainweb&logo=GitHub\n", "[CI-target]: https://github.com/casperdcl/brainweb/actions/workflows/test.yml\n", - "[Quality]: https://api.codacy.com/project/badge/Grade/cdad13693b0141199c31d5b44c7ab185\n", - "[Quality-target]: https://www.codacy.com/app/casper-dcl/brainweb\n", + "[Quality]: https://app.codacy.com/project/badge/Grade/cdad13693b0141199c31d5b44c7ab185\n", + "[Quality-target]: https://app.codacy.com/gh/casperdcl/brainweb\n", "[DOI]: https://img.shields.io/badge/DOI-10.5281/zenodo.3269888-blue.svg\n", "[DOI-target]: https://doi.org/10.5281/zenodo.3269888\n", "[LICENCE]: https://img.shields.io/pypi/l/brainweb.svg?label=licence\n", diff --git a/README.md b/README.md index fcbb5bd..92b7e60 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ coregistration. [![PyPI](https://img.shields.io/pypi/v/brainweb.svg)](https://pypi.org/project/brainweb) [![CI](https://img.shields.io/github/actions/workflow/status/casperdcl/brainweb/test.yml?branch=main&label=brainweb&logo=GitHub)](https://github.com/casperdcl/brainweb/actions/workflows/test.yml) -[![Quality](https://api.codacy.com/project/badge/Grade/cdad13693b0141199c31d5b44c7ab185)](https://www.codacy.com/app/casper-dcl/brainweb) +[![Quality](https://app.codacy.com/project/badge/Grade/cdad13693b0141199c31d5b44c7ab185)](https://app.codacy.com/gh/casperdcl/brainweb) [![DOI](https://img.shields.io/badge/DOI-10.5281/zenodo.3269888-blue.svg)](https://doi.org/10.5281/zenodo.3269888) [![LICENCE](https://img.shields.io/pypi/l/brainweb.svg?label=licence)](https://www.mozilla.org/MPL/2.0) diff --git a/brainweb/utils.py b/brainweb/utils.py index f7adb86..d21e415 100644 --- a/brainweb/utils.py +++ b/brainweb/utils.py @@ -10,7 +10,12 @@ import numpy as np import requests from numpy.random import seed -from scipy.ndimage.filters import gaussian_filter + +try: + from scipy.ndimage import gaussian_filter +except ImportError: # scikit-image<2 + from scipy.ndimage.filters import gaussian_filter + from skimage.transform import resize from tqdm.auto import tqdm, trange from tqdm.contrib import tenumerate @@ -178,7 +183,7 @@ def get_file(fname, origin, cache_dir=None, chunk_size=None): if not path.exists(fpath): log.debug(f"Downloading {fpath} from {origin}") try: - d = requests.get(origin, stream=True) + d = requests.get(origin, stream=True, timeout=(10, None)) with tqdm(total=float(d.headers.get('Content-length') or 0), desc=fname, unit="B", unit_scale=True, unit_divisor=1024, leave=False) as fprog: with open(fpath, 'wb') as fo: @@ -583,12 +588,16 @@ def register(src, target=None, ROI=None, target_shape=Shape.mMR, src_resolution= """ from dipy.align.imaffine import AffineMap, transform_centers_of_mass - assert src.ndim == 3 + def assert3D(ndim, name): + if ndim != 3: + raise ValueError(f"{name} must be 3D, got {ndim}D") + + assert3D(src.ndim, "src") if target is not None: - assert target.ndim == 3 - assert len(target_shape) == 3 - assert len(src_resolution) == 3 - assert len(target_resolution) == 3 + assert3D(target.ndim, "target") + assert3D(len(target_shape), "target_shape") + assert3D(len(src_resolution), "src_resolution") + assert3D(len(target_resolution), "target_resolution") if ROI is None: ROI = ((0, None),) From 19be73c353fddf0eaa81814b503a7f3eb64eee61 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Thu, 30 Jul 2026 14:12:47 +0100 Subject: [PATCH 2/3] tests: pytest --- .github/workflows/test.yml | 35 ++++++++++++++++++++++++++++- .gitignore | 3 +++ .pre-commit-config.yaml | 15 +++++++++++++ brainweb/utils.py | 4 +++- pyproject.toml | 18 ++++++++++++--- tests/test_brainweb.py | 45 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 tests/test_brainweb.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85caca4..4b2fe59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,41 @@ on: pull_request: schedule: [{cron: '4 3 1 * *'}] # M H d m w (monthly at 3:04) jobs: + test: + if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) + strategy: + matrix: + python: [3.8, 3.14] + os: [ubuntu] + include: [{os: macos, python: 3.14}, {os: windows, python: 3.14}] + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 5 + defaults: {run: {shell: bash}} + steps: + - uses: actions/checkout@v7 + with: {fetch-depth: 0} + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python }} + - run: pip install -e .[full,dev] + - run: pytest --cov=brainweb --cov-fail-under=50 --cov-report=xml --cov-report=term-missing + - name: codacy + run: | + bash <(curl -fsL https://coverage.codacy.com/get.sh) report -l Python -r coverage.xml --partial || : + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + finish: + continue-on-error: ${{ github.event_name != 'push' }} + needs: test + runs-on: ubuntu-slim + steps: + - name: codacy + run: | + bash <(curl -fsL https://coverage.codacy.com/get.sh) final || : + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} deploy: - name: PyPI Deploy + needs: test runs-on: ubuntu-latest environment: pypi permissions: {contents: write, id-token: write, packages: write} diff --git a/.gitignore b/.gitignore index 7e47800..2fdf9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ __pycache__/ /*.egg*/ /build/ /dist/ + +/.coverage* +/coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 799b950..979d380 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,19 @@ repos: args: [-i] types: [text] exclude: ^(.pre-commit-config.yaml|.github/workflows/test.yml)$ + - id: pytest + name: pytest + language: python + entry: env PYTHONPATH=. pytest + args: [--cov=brainweb, -vv, -k=quick] + types: [python] + pass_filenames: false + additional_dependencies: + - pytest-cov + - numpy + - requests + - scikit-image + - tqdm>=4.42.0 - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: @@ -53,3 +66,5 @@ repos: rev: 9.0.0b1 hooks: - id: isort +ci: + skip: [pytest] diff --git a/brainweb/utils.py b/brainweb/utils.py index d21e415..6c1654c 100644 --- a/brainweb/utils.py +++ b/brainweb/utils.py @@ -614,7 +614,9 @@ def assert3D(ndim, name): affine_init[:3, -1] -= moving.shape * src_resolution / target_resolution affine_init[:3, -1] /= 2 affine_init[:3, -1] += src_offset - affine_map = AffineMap(np.eye(4), target_shape, np.eye(4), moving.shape, affine_init) # unmoved target + # unmoved target + affine_map = AffineMap(np.eye(4), domain_grid_shape=target_shape, domain_grid2world=np.eye(4), + codomain_grid_shape=moving.shape, codomain_grid2world=affine_init) src = affine_map.transform(moving) if target is not None: diff --git a/pyproject.toml b/pyproject.toml index 9226883..c82628c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,11 +27,12 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3"] dependencies = [ - "tqdm>=4.42.0", "numpy", "requests", - "scikit-image"] + "scikit-image", + "tqdm>=4.42.0"] [project.optional-dependencies] +dev = ["pytest>=6", "pytest-cov"] plot = ["matplotlib"] register = ["dipy"] full = ["matplotlib", "dipy"] @@ -55,4 +56,15 @@ blank_line_before_nested_class_or_def = false [tool.isort] line_length = 120 -known_first_party = ["brainweb"] +known_first_party = ["brainweb", "tests"] + +[tool.pytest.ini_options] +log_level = "INFO" +testpaths = ["tests"] +markers = ["quick"] +addopts = "-v --tb=short -rxs -W=error --durations=0 --durations-min=0.1" + +[tool.coverage.run] +branch = true +[tool.coverage.report] +show_missing = true diff --git a/tests/test_brainweb.py b/tests/test_brainweb.py new file mode 100644 index 0000000..7ee5c9a --- /dev/null +++ b/tests/test_brainweb.py @@ -0,0 +1,45 @@ +import pytest + +import brainweb as bw + + +@pytest.mark.quick +def test_import(): + assert bw.__version__ + + +@pytest.fixture(scope="session") +def subject_file(tmp_path_factory): + f, url = next(iter(bw.LINKS.items())) + return bw.get_file(f, url, cache_dir=tmp_path_factory.mktemp("brainweb")) + + +def test_download(subject_file): + from pathlib import Path + assert Path(subject_file).exists() + + +def test_load(subject_file): + data = bw.load_file(subject_file) + assert data.shape == (362, 434, 362) + + +def test_mMR(subject_file): + vol = bw.get_mmr_fromfile(subject_file) + assert not {'T1', 'T2', 'uMap', 'PET'} - set(vol.keys()) + assert vol['PET'].shape == (127, 344, 344) + + +def test_lesions(subject_file): + vol = bw.get_mmr_fromfile(subject_file) + im3d = bw.add_lesions(vol['PET']) + assert im3d.shape == vol['PET'].shape + assert (im3d != vol['PET']).any() + + +def test_registration(subject_file): + pytest.importorskip("dipy") + data = bw.load_file(subject_file) + vol = bw.get_mmr_fromfile(subject_file) + reg = bw.register(data[:, ::-1], target=vol['PET'], src_resolution=bw.Res.brainweb, target_resolution=bw.Res.mMR) + assert reg.shape == vol['PET'].shape From 444bb465320bd1ceae873ee14c3ad350c0bb9acf Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Thu, 30 Jul 2026 14:39:25 +0100 Subject: [PATCH 3/3] lint: bandit --- .pre-commit-config.yaml | 6 ++++++ brainweb/utils.py | 3 ++- pyproject.toml | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 979d380..a6c5407 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,6 +39,11 @@ repos: - requests - scikit-image - tqdm>=4.42.0 +- repo: https://github.com/PyCQA/bandit + rev: 1.9.4 + hooks: + - id: bandit + args: [-c, pyproject.toml] - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: @@ -67,4 +72,5 @@ repos: hooks: - id: isort ci: + autoupdate_schedule: monthly skip: [pytest] diff --git a/brainweb/utils.py b/brainweb/utils.py index 6c1654c..ae24f6a 100644 --- a/brainweb/utils.py +++ b/brainweb/utils.py @@ -174,7 +174,8 @@ def get_file(fname, origin, cache_dir=None, chunk_size=None): except Exception: log.warning("cannot create:" + cache_dir) if not os.access(cache_dir, os.W_OK): - cache_dir = path.join('/tmp', '.brainweb') + from tempfile import gettempdir + cache_dir = path.join(gettempdir(), '.brainweb') if not path.exists(cache_dir): os.makedirs(cache_dir) diff --git a/pyproject.toml b/pyproject.toml index c82628c..0be186c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ plot = ["matplotlib"] register = ["dipy"] full = ["matplotlib", "dipy"] +[tool.bandit] +exclude_dirs = ["tests"] + [tool.flake8] max_line_length = 120 extend_ignore = ["E261"]