From 5d9e66665cc471f2dfed43a6cad8cda296515e76 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Fri, 15 Nov 2024 15:50:24 -0500 Subject: [PATCH] chore: Switch to tox for testing and CI (#900) * chore(deps): Update lower bounds so uv lowest-direct passes tests * chore(tox): Adapt tox from fmriprep, use uv_resolution * chore(ci): Switch to tox-gh-actions for testing * Replace test workflow with tox * chore: Pin pyyaml 5.4 * chore: Update pytest/coverage config * ci: Add FORCE_COLOR to CI environment * ci: Skip pre-release tests for 3.9/3.10 --- .github/workflows/pythonpackage.yml | 229 ++++++++-------------------- pyproject.toml | 72 +++++---- tox.ini | 106 +++++++++++++ 3 files changed, 216 insertions(+), 191 deletions(-) create mode 100644 tox.ini diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0572362d613..9d2ea662066 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,7 +1,4 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python package +name: Tox on: push: @@ -13,42 +10,17 @@ on: - cron: '0 0 * * *' concurrency: - group: python-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -jobs: - job_metadata: - if: github.repository == 'nipreps/niworkflows' - runs-on: ubuntu-latest - outputs: - commit_message: ${{ steps.get_commit_message.outputs.commit_message }} - version: ${{ steps.show_version.outputs.version }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Print head git commit message - id: get_commit_message - run: | - if [[ -z "$COMMIT_MSG" ]]; then - COMMIT_MSG=$(git show -s --format=%s $REF) - fi - echo commit_message=$COMMIT_MSG | tee -a $GITHUB_OUTPUT - env: - COMMIT_MSG: ${{ github.event.head_commit.message }} - REF: ${{ github.event.pull_request.head.sha }} - - name: Detect version - id: show_version - run: | - if [[ "$GITHUB_REF" == refs/tags/* ]]; then - VERSION=${GITHUB_REF##*/} - else - pipx run hatch version # Once to avoid output of initial setup - VERSION=$( pipx run hatch version ) - fi - echo version=$VERSION | tee -a $GITHUB_OUTPUT +permissions: + contents: read + +env: + # Force tox and pytest to use color + FORCE_COLOR: true +jobs: build: if: github.repository == 'nipreps/niworkflows' runs-on: ubuntu-latest @@ -118,140 +90,73 @@ jobs: datalad get -J 2 -r ds000003 ds000030/sub-10228/func test: - needs: [build, get_data, job_metadata] + needs: [get_data] runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] - install: [repo] + dependencies: [latest, pre] include: - - python-version: "3.12" - install: sdist - - python-version: "3.12" - install: wheel - - python-version: "3.12" - install: editable - - env: - INSTALL_TYPE: ${{ matrix.install }} - - steps: - - uses: actions/checkout@v4 - if: matrix.install == 'repo' || matrix.install == 'editable' - with: - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Load test data cache - uses: actions/cache@v4 - id: stanford-crn - with: - path: ~/.cache/stanford-crn/ - key: data-v0-${{ github.ref_name }}-${{ github.sha }} - - name: Load TemplateFlow cache - uses: actions/cache@v4 - id: templateflow - with: - path: ~/.cache/templateflow - key: templateflow-v0-${{ github.ref_name }}-${{ strategy.job-index }}-${{ github.sha }} - restore-keys: | - templateflow-v0-${{ github.ref_name }}- - templateflow-v0- - - name: Fetch packages - if: matrix.install == 'sdist' || matrix.install == 'wheel' - uses: actions/download-artifact@v3 - with: - name: dist - path: dist/ - - name: Select archive - run: | - if [ "$INSTALL_TYPE" = "sdist" ]; then - ARCHIVE=$( ls dist/*.tar.gz ) - elif [ "$INSTALL_TYPE" = "wheel" ]; then - ARCHIVE=$( ls dist/*.whl ) - elif [ "$INSTALL_TYPE" = "repo" ]; then - ARCHIVE="." - elif [ "$INSTALL_TYPE" = "editable" ]; then - ARCHIVE="-e ." - fi - echo "ARCHIVE=$ARCHIVE" | tee -a $GITHUB_ENV - - name: Install package - run: python -m pip install $ARCHIVE - - name: Check version - run: | - INSTALLED_VERSION=$(python -c 'import niworkflows; print(niworkflows.__version__, end="")') - echo "INSTALLED: \"${INSTALLED_VERSION}\"" - test "${INSTALLED_VERSION}" = "${VERSION}" - env: - VERSION: ${{ needs.job_metadata.outputs.version }} - - name: Install test dependencies - run: python -m pip install "niworkflows[tests]" - - name: Run tests - run: pytest -sv --doctest-modules --cov niworkflows --pyargs niworkflows - - uses: codecov/codecov-action@v4 - name: Submit to CodeCov - with: - token: ${{ secrets.CODECOV_TOKEN }} - - test-pre: - needs: [get_data, job_metadata] - if: ${{ !contains(needs.job_metadata.outputs.commit_message, '[skip pre]') }} - runs-on: ubuntu-latest - strategy: - matrix: - # Only run --pre tests on Python versions within SPEC0 support - python-version: ["3.11", "3.12"] - install: [repo] - pip-flags: ['--pre'] + - python-version: "3.9" + 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 env: - INSTALL_TYPE: ${{ matrix.install }} - PIP_FLAGS: ${{ matrix.pip-flags }} + DEPENDS: ${{ matrix.dependencies }} steps: - - name: Debug commit message - run: echo "${{ needs.job_metadata.outputs.commit_message }}" - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Load test data cache - uses: actions/cache@v4 - id: stanford-crn - with: - path: ~/.cache/stanford-crn/ - key: data-v0-${{ github.ref_name }}-${{ github.sha }} - - name: Load TemplateFlow cache - uses: actions/cache@v4 - id: templateflow - with: - path: ~/.cache/templateflow - key: templateflow-v0-${{ github.ref_name }}-${{ strategy.job-index }}-${{ github.sha }} - restore-keys: | - templateflow-v0-${{ github.ref_name }}- - templateflow-v0- - - name: Install package - run: python -m pip install $PIP_FLAGS . - - name: Check version - run: | - INSTALLED_VERSION=$(python -c 'import niworkflows; print(niworkflows.__version__, end="")') - echo "INSTALLED: \"${INSTALLED_VERSION}\"" - test "${INSTALLED_VERSION}" = "${VERSION}" - env: - VERSION: ${{ needs.job_metadata.outputs.version }} - - name: Install test dependencies - run: python -m pip install $PIP_FLAGS "niworkflows[tests]" - - name: Run tests - run: pytest -sv --doctest-modules --cov niworkflows --pyargs niworkflows - - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - name: Submit to CodeCov + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v3 + - name: Load test data cache + uses: actions/cache@v4 + id: stanford-crn + with: + path: ~/.cache/stanford-crn/ + key: data-v0-${{ github.ref_name }}-${{ github.sha }} + - name: Load TemplateFlow cache + uses: actions/cache@v4 + id: templateflow + with: + path: ~/.cache/templateflow + key: templateflow-v0-${{ github.ref_name }}-${{ strategy.job-index }}-${{ github.sha }} + restore-keys: | + templateflow-v0-${{ github.ref_name }}- + templateflow-v0- + - uses: actions/cache@v4 + with: + path: ~/.cache/templateflow + key: templateflow-v1 + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y --no-install-recommends graphviz + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - 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: + file: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + if: ${{ always() }} flake8: if: github.event_name != 'schedule' diff --git a/pyproject.toml b/pyproject.toml index 64a26c50f84..5624790eb3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,26 +24,26 @@ classifiers = [ ] dependencies = [ "acres", - "attrs", + "attrs >=20.1", "importlib_resources >= 5.7; python_version < '3.11'", - "jinja2", + "jinja2 >=3", "looseversion", - "matplotlib >= 3.4.2", + "matplotlib >= 3.5", "nibabel >= 3.0", - "nilearn >= 0.5.2", + "nilearn >= 0.8", "nipype >= 1.8.5", - "nitransforms >= 21.0.0", - "numpy", + "nitransforms >= 22.0.0", + "numpy >= 1.20", "packaging", - "pandas", + "pandas >= 1.2", "pybids >= 0.15.1", - "PyYAML", - "scikit-image", - "scipy", - "seaborn", + "PyYAML >= 5.4", + "scikit-image >= 0.18", + "scipy >= 1.8", + "seaborn >= 0.11", "svgutils >= 0.3.4", - "templateflow >= 0.7.2", - "transforms3d", + "templateflow >= 23.1", + "transforms3d >= 0.4", ] [project.optional-dependencies] @@ -62,12 +62,12 @@ style = [ "flake8 >= 3.7.0", ] tests = [ - "coverage >=5.2.1", - "pytest >= 4.4", - "pytest-cov", + "coverage[toml] >=5.2.1", + "pytest >= 6", + "pytest-cov >= 2.11", "pytest-env", - "pytest-xdist >= 1.28", - "pytest-xvfb", + "pytest-xdist >= 2.5", + "pytest-xvfb >= 2", ] # Aliases all = ["niworkflows[doc,pointclouds,style,tests]"] @@ -116,23 +116,31 @@ skip-string-normalization = true extend-exclude = '_version.py' [tool.pytest.ini_options] -norecursedirs = ".git" -addopts = "-sv --doctest-modules" +minversion = "6" +testpaths = ["niworkflows"] +log_cli_level = "INFO" +xfail_strict = true +norecursedirs = [".git"] +addopts = [ + "-svx", + "-ra", + "--strict-config", + "--strict-markers", + "--doctest-modules", + # Config pytest-cov + "--cov=niworkflows", + "--cov-report=xml", + "--cov-config=pyproject.toml", +] doctest_optionflags = "ALLOW_UNICODE NORMALIZE_WHITESPACE ELLIPSIS" -env = """ - PYTHONHASHSEED=0 -""" -filterwarnings = """ - ignore::DeprecationWarning -""" +env = "PYTHONHASHSEED=0" +filterwarnings = ["ignore::DeprecationWarning"] junit_family = "xunit2" [tool.coverage.run] branch = true omit = [ - "*/tests/*", - "niworkflows/_version.py", - "niworkflows/conftest.py", + "*/_version.py", ] [tool.coverage.report] @@ -141,3 +149,9 @@ exclude_lines = [ "raise NotImplementedError", "warnings\\.warn", ] + +[tool.coverage.paths] +source = [ + "niworkflows", + "**/site-packages/niworkflows" +] diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000000..d3d58f325a9 --- /dev/null +++ b/tox.ini @@ -0,0 +1,106 @@ +[tox] +requires = + tox>=4 +envlist = + py3{9,10,11,12}-latest + py39-min + py3{10,11,12}-pre +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 + +[gh-actions:env] +DEPENDS = + min: min + latest: latest + pre: pre + +[testenv] +description = Pytest with coverage +labels = test +pip_pre = + pre: true +pass_env = + # 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 +extras = tests +setenv = + pre: PIP_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_pre = +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/*