Repository: PlasmaPy/PlasmaPy

Issue:
Run tests with `Nox` instead of `tox`
We currently use [tox](https://tox.wiki/en/latest/) to automate our tests.  It is a really good tool, with the tradeoff that the configuration file, [`tox.ini`](https://github.com/PlasmaPy/PlasmaPy/blob/main/tox.ini), is in a rather esoteric format which makes it hard to update & edit tests.  (At the very least, I personally have trouble understanding it.)  Another limitation is that it's hard to dynamically generate test environments.

An alternative is [nox](https://nox.thea.codes/en/stable/), which is similar to tox, except that the test configuration file is written in Python.  We're a lot more familiar with Python, so that will make the tests easier to maintain.  Plus, we can more easily dynamically generate tests.

One motivation for switching to nox would be that we would be able to get the minimum requirements from our requirements files, rather than having to maintain requirements in one more place (currently in our `tox.ini` file).  This would help us with #1276.

A while back I set up a [`noxfile.py`](https://github.com/HinodeXRT/xrtpy/blob/main/noxfile.py) for xrtpy, and was thinking it would be worthwhile to set up a prototype for PlasmaPy as well.

If we do switch to nox, we would also need to update our testing guide.


Discussion:

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -89,6 +89,8 @@
     "**.ipynb_checkpoints",
     "**Untitled*",
     ".DS_Store",
+    ".nox",
+    ".tox",
     "_build",
     "notebooks/langmuir_samples",
     "plasmapy_sphinx",
@@ -99,7 +101,7 @@
 html_extra_path = ["robots.txt"]
 html_favicon = "./_static/icon.ico"
 modindex_common_prefix = ["plasmapy."]
-pygments_style = "default"  # code highlighting style to meet WCAG AA contrast standard
+pygments_style = "default"  # code highlighting to meet WCAG AA contrast standard
 root_doc = "index"
 source_suffix = ".rst"
 templates_path = ["_templates"]
diff --git a/noxfile.py b/noxfile.py
--- a/noxfile.py
+++ b/noxfile.py
@@ -15,6 +15,7 @@
 PlasmaPy dependencies, and not when code coverage checks are performed.
 """
 
+import os
 import sys
 
 import nox
@@ -114,15 +115,17 @@ def requirements(session):
 
 with_coverage: tuple[str, ...] = (
     "--cov=plasmapy",
+    "--cov-report=xml",
     "--cov-config=pyproject.toml",
     "--cov-append",
-    "--cov-report xml:coverage.xml",
+    "--cov-report",
+    "xml:coverage.xml",
 )
 
 skipslow: tuple[str, ...] = ("-m", "not slow")
 
-test_specifiers: list[nox._parametrize.Param] = [
-    nox.param("all", id="all"),
+test_specifiers: list = [
+    nox.param("run all tests", id="all"),
     nox.param("with code coverage", id="cov"),
     nox.param("skip slow tests", id="skipslow"),
     nox.param("lowest-direct", id="lowest-direct"),
@@ -131,7 +134,7 @@ def requirements(session):
 
 @nox.session(python=supported_python_versions)
 @nox.parametrize("test_specifier", test_specifiers)
-def tests(session, test_specifier: nox._parametrize.Param):
+def tests(session: nox.Session, test_specifier: nox._parametrize.Param):
     """Run tests with pytest."""
 
     resolution = "lowest-direct" if test_specifier == "lowest-direct" else "highest"
@@ -144,10 +147,10 @@ def tests(session, test_specifier: nox._parametrize.Param):
 
     options: list[str] = []
 
-    if test_specifier == "skipslow":
+    if test_specifier == "skip slow tests":
         options += skipslow
 
-    if test_specifier == "cov":
+    if test_specifier == "with code coverage":
         options += with_coverage
 
     # Doctests are only run with the most recent versions of Python and
@@ -156,6 +159,9 @@ def tests(session, test_specifier: nox._parametrize.Param):
     if session.python == maxpython and test_specifier in {"all", "skipslow"}:
         options += with_doctests
 
+    if gh_token := os.getenv("GH_TOKEN"):
+        session.env["GH_TOKEN"] = gh_token
+
     session.install("-r", requirements)
     session.install(".")
     session.run(*pytest_command, *options, *session.posargs)
@@ -176,7 +182,7 @@ def tests(session, test_specifier: nox._parametrize.Param):
 
 
 @nox.session(python=maxpython)
-def docs(session):
+def docs(session: nox.Session):
     """Build documentation with most recent supported version of Python."""
     session.install("-r", docs_requirements)
     session.install(".")
@@ -184,7 +190,7 @@ def docs(session):
 
 
 @nox.session(python=maxpython)
-def linkcheck(session):
+def linkcheck(session: nox.Session):
     """
     Check hyperlinks in documentation.
 
@@ -198,7 +204,7 @@ def linkcheck(session):
 
 
 @nox.session
-def mypy(session):
+def mypy(session: nox.Session):
     """Perform static type checking."""
     mypy_command: tuple[str, ...] = (
         "mypy",
@@ -215,24 +221,24 @@ def mypy(session):
 
 
 @nox.session(name="import")
-def try_import(session):
+def try_import(session: nox.Session):
     """Install PlasmaPy and import it."""
     session.install(".")
     session.run("python", "-c", "import plasmapy")
 
 
 @nox.session
-def cff(session):
-    """Validate CITATION.cff."""
-    session.install("cffconvert")
-    session.run("cffconvert", "--validate")
-
-
-@nox.session
-def build(session):
+def build(session: nox.Session):
     """Build and verify a source distribution and wheel."""
     session.install("twine", "build")
     build_command = ("python", "-m", "build")
     session.run(*build_command, "--sdist")
     session.run(*build_command, "--wheel")
     session.run("twine", "check", "dist/*")
+
+
+@nox.session
+def cff(session: nox.Session):
+    """Validate CITATION.cff."""
+    session.install("cffconvert")
+    session.run("cffconvert", "--validate")


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,154 +0,0 @@
-name: CI
-
-on:
-  push:
-    branches:
-    - main
-    - stable
-    - v*.*.*
-    tags:
-    - v*
-  pull_request:
-  workflow_dispatch:
-
-env:
-  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-jobs:
-
-  tests:
-    name: ${{ matrix.name }}
-    runs-on: ${{ matrix.os }}
-
-    strategy:
-      fail-fast: false
-
-      matrix:
-        include:
-
-        - name: Python 3.12, with doctests, skip slow tests, Ubuntu
-          os: ubuntu-latest
-          python: '3.12'
-          toxenv: py312-pins-pytest_doctests_skipslow
-
-        - name: Python 3.11, all tests, code coverage, macOS
-          os: macos-latest
-          python: '3.11'
-          toxenv: py311-pins-pytest_cov_all
-
-        - name: Python 3.10, lowest direct dependencies, skip slow tests, Windows
-          os: windows-latest
-          python: '3.10'
-          toxenv: py310-lowest_direct-pytest_skipslow
-
-    steps:
-
-    - name: Checkout code
-      uses: actions/checkout@v4
-      with:
-        fetch-depth: 0
-
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: ${{ matrix.python }}
-        cache: pip
-
-    - name: Install and upgrade test tools
-      run: python -m pip install --progress-bar off --upgrade tox tox-uv
-
-    - name: Cache
-      uses: actions/cache@v4
-      with:
-        path: |
-          .tox
-        key: ${{ matrix.toxenv }}-${{ runner.os }}-${{ hashFiles('requirements.txt', 'pyproject.toml') }}
-
-    - name: Run tests
-      run: tox -e ${{ matrix.toxenv }}
-
-    - name: Upload coverage reports to Codecov
-      if: ${{ contains(matrix.toxenv,'_cov') }}
-      uses: codecov/codecov-action@v4
-      with:
-        file: ./coverage.xml
-        fail_ci_if_error: false
-      env:
-        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-
-  import-plasmapy:
-
-    name: Import PlasmaPy, Python 3.10, Windows
-    runs-on: windows-latest
-
-    steps:
-
-    - name: Checkout code
-      uses: actions/checkout@v4
-      with:
-        fetch-depth: 0
-
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.10'
-        cache: pip
-
-    - name: Install tox and its extensions
-      run: python -m pip install --progress-bar off --upgrade tox tox-uv
-
-    - name: Import PlasmaPy
-      run: tox -e py310-lowest_direct-pypi-import
-
-  build-n-publish:
-    name: Packaging, Python 3.12, Ubuntu
-    runs-on: ubuntu-22.04
-
-    steps:
-
-    - uses: actions/checkout@v4
-
-    - name: Get history and tags for SCM versioning to work
-      run: |
-        git fetch --prune --unshallow
-        git fetch --depth=1 origin +refs/tags/*:refs/tags/*
-
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.12'
-        cache: pip
-
-    - name: Install and upgrade requirements
-      run: |
-        pip install --progress-bar off setuptools numpy wheel setuptools_scm twine build pip --upgrade
-
-    - name: Build a source distribution
-      run: python -m build --sdist --outdir dist/
-
-    - name: Build a binary wheel
-      run: python -m build --wheel --outdir dist/
-
-    - name: Twine check
-      run: twine check dist/*
-
-    - name: Install PlasmaPy with pip
-      run: |
-        pip install --progress-bar off .
-
-    - name: Perform editable installation with tests & docs requirements sets
-      run: |
-        pip install --progress-bar off -e .[tests,docs]
-
-  validate-citation-cff:
-    runs-on: ubuntu-latest
-    name: Validate CITATION.cff
-    env:
-      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
-
-    steps:
-    - name: Checkout
-      uses: actions/checkout@v4
-
-    - name: Validate CITATION.cff
-      uses: dieghernan/cff-validator@v3
diff --git a/docs/contributing/testing_guide.rst b/docs/contributing/testing_guide.rst
--- a/docs/contributing/testing_guide.rst
+++ b/docs/contributing/testing_guide.rst
@@ -12,11 +12,11 @@ Testing Guide
 Summary
 =======
 
-Running tests
--------------
+Running tests (quickstart guide)
+--------------------------------
 
 To prepare to run tests from the command line, |open a terminal| and
-install |tox| with its extensions:
+install |Nox| and |uv|:
 
 .. tabs::
 
@@ -24,20 +24,20 @@ install |tox| with its extensions:
 
       .. code-block:: console
 
-         python -m pip install tox tox-uv
+         python -m pip install nox uv
 
    .. group-tab:: Windows
 
       .. code-block:: console
 
-         py -m pip install tox tox-uv
+         py -m pip install nox uv
 
 To run tests, navigate to a directory within your local clone of
 PlasmaPy and run:
 
 .. code-block:: console
 
-   tox
+   nox
 
 This command will invoke `pytest` to run PlasmaPy's tests, excluding the
 tests marked as slow.
@@ -45,8 +45,7 @@ tests marked as slow.
 .. important::
 
    PlasmaPy is in the process of switching its test runner from |tox| to
-   |Nox|. In the near future, it will likely be necessary to run ``nox``
-   instead of ``tox`` to invoke tests.
+   |Nox|. |tox| is still used for defining weekly tests.
 
 Writing tests
 -------------
@@ -103,7 +102,7 @@ does it in isolation from other tests :cite:p:`khorikov:2020`. A typical
 *assert* :cite:p:`osherove:2013`. An |integration test| verifies that
 multiple software components work together as intended.
 
-PlasmaPy's tests are run using `pytest` and |tox|. Tests are located in
+PlasmaPy's tests are run using `pytest` and |Nox|. Tests are located in
 the |tests/| directory. For example, tests of `plasmapy.formulary`
 are located in :file:`tests/formulary` and tests of
 `plasmapy.formulary.speeds` are located in
@@ -543,7 +542,7 @@ PlasmaPy's tests can be run in the following ways:
 
 1. Creating and updating a pull request on |GitHub|.
 2. Running `pytest` from the command line.
-3. Running |tox| from the command line.
+3. Running |Nox| from the command line.
 4. Running tests from an :wikipedia:`integrated development environment
    <integrated_development_environment>` (IDE).
 
@@ -552,8 +551,8 @@ GitHub. Creating a draft pull request and keeping it updated ensures
 that all necessary checks are run frequently.
 
 Experienced contributors may find it useful to run tests from the
-command line using `pytest` or |tox|, or via an IDE. In particular,
-using |tox| ensures that tests are run in the same way as in CI.
+command line using `pytest` or |Nox|, or via an IDE. In particular,
+using |Nox| ensures that tests are run in the same way as in CI.
 
 Using GitHub
 ------------
@@ -580,7 +579,7 @@ The following checks are performed with each pull request.
 * Checks with labels like **CI / Python 3.x (pull request)** verify that
   PlasmaPy works with different versions of Python and other
   dependencies, and on different operating systems. These tests are set
-  up using |tox| and run with `pytest` via |GitHub Actions|. When
+  up using |Nox| and run with `pytest` via |GitHub Actions|. When
   multiple tests fail, investigate these tests first.
 
   .. tip::
@@ -668,7 +667,7 @@ Using pytest
 ------------
 
 To install the packages necessary to run tests on your local computer
-(including |tox| and pytest_), run:
+(including |Nox| and pytest_), run:
 
 .. code-block:: console
 
@@ -713,58 +712,54 @@ flags you can use with it:
 * Use the ``--pdb`` flag to enter the `Python debugger`_ upon test
   failures.
 
-Using tox
+Using Nox
 ---------
 
-PlasmaPy's continuous integration tests on |GitHub| are typically run
-using |tox|, a tool for automating Python testing. Using |tox| simplifies
-testing PlasmaPy with different releases of Python, with different
-versions of PlasmaPy's dependencies, and on different operating systems.
-Testing with |tox| is more robust than testing with `pytest` because
-|tox| creates its own virtual environments and ensures that tests are
-run the same way as in CI. The ``tox-uv`` extension lets |tox| use |uv|
-as its back-end for significantly improved dependency resolution and
-caching.
+PlasmaPy's continuous integration checks on |GitHub| are typically run
+using |Nox|, a Python tool for automating tasks such as running software
+tests, building documentation, and performing other checks. Using |Nox|
+simplifies testing PlasmaPy with different releases of Python, with
+different versions of PlasmaPy's dependencies, and on different
+operating systems. Testing with |Nox| is more robust than testing with
+`pytest` alone because |Nox| creates its own virtual environments and
+ensures that tests are run the same way as in CI.
 
-To run PlasmaPy's tests (except for those marked as slow), run:
-
-.. code-block:: console
+.. tip::
 
-   tox
+   Installing |uv| alongside |Nox| leads to significantly faster
+   dependency resolution and improved caching.
 
-To run PlasmaPy's tests for a particular environment, run:
+To run PlasmaPy's tests (except for those marked as slow), run:
 
 .. code-block:: console
 
-   tox -e ⟨envname⟩
+   nox
 
-where ``⟨envname⟩`` is replaced with the name of the |tox| environment,
-as described below.
+To find out what |Nox| sessions are defined, run:
 
-Some testing environments for |tox| are pre-defined. For example, you
-can replace ``⟨envname⟩`` with ``py310`` if you are running Python
-``3.10.x``, ``py311`` if you are running Python ``3.11.x``, or ``py312``
-if you are running Python ``3.12.x``. Running |tox| with any of these
-environments requires that the appropriate version of Python has been
-installed and can be found by |tox|. To find the version of Python that
-you are using, go to the command line and run ``python --version``.
+.. code-block::
 
-Additional `tox environments`_ are defined in :file:`tox.ini` in the
-top-level directory of PlasmaPy's repository. To find which testing
-environments are available, run:
+   nox -l
+
+To run PlasmaPy's tests for a particular session, run:
 
 .. code-block:: console
 
-   tox -a
+   nox -s '<session>'
+
+where ``<session>`` is replaced with the name of the |Nox| session. The
+quotes are only needed if ``<session>`` contains special characters like
+parentheses.
 
 For example, static type checking with |mypy| can be run locally with
 
 .. code-block:: console
 
-   tox -e mypy
+   nox -s mypy
 
-Commands using |tox| can be run in any directory within PlasmaPy's
-repository with the same effect.
+Commands using |Nox| must be run in the top-level directory of the
+PlasmaPy repository, which is the directory containing
+:file:`noxfile.py`.
 
 .. _code-coverage:
 
@@ -830,8 +825,8 @@ yet able to handle correctly.
 
 To exclude a line from a coverage report, end it with
 ``# coverage: ignore``. Alternatively, we may add a line to
-``exclude_lines`` in the ``[coverage:report]`` section of
-:file:`setup.cfg` that consists of a
+``exclude_lines`` in the ``[tool.coverage.report]`` section of
+:file:`pyproject.toml` that consists of a
 a pattern that indicates that a line be excluded from coverage reports.
 In general, untested lines of code should remain marked as untested to
 give future developers a better idea of where tests should be added in
@@ -840,9 +835,10 @@ the future and where potential bugs may exist.
 Coverage configurations
 -----------------------
 
-Configurations for coverage tests are given in the ``[coverage:run]``
-and ``[coverage:report]`` sections of :file:`setup.cfg`. Codecov_
-configurations are given in :file:`codecov.yaml`.
+Configurations for coverage tests are given in the
+``[tool.coverage.report]`` and ``[tool.coverage.run]`` sections of
+:file:`pyproject.toml`. Codecov_ configurations are given in
+:file:`codecov.yml`.
 
 Using an integrated development environment
 -------------------------------------------
@@ -882,6 +878,5 @@ popular IDEs:
 .. _`test discovery conventions`: https://docs.pytest.org/en/latest/goodpractices.html#conventions-for-python-test-discovery
 .. _`test warnings`: https://docs.pytest.org/en/latest/warnings.html#warns
 .. _`test exceptions`: https://docs.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions
-.. _`tox environments`: https://tox.wiki/en/stable/config.html#tox-environment
 .. _unpacking: https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists
 .. _`Visual Studio`: https://visualstudio.microsoft.com