diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 288e7788..f00cf3cb 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -24,72 +24,63 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + +env: + # Force tox and pytest to use color + FORCE_COLOR: true + TEMPLATEFLOW_HOME: /tmp/templateflow + jobs: build: - if: | - github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - || ! contains(github.event.head_commit.message, '[skip ci]') - runs-on: 'ubuntu-latest' + name: Build & verify package + runs-on: ubuntu-latest + permissions: + attestations: write + id-token: write steps: - uses: actions/checkout@v4 with: - submodules: recursive fetch-depth: 0 - - uses: actions/setup-python@v5 + - uses: hynek/build-and-inspect-python-package@v2 with: - python-version: '3.x' - - name: Install build dependencies - run: pip install --upgrade pip build twine - - name: Build sdist and wheel - run: python -m build -s -w - - name: Check distributions - run: twine check dist/* - - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ + attest-build-provenance-github: ${{ github.event_name != 'pull_request' }} test: - needs: [build] runs-on: ${{ matrix.os }} strategy: matrix: #os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] os: ['ubuntu-latest'] - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] - architecture: ['x64', 'x86'] - package: ['.', 'dist/*.whl', 'dist/*.tar.gz'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + dependencies: [latest] # , pre] + architecture: ['x64'] + include: + # Test minimum dependencies on oldest supported Python + - os: ubuntu-latest + python-version: "3.9" + dependencies: min exclude: - # 32-bit is a Windows-only consideration + # Do not test pre-releases for versions out of SPEC0 + - os: ubuntu-latest + python-version: "3.9" + dependencies: pre - os: ubuntu-latest - architecture: x86 - - os: macos-latest - architecture: x86 - # Only run newest on Windows/Mac - - os: windows-latest - python-version: 3.8 - - os: windows-latest - python-version: 3.9 - - os: macos-latest - python-version: 3.8 - - os: macos-latest - python-version: 3.9 - # Skip 32-bit Windows with Python 3.10 (see #42) - - os: windows-latest - architecture: x86 - python-version: 3.10 + python-version: "3.10" + dependencies: pre + # If we reenable Windows/Mac tests, add the following exclusions: + # 32-bit is a Windows-only consideration + # Only run 2 newest Python on Windows/Mac + # Skip 32-bit Windows with Python 3.10+ (see #42) env: - PACKAGE: ${{ matrix.package }} - TEMPLATEFLOW_HOME: /tmp/templateflow + DEPENDS: ${{ matrix.dependencies }} steps: - uses: actions/checkout@v4 - - name: Fetch packages - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -109,49 +100,53 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends dvipng texlive texlive-latex-extra cm-super - - name: Install package - run: pip install $PACKAGE - - name: Verify installation - run: python -c "import nireports; print(nireports.__version__)" - - name: Install test dependencies - run: pip install .[test] - - name: Restore cached templateflow id: tf-cache-restore - uses: actions/cache/restore@v4 + uses: actions/cache@v4 with: path: /tmp/templateflow - key: templateflow-v1 - - name: Fetch templates + key: templateflow-v2 + # Fall back to and build on v1 + # If the cache need to be cleared, remove this when bumping key version + restore-keys: | + templateflow-v1 + - name: Pre-fetch templates run: | + uv pip install --system templateflow python -c "from templateflow.api import get; get('Fischer344', desc=None, suffix='T2w')" python -c "from templateflow.api import get; get('MNI152NLin6Asym', resolution=2, desc='LR', suffix='T1w')" - - name: Save templateflow cache - id: tf-cache-save - uses: actions/cache/save@v4 - with: - path: /tmp/templateflow - key: ${{ steps.tf-cache-restore.outputs.cache-primary-key }} + if: steps.tf-restore-cache.outputs.cache-hit != 'true' - - name: Run tests - run: pytest --cov nireports nireports - - uses: codecov/codecov-action@v5 - if: ${{ always() }} + - name: Install tox + run: | + uv tool install tox --with=tox-uv --with=tox-gh-actions + - name: Show tox config + run: tox c + - name: Run tox + run: tox -v --exit-and-dump-after 1200 + - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} + if: ${{ always() }} - deploy: + publish: + name: Publish released package to pypi.org if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: [test] runs-on: ubuntu-latest + needs: [build, test] + permissions: + attestations: write + id-token: write + steps: - - name: Fetch packages + - name: Download packages built by build-and-inspect-python-package uses: actions/download-artifact@v4 with: - name: dist - path: dist/ - - name: Upload to PyPI + name: Packages + path: dist + + - name: Upload package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + # Remove once OIDC is set up with: - user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index b67d93e9..5255c73f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,25 +12,26 @@ 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", ] -license = {file = "LICENSE"} -requires-python = ">=3.8" +license = "Apache-2.0" +requires-python = ">=3.9" dependencies = [ "importlib_resources >= 5.12; python_version < '3.12'", - "matplotlib >= 3.4.2", + "matplotlib >= 3.5", "nibabel >= 3.0.1", - "nilearn >= 0.5.2", - "nipype", - "numpy", - "pandas", - "pybids", - "pyyaml", - "seaborn", + "nilearn >= 0.8", + "nipype >= 1.8.5", + "numpy >= 1.20", + "pandas >= 1.2", + "pybids >= 0.15.1", + "pyyaml >= 5.4", + "seaborn >= 0.13", "svgutils >= 0.3.4", - "templateflow", + "templateflow >= 23.1", ] dynamic = ["version"] @@ -55,14 +56,13 @@ dev = [ ] test = [ - "coverage", - "matplotlib", + "coverage[toml] >=5.2.1", "packaging", - "pytest", - "pytest-cov", + "pytest >= 6", + "pytest-cov >= 2.11", "pytest-env", - "pytest-xdist", - "sphinx", + "pytest-xdist >= 2.5", + "sphinx >= 6", ] # Aliases @@ -150,8 +150,22 @@ quote-style = "double" known-first-party=["nireports"] [tool.pytest.ini_options] +minversion = "6" +testpaths = ["nireports"] +log_cli_level = "INFO" +xfail_strict = true norecursedirs = [".git"] -addopts = "-svx --doctest-modules -n auto" +addopts = [ + "-svx", + "-ra", + "--strict-config", + "--strict-markers", + "--doctest-modules", + # Config pytest-cov + "--cov=nireports", + "--cov-report=xml", + "--cov-config=pyproject.toml", +] doctest_optionflags = "ALLOW_UNICODE NORMALIZE_WHITESPACE ELLIPSIS" env = "PYTHONHASHSEED=0" filterwarnings = ["ignore::DeprecationWarning"] @@ -173,3 +187,12 @@ exclude_lines = [ 'raise NotImplementedError', 'warnings\.warn', ] + +[tool.codespell] +skip = "*/data/*,*/docs/_build/*" +ignore-words-list = "objekt" + +[tool.check-wheel-contents] +ignore = [ + "W002", +] diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..92f00deb --- /dev/null +++ b/tox.ini @@ -0,0 +1,126 @@ +[tox] +requires = + tox>=4 +envlist = + py3{9,10,11,12,13}-latest + py39-min + py3{11,12,13}-pre + style + spellcheck +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 + +[gh-actions:env] +DEPENDS = + min: min + latest: latest + pre: pre + +[testenv] +description = Pytest with coverage +labels = test +pip_pre = + pre: true +pass_env = + TEMPLATEFLOW_HOME + # Freesurfer variables searched for + FREESURFER_HOME + SUBJECTS_DIR + FS_LICENSE + # CI variables + TEST_DATA_HOME + TEST_OUTPUT_DIR + TEST_WORK_DIR + FMRIPREP_REGRESSION_SOURCE + CACHED_WORK_DIRECTORY + # CircleCI-specific + CIRCLE_NPROCS + SAVE_CIRCLE_ARTIFACTS + # getpass.getuser() sources for Windows: + LOGNAME + USER + LNAME + USERNAME + # Pass user color preferences through + PY_COLORS + FORCE_COLOR + NO_COLOR + CLICOLOR + CLICOLOR_FORCE + PYTHON_GIL +deps = + # Waiting on a release + 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 + pre: UV_EXTRA_INDEX_URL=https://pypi.anaconda.org/scientific-python-nightly-wheels/simple +uv_resolution = + min: lowest-direct + +commands = + pytest --durations=20 --durations-min=1.0 --cov-report term-missing {posargs:-n auto} + +[testenv:style] +description = Check our style guide +labels = check +deps = + ruff +skip_install = true +commands = + ruff check --diff + ruff format --diff + +[testenv:style-fix] +description = Auto-apply style guide to the extent possible +labels = pre-release +deps = + ruff +skip_install = true +commands = + ruff check --fix + ruff format + ruff check --select ISC001 + +[testenv:spellcheck] +description = Check spelling +labels = check +deps = + codespell[toml] +skip_install = true +commands = + codespell . {posargs} + +[testenv:build{,-strict}] +labels = + check + pre-release +deps = + build + twine +skip_install = true +set_env = + # Ignore specific known warnings: + # https://github.com/pypa/pip/issues/11684 + # https://github.com/pypa/pip/issues/12243 + strict: PYTHONWARNINGS=error,once:pkg_resources is deprecated as an API.:DeprecationWarning:pip._internal.metadata.importlib._envs,once:Unimplemented abstract methods {'locate_file'}:DeprecationWarning:pip._internal.metadata.importlib._dists +commands = + python -m build + python -m twine check dist/* + +[testenv:publish] +depends = build +labels = release +deps = + twine +skip_install = true +commands = + python -m twine upload dist/*