Repository: bids-standard/pybids (language: python)

Issue:
Build(deps): Bump scipy from 1.5 to 1.11.1
Bumps [scipy](https://github.com/scipy/scipy) from 1.5 to 1.11.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/scipy/scipy/releases">scipy's releases</a>.</em></p>
<blockquote>
<h1>SciPy 1.11.1 Release Notes</h1>
<p>SciPy <code>1.11.1</code> is a bug-fix release with no new features
compared to <code>1.11.0</code>. In particular, a licensing issue
discovered after the release of <code>1.11.0</code> has been addressed.</p>
<h1>Authors</h1>
<ul>
<li>Name (commits)</li>
<li>h-vetinari (1)</li>
<li>Robert Kern (1)</li>
<li>Ilhan Polat (4)</li>
<li>Tyler Reddy (8)</li>
</ul>
<p>A total of 4 people contributed to this release.
People with a &quot;+&quot; by their names contributed a patch for the first time.
This list of names is automatically generated, and may not be fully complete.</p>
<h1>SciPy 1.11.0 Release Notes</h1>
<p>SciPy <code>1.11.0</code> is the culmination of 6 months of hard work. It contains
many new features, numerous bug-fixes, improved test coverage and better
documentation. There have been a number of deprecations and API changes
in this release, which are documented below. All users are encouraged to
upgrade to this release, as there are a large number of bug-fixes and
optimizations. Before upgrading, we recommend that users check that
their own code does not use deprecated SciPy functionality (to do so,
run your code with <code>python -Wd</code> and check for <code>DeprecationWarning</code> s).
Our development attention will now shift to bug-fix releases on the
1.11.x branch, and on adding new features on the main branch.</p>
<p>This release requires Python <code>3.9+</code> and NumPy <code>1.21.6</code> or greater.</p>
<p>For running on PyPy, PyPy3 <code>6.0+</code> is required.</p>
<h1>Highlights of this release</h1>
<ul>
<li>Several <code>scipy.sparse</code> array API improvements, including <code>sparse.sparray</code>, a new
public base class distinct from the older <code>sparse.spmatrix</code> class,
proper 64-bit index support, and numerous deprecations paving the way to a
modern sparse array experience.</li>
<li><code>scipy.stats</code> added tools for survival analysis, multiple hypothesis testing,
sensitivity analysis, and working with censored data.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/scipy/scipy/commit/cfe80116aaa145061246b8aec0e98248fbefb835"><code>cfe8011</code></a> REL: 1.11.1 rel commit [wheel build]</li>
<li><a href="https://github.com/scipy/scipy/commit/450d8aa5a66ce7122f5a3e255421d1acc61655e9"><code>450d8aa</code></a> Merge pull request <a href="https://redirect.github.com/scipy/scipy/issues/18779">#18779</a> from tylerjereddy/treddy_1_11_1_prep</li>
<li><a href="https://github.com/scipy/scipy/commit/6f942e8aff0c3df9655a8cc2acbfc205ee12050f"><code>6f942e8</code></a> DOC: update 1.11.1 relnotes</li>
<li><a href="https://github.com/scipy/scipy/commit/145cec505685866ab524b1d3a8d507483e27bf17"><code>145cec5</code></a> MAINT: fix unuran licensing</li>
<li><a href="https://github.com/scipy/scipy/commit/0760bababfc16b7b08e724eb94de0a9ce3091d98"><code>0760bab</code></a> MAINT:linalg.det:Return scalars for singleton inputs (<a href="https://redirect.github.com/scipy/scipy/issues/18763">#18763</a>)</li>
<li><a href="https://github.com/scipy/scipy/commit/a1c6f99b5b99c10d2bfa39eb013a11f83b2864e6"><code>a1c6f99</code></a> MAINT:linalg:Use only NumPy types in lu</li>
<li><a href="https://github.com/scipy/scipy/commit/5cdc2fe96af0c56ea1a89e7c5373c26a9549e3af"><code>5cdc2fe</code></a> MAINT:linalg:Remove memcpy from lu</li>
<li><a href="https://github.com/scipy/scipy/commit/d9ac3f329cde53765073e6df5ff1d80f7bc40d39"><code>d9ac3f3</code></a> FIX:linalg:Guard against possible permute_l out of bound behavior</li>
<li><a href="https://github.com/scipy/scipy/commit/7ec501097b3a22569025bf0d62cb2d89474c812b"><code>7ec5010</code></a> BUG: fix handling for <code>factorial(..., exact=False)</code> for 0-dim array inputs (#...</li>
<li><a href="https://github.com/scipy/scipy/commit/90415c6890365585576e96e42c5aeba253da0091"><code>90415c6</code></a> BUG: Fix work array construction for various weight shapes. (<a href="https://redirect.github.com/scipy/scipy/issues/18741">#18741</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/scipy/scipy/compare/v1.5.0...v1.11.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=scipy&package-manager=pip&previous-version=1.5&new-version=1.11.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/bids-standard/pybids/network/alerts).

</details>


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 062faa58e..90de696f2 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -9,18 +9,31 @@ on:
       - docs/*
     tags:
       - '*'
+  pull_request:
+  workflow_dispatch:
 
 defaults:
   run:
     shell: bash
 
+# Force tox to use color
+env:
+  FORCE_COLOR: true
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
 jobs:
   docs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
         include:
-          - python-version: 3.9
+          - python-version: '3.11'
     steps:
       - uses: actions/checkout@v4
         with:
@@ -32,10 +45,8 @@ jobs:
           python-version: ${{ matrix.python-version }}
       - name: Display Python version
         run: python -c "import sys; print(sys.version)"
-      - name: Install pybids
-        run: pip install .[doc]
       - name: Build documentation
-        run: make -C doc html
+        run: pipx run tox -e docs
       - name: Upload docs as artifacts
         uses: actions/upload-artifact@v4
         with:
diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml
deleted file mode 100644
index f1cb3f6bb..000000000
--- a/.github/workflows/pre-release.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Pre-release checks
-
-# This file tests against pre-release wheels for dependencies
-
-on:
-  push:
-    branches:
-      - master
-      - maint/*
-  pull_request:
-    branches:
-      - master
-      - maint/*
-  schedule:
-    # 8am EST / 9am EDT Mondays
-    - cron: '0 13 * * 1'
-
-defaults:
-  run:
-    shell: bash
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: true
-
-jobs:
-  pre-release:
-    # Check pre-releases of dependencies on stable Python
-    runs-on: ${{ matrix.os }}
-    continue-on-error: true
-    strategy:
-      matrix:
-        os: ['ubuntu-latest', 'macos-latest']
-        python-version: [3.9, "3.10", "3.11"]
-        install: ['pip']
-        check: ['ci_tests']
-        pip-flags: ['PRE_PIP_FLAGS']
-        requirements: ['']
-      fail-fast: false
-    env:
-      INSTALL_TYPE: ${{ matrix.install }}
-      CHECK_TYPE: ${{ matrix.check }}
-      EXTRA_PIP_FLAGS: ${{ matrix.pip-flags }}
-      OS_TYPE: ${{ matrix.os }}
-      REQUIREMENTS: ${{ matrix.requirements }}
-
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          submodules: recursive
-          fetch-depth: 0
-      - name: Install dependencies
-        run: tools/ci/install_dependencies.sh
-      - 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: Create virtual environment
-        run: tools/ci/create_venv.sh
-      - name: Build archive
-        run: |
-          source tools/ci/build_archive.sh
-          echo "ARCHIVE=$ARCHIVE" >> $GITHUB_ENV
-      - name: Install pybids
-        run: tools/ci/install.sh
-      - name: Install extras
-        run: tools/ci/install_extras.sh
-      - name: Run tests
-        run: |
-          source tools/ci/activate.sh
-          make $CHECK_TYPE
-        if: ${{ matrix.check != 'skiptests' }}
-      - uses: codecov/codecov-action@v4
-        with:
-          file: cov.xml
-          token: ${{ secrets.CODECOV_TOKEN }}
-        if: ${{ always() }}
diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml
deleted file mode 100644
index c02933ee4..000000000
--- a/.github/workflows/stable.yml
+++ /dev/null
@@ -1,104 +0,0 @@
-name: Stable tests
-
-# This file tests the claimed support range of PyBIDS including
-#
-# * Operating systems: Linux, OSX
-# * Installation methods: pip, sdist, wheel
-
-on:
-  push:
-    branches:
-      - master
-      - maint/*
-  pull_request: {}
-  schedule:
-    # 8am EST / 9am EDT M-F
-    - cron: '0 13 * * 1,2,3,4,5'
-
-defaults:
-  run:
-    shell: bash
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: true
-
-jobs:
-  stable:
-    # Check each OS, all supported Python, minimum versions and latest releases
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: ['ubuntu-latest', 'macos-latest']
-        python-version: [3.8, 3.9, "3.10", "3.11"]
-        install: ['pip']
-        check: ['ci_tests']
-        pip-flags: ['']
-        requirements: ['']
-        include:
-          # Build docs
-          - os: ubuntu-latest
-            python-version: 3.11
-            install: pip
-            check: doc
-            pip-flags: ''
-          # Check all installation methods
-          - os: ubuntu-latest
-            python-version: 3.11
-            install: wheel
-            check: ci_tests
-            pip-flags: ''
-            requirements: ''
-          - os: ubuntu-latest
-            python-version: 3.11
-            install: sdist
-            check: ci_tests
-            pip-flags: ''
-            requirements: ''
-          - os: ubuntu-latest
-            python-version: 3.8
-            install: pip
-            check: ci_tests
-            pip-flags: ''
-            requirements: '-r min-requirements.txt'
-      fail-fast: false
-    env:
-      INSTALL_TYPE: ${{ matrix.install }}
-      CHECK_TYPE: ${{ matrix.check }}
-      EXTRA_PIP_FLAGS: ${{ matrix.pip-flags }}
-      OS_TYPE: ${{ matrix.os }}
-      REQUIREMENTS: ${{ matrix.requirements }}
-
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          submodules: recursive
-          fetch-depth: 0
-      - name: Install dependencies
-        run: tools/ci/install_dependencies.sh
-      - 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: Create virtual environment
-        run: tools/ci/create_venv.sh
-      - name: Build archive
-        run: |
-          source tools/ci/build_archive.sh
-          echo "ARCHIVE=$ARCHIVE" >> $GITHUB_ENV
-      - name: Install pybids
-        run: tools/ci/install.sh
-      - name: Install extras
-        run: tools/ci/install_extras.sh
-      - name: Run tests
-        run: |
-          source tools/ci/activate.sh
-          make $CHECK_TYPE
-        if: ${{ matrix.check != 'skiptests' }}
-      - uses: codecov/codecov-action@v4
-        with:
-          file: cov.xml
-          token: ${{ secrets.CODECOV_TOKEN }}
-        if: ${{ always() }}
diff --git a/Makefile b/Makefile
deleted file mode 100644
index a9302916c..000000000
--- a/Makefile
+++ /dev/null
@@ -1,7 +0,0 @@
-.PHONY: doc travis_tests
-
-ci_tests:
-	pytest --doctest-modules -n 2 -v --cov bids --cov-config .coveragerc --cov-report xml:cov.xml bids
-
-doc:
-	$(MAKE) -C doc html
diff --git a/README.md b/README.md
index 1ef250f5a..214c85e4a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ If you want to live on the bleeding edge, you can install from master:
 
 > pip install git+https://github.com/bids-standard/pybids.git
 
-#### Dependencies
+### Dependencies
 PyBIDS has a number of dependencies. The core querying functionality requires only the `BIDS-Validator` package. However, most other modules require the core Python neuroimaging stack: `numpy`, `scipy`, `pandas`, and `nibabel`. The `reports` module additionally requires `num2words`. By default, all dependencies will be installed with pybids (if they aren't already available).
 
 ## Usage
@@ -33,6 +33,30 @@ Get started by checking out [the documentation](https://bids-standard.github.io/
 
 Or you can start at [our tutorial](https://bids-standard.github.io/pybids/examples/pybids_tutorial.html)! You can run it interactively without installing anything via [binder](https://mybinder.org/v2/gh/bids-standard/pybids/master). Click on the link and then navigate to `doc/examples/pybids_tutorial.md` to explore.
 
+## Development and testing
+PyBIDS uses [tox](https://tox.wiki/) to manage testing environments and ensure that the same
+commands can easily be run locally and in CI.
+
+Install tox with:
+
+```
+pip install tox
+```
+
+To run tests with standard, minimal, or pre-release dependencies, use:
+
+```
+tox run -e [full|min|pre]
+```
+
+To run with a specific version, also specify `py3X`:
+
+```
+tox run -e py311-full
+```
+
+See `tox` documentation for more detail.
+
 ## How to cite
 To credit PyBIDS in your work, please cite both the [JOSS paper](https://joss.theoj.org/papers/10.21105/joss.01294) and the [Zenodo archive](https://zenodo.org/record/3363985). The former provides a high level description of the package, and the latter points to a permanent record of all PyBIDS versions (we encourage you to cite the specific version you used). Example citations (for PyBIDS 0.9.3):
 
diff --git a/bids/layout/config/bids.json b/bids/layout/config/bids.json
index d1db778ae..58be73a6b 100644
--- a/bids/layout/config/bids.json
+++ b/bids/layout/config/bids.json
@@ -115,7 +115,7 @@
         }
     ],
     "default_path_patterns": [
-        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_part-{part}]_{suffix<T1w|T2w|T2star|T2starw|FLAIR|FLASH|PD|PDw|PDT2|inplaneT[12]|angio>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
+        "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_part-{part}][_chunk-{chunk}]_{suffix<T1w|T2w|T2star|T2starw|FLAIR|FLASH|PD|PDw|PDT2|inplaneT[12]|angio>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
         "sub-{subject}[/ses-{session}]/{datatype<anat>|anat}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_mod-{modality}]_{suffix<defacemask>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
         "sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}]_{suffix<bold|cbv|sbref>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
         "sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}]_{suffix<phase>}{extension<.nii|.nii.gz|.json>|.nii.gz}",
@@ -164,7 +164,7 @@
         "sub-{subject}[/ses-{session}]/{datatype<perf>|perf}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}]_{suffix<aslcontext>}{extension<.tsv|.json>}",
         "sub-{subject}[/ses-{session}]/{datatype<perf>|perf}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_run-{run}]_{suffix<asllabeling>}{extension<.jpg>}",
         "sub-{subject}[/ses-{session}]/{datatype<perf>|perf}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_recording{recording}]_{suffix<physio|stim>}{extension<.tsv|.json>}",  
-        "[task-{task}_][acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_][run-{run}_]{suffix<T1w|T2w|T2star|T2starw|FLAIR|FLASH|PD|PDw|PDT2|inplaneT[12]|angio>}{extension<.json>|.json}",
+        "[task-{task}_][acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_][run-{run}_][echo-{echo}_][part-{part}_][chunk-{chunk}_]{suffix<T1w|T2w|T2star|T2starw|FLAIR|FLASH|PD|PDw|PDT2|inplaneT[12]|angio>}{extension<.json>|.json}",
         "[task-{task}_][acq-{acquisition}_][ce-{ceagent}_][rec-{reconstruction}_][run-{run}_][mod-{modality}_]{suffix<defacemask>}{extension<.json>|.json}",
         "task-{task}[_acq-{acquisition}][_run-{run}]_{suffix<nirs>}{extension<.json>}",
         "task-{task}[_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_echo-{echo}]_{suffix<bold|cbv|phase|sbref>}{extension<.json>|.json}",
diff --git a/min-requirements.txt b/min-requirements.txt
deleted file mode 100644
index 74d4abfed..000000000
--- a/min-requirements.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-# Auto-generated by tools/update_requirements.py
-numpy ==1.19
-scipy ==1.5
-nibabel ==3.0
-pandas ==0.25.2
-formulaic ==0.2.4, <0.6
-sqlalchemy ==1.3.16
-bids-validator==1.11
-num2words ==0.5.5
-click ==8.0
diff --git a/pyproject.toml b/pyproject.toml
index bc43fffa3..528d5775b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,7 +49,8 @@ test = [
   "pytest-cov",
   "bsmschema",
   "coverage[toml]",
-  "altair"
+  "altair",
+  "pytest-xdist",
 ]
 model_reports = [
   "jinja2",
@@ -62,10 +63,10 @@ tutorial = [
   "jupyter_client",
   "ipykernel",
 ]
-ci_tests = ["pybids[test]", "pytest-xdist"]
 # Aliases
 docs = ["pybids[doc]"]
 tests = ["pybids[test]"]
+ci_tests = ["pybids[test]"]
 dev = ["pybids[doc,test,plotting]"]
 
 [project.urls]
@@ -84,7 +85,6 @@ versionfile_build = "bids/_version.py"
 [tool.coverage.run]
 branch = true
 source = ["bids/*"]
-include = ["bids/*"]
 omit = [
   "*/setup.py",
   "*/external/*",
diff --git a/tools/ci/activate.sh b/tools/ci/activate.sh
deleted file mode 100644
index 567e13a67..000000000
--- a/tools/ci/activate.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-if [ -e virtenv/bin/activate ]; then
-    source virtenv/bin/activate
-elif [ -e virtenv/Scripts/activate ]; then
-    source virtenv/Scripts/activate
-else
-    echo Cannot activate virtual environment
-    ls -R virtenv
-    false
-fi
diff --git a/tools/ci/build_archive.sh b/tools/ci/build_archive.sh
deleted file mode 100755
index c8b1d9810..000000000
--- a/tools/ci/build_archive.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-
-echo "Building archive"
-
-source tools/ci/activate.sh
-
-set -eu
-
-# Required dependencies
-echo "INSTALL_TYPE = $INSTALL_TYPE"
-
-set -x
-
-if [ "$INSTALL_TYPE" = "sdist" -o "$INSTALL_TYPE" = "wheel" ]; then
-    python -m build
-elif [ "$INSTALL_TYPE" = "archive" ]; then
-    ARCHIVE="/tmp/package.tar.gz"
-    git archive -o $ARCHIVE HEAD
-fi
-
-if [ "$INSTALL_TYPE" = "sdist" ]; then
-    ARCHIVE=$( ls $PWD/dist/*.tar.gz )
-elif [ "$INSTALL_TYPE" = "wheel" ]; then
-    ARCHIVE=$( ls $PWD/dist/*.whl )
-elif [ "$INSTALL_TYPE" = "pip" ]; then
-    ARCHIVE="$PWD"
-fi
-
-if [ "$INSTALL_TYPE" = "sdist" -o "$INSTALL_TYPE" = "wheel" ]; then
-    python -m pip install twine
-    python -m twine check $ARCHIVE
-fi
-
-set +eux
diff --git a/tools/ci/create_venv.sh b/tools/ci/create_venv.sh
deleted file mode 100755
index 7a2876739..000000000
--- a/tools/ci/create_venv.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-
-echo Creating isolated virtual environment
-
-source tools/ci/env.sh
-
-set -eu
-
-# Required variables
-echo SETUP_REQUIRES = $SETUP_REQUIRES
-
-set -x
-
-python -m pip install --upgrade pip virtualenv
-virtualenv --python=python virtenv
-source tools/ci/activate.sh
-python --version
-python -m pip install -U $SETUP_REQUIRES
-which python
-which pip
-
-set +eux
-
-echo Done creating isolated virtual environment
diff --git a/tools/ci/env.sh b/tools/ci/env.sh
deleted file mode 100644
index f9fb458b2..000000000
--- a/tools/ci/env.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-SETUP_REQUIRES="pip build"
-
-# Numpy and scipy upload nightly/weekly/intermittent wheels
-NIGHTLY_WHEELS="https://pypi.anaconda.org/scipy-wheels-nightly/simple"
-STAGING_WHEELS="https://pypi.anaconda.org/multibuild-wheels-staging/simple"
-PRE_PIP_FLAGS="--pre --extra-index-url $NIGHTLY_WHEELS --extra-index-url $STAGING_WHEELS"
diff --git a/tools/ci/install.sh b/tools/ci/install.sh
deleted file mode 100755
index 7c54f0aac..000000000
--- a/tools/ci/install.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-echo Installing pybids
-
-source tools/ci/activate.sh
-source tools/ci/env.sh
-
-set -eu
-
-# Required variables
-echo INSTALL_TYPE = $INSTALL_TYPE
-echo CHECK_TYPE = $CHECK_TYPE
-echo EXTRA_PIP_FLAGS = $EXTRA_PIP_FLAGS
-echo REQUIREMENTS = $REQUIREMENTS
-
-set -x
-
-if [ -n "$EXTRA_PIP_FLAGS" ]; then
-    EXTRA_PIP_FLAGS=${!EXTRA_PIP_FLAGS}
-fi
-
-pip install $EXTRA_PIP_FLAGS $REQUIREMENTS $ARCHIVE
-
-# Basic import check
-python -c 'import bids; print(bids.__version__)'
-
-set +eux
-
-echo Done installing pybids
diff --git a/tools/ci/install_dependencies.sh b/tools/ci/install_dependencies.sh
deleted file mode 100755
index 7e0e33fe7..000000000
--- a/tools/ci/install_dependencies.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-echo "Installing dependencies"
-
-set -eu
-
-# Required variables
-echo OS_TYPE = $OS_TYPE
-
-if [ "$OS_TYPE" = "ubuntu-latest" ]; then
-    sudo apt update
-    sudo apt install -y graphviz
-elif  [ "$OS_TYPE" = "macos-latest" ]; then
-    brew install graphviz
-else
-    echo "Unknown OS_TYPE: $OS_TYPE"
-fi
-
-set +eux
-
-echo Done installing dependencies
diff --git a/tools/ci/install_extras.sh b/tools/ci/install_extras.sh
deleted file mode 100755
index 7a1897d92..000000000
--- a/tools/ci/install_extras.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-
-echo Installing dependencies
-
-source tools/ci/activate.sh
-source tools/ci/env.sh
-
-set -eu
-
-# Required variables
-echo EXTRA_PIP_FLAGS = $EXTRA_PIP_FLAGS
-echo CHECK_TYPE = $CHECK_TYPE
-
-set -x
-
-if [ -n "$EXTRA_PIP_FLAGS" ]; then
-    EXTRA_PIP_FLAGS=${!EXTRA_PIP_FLAGS}
-fi
-
-pip install $EXTRA_PIP_FLAGS "pybids[$CHECK_TYPE]"
-
-set +eux
-
-echo Done installing dependencies
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 000000000..b17054b5a
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,96 @@
+[tox]
+requires =
+  tox>=4
+envlist =
+  py3{8,9,10,11,12}-{full,pre}
+  py38-min
+skip_missing_interpreters = true
+
+# Configuration that allows us to split tests across GitHub runners effectively
+[gh-actions]
+python =
+  3.8: py38
+  3.9: py39
+  3.10: py310
+  3.11: py311
+  3.12: py312
+
+[gh-actions:env]
+DEPENDS =
+  min: min
+  full: full
+  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
+extras = test
+deps =
+  min: numpy ==1.19
+  min: scipy ==1.5
+  min: nibabel ==3.0
+  min: pandas ==0.25.2
+  # Tested on 0.2.4-0.5.2
+  min: formulaic ==0.2.4
+  min: sqlalchemy ==1.3.16
+  min: num2words ==0.5.5
+  min: click ==8.0
+
+commands =
+  pytest --doctest-modules -v \
+    --cov bids --cov-report xml:cov.xml --cov-report term \
+    bids {posargs:-n auto}
+
+[testenv:docs]
+description = Build documentation site
+labels = docs
+allowlist_externals = make
+extras = doc
+commands =
+  make -C doc html
+
+[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 =
+  build-strict: PYTHONWARNINGS=error
+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/*


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 000000000..a441f1e40
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,83 @@
+name: Python tests
+
+# This file tests the claimed support range of PyBIDS including
+#
+# * Operating systems: Linux, OSX
+
+on:
+  push:
+    branches:
+      - master
+      - maint/*
+  pull_request: {}
+  schedule:
+    # 8am EST / 9am EDT Mondays
+    - cron: '0 13 * * 1'
+  # Allow job to be triggered manually from GitHub interface
+  workflow_dispatch:
+
+defaults:
+  run:
+    shell: bash
+
+# Force tox and pytest to use color
+env:
+  FORCE_COLOR: true
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
+jobs:
+  test:
+    # Check each OS, all supported Python, minimum versions and latest releases
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: ['ubuntu-latest', 'macos-latest']
+        python-version: [3.8, 3.9, "3.10", "3.11"]
+        dependencies: ['full', 'pre']
+        include:
+          - os: ubuntu-latest
+            python-version: 3.8
+            dependencies: min
+        exclude:
+          # Drop pre tests for SPEC-0-unsupported Python versions
+          # See https://scientific-python.org/specs/spec-0000/
+          - python-version: '3.8'
+            dependencies: pre
+          - python-version: '3.9'
+            dependencies: pre
+      fail-fast: false
+
+    env:
+      DEPENDS: ${{ matrix.dependencies }}
+
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+          fetch-depth: 0
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python-version }}
+          allow-prereleases: true
+      - name: Display Python version
+        run: python -c "import sys; print(sys.version)"
+      - name: Install tox
+        run: |
+          python -m pip install --upgrade pip
+          python -m pip install tox 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
+        if: ${{ always() }}
+        with:
+          files: cov.xml
+          token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/bids/layout/tests/test_layout.py b/bids/layout/tests/test_layout.py
index 93bf81d0c..664fdd04c 100644
--- a/bids/layout/tests/test_layout.py
+++ b/bids/layout/tests/test_layout.py
@@ -789,15 +789,16 @@ def test_get_tr(layout_7t_trt):
 def test_to_df(layout_ds117):
     # Only filename entities
     df = layout_ds117.to_df()
-    assert df.shape == (115, 12)
-    target = {'datatype', 'fmap', 'run', 'path', 'acquisition', 'scans',
+    n_files = len(layout_ds117.files)
+    assert len(df) == n_files
+    target = {'datatype', 'fmap', 'run', 'path', 'acquisition', 'scans', 'echo',
               'session', 'subject', 'suffix', 'task', 'proc', 'extension'}
     assert set(df.columns) == target
     assert set(df['subject'].dropna().unique()) == {'01', '02', 'emptyroom'}
 
     # Include metadata entities
     df = layout_ds117.to_df(metadata=True)
-    assert df.shape == (115, 56)
+    assert df.shape == (n_files, len(target) + 44)
     assert not ({'InstitutionAddress', 'TriggerChannelCount', 'EchoTime'} -
                 set(df.columns))