diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac6c4758..9e203bb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,3 +72,45 @@ jobs: - uses: yezz123/setup-uv@v4 - name: Run tests against nightly wheels for NumPy and SciPy run: uvx nox -s nightly-tests + + # In this job, we test with the free-threaded (no-GIL) Python 3.13t + # interpreter on all platforms. This is a special job and uses the + # "free-threading" session in noxfile.py. This job is available only + # for Python 3.13t at the time of writing and uses the nightly wheels + # for NumPy and SciPy, since free-threaded builds for either of them + # are not available on PyPI yet. + free-threaded-testing: + name: "Free-threading tests / ${{ matrix.platform }} / Python ${{ matrix.python-version }} / Plugin ${{ matrix.plugin }}" + runs-on: ${{ matrix.platform }} + # Temporarily disable this environment variable to see if this fixes the message as follows: + # "Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build" + # env: + # PYTHON_GIL: "0" + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-13, macos-latest, windows-latest] + python-version: ["3.13t"] + plugin: ["none", "pytest-run-parallel", "pytest-freethreaded"] + steps: + - uses: actions/checkout@v4.2.2 + - uses: Quansight-Labs/setup-python@b9ab292c751a42bcd2bb465b7fa202ea2c3f5796 # v5.3.1 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - uses: yezz123/setup-uv@ab6be5a42627f19dc36e57b548592a5e52cece4a # v4.1 + + # 1) Test suite with free-threading enabled, but no free-threading-specific plugins + - name: Run free-threaded tests on ${{ matrix.platform}} + if: ${{ matrix.plugin == 'none' }} + run: uvx nox -s free-threading + + # 2) Test suite with free-threading enabled, plus pytest-run-parallel plugin + - name: Run free-threaded tests with ${{ matrix.plugin }} on ${{ matrix.platform}} + if: ${{ matrix.plugin == 'pytest-run-parallel' }} + run: uvx nox -s free-threading-pytest-run-parallel -- -sra --parallel-threads 5 --iterations 10 + + # 3) Test suite with free-threading enabled, plus pytest-freethreaded plugin + - name: Run free-threaded tests with ${{ matrix.plugin }} on ${{ matrix.platform}} + if: ${{ matrix.plugin == 'pytest-freethreaded' }} + run: uvx nox -s free-threading-pytest-freethreaded -- -sra --threads 5 --iterations 100 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b87093c..77043b21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,14 +10,15 @@ pip install nox ## Run tests, linting, packaging checks -| Command | Description | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `nox --list` | Lists all available Nox sessions, including selected ones | -| `nox -s lint` | Runs code style checks with pre-commit and pre-commit hooks as listed in `.pre-commit-config.yaml`. Accepts posargs to pass additional arguments to the linter. | -| `nox -s tests` | Runs tests with your default Python interpreter. Accepts posargs to pass additional arguments and configuration to `pytest`. | -| `nox -s nightly-tests` | Similar to `nox -s tests`, except that it runs tests with nightly versions of dependencies (NumPy, SciPy, etc.). | -| `nox -s validate-package` | Builds a source distribution and a wheel using `pypa/build` and checks the package with `twine` in strict mode. | -| `nox` | Runs all selected sessions, as listed in `nox.options.sessions` in `noxfile.py`. | +| Command | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `nox --list` | Lists all available Nox sessions, including selected ones | +| `nox -s lint` | Runs code style checks with pre-commit and pre-commit hooks as listed in `.pre-commit-config.yaml`. Accepts posargs to pass additional arguments to the linter. | +| `nox -s tests` | Runs tests with your default Python interpreter. Accepts posargs to pass additional arguments and configuration to `pytest`. | +| `nox -s nightly-tests` | Similar to `nox -s tests`, except that it runs tests with nightly versions of dependencies (NumPy, SciPy, etc.). | +| `nox -s free-threading` | Similar to `nox -s nightly-tests`, except that it runs tests with a free-threaded Python interpreter and corresponding builds for dependencies (NumPy, SciPy, etc.). | +| `nox -s validate-package` | Builds a source distribution and a wheel using `pypa/build` and checks the package with `twine` in strict mode. | +| `nox` | Runs all selected sessions, as listed in `nox.options.sessions` in `noxfile.py`. | Additionally, `nox` supports tags to run specific sessions, e.g., `nox --tags tests` runs all sessions tagged with `tests`. diff --git a/noxfile.py b/noxfile.py index 56b2e132..6203880b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,6 @@ "UV_INDEX_URL": NIGHTLY_INDEX_URL, "UV_PRERELEASE": "allow", "UV_INDEX_STRATEGY": "first-index", - "UV_NO_CACHE": "true", } nox.needs_version = ">=2024.4.15" @@ -34,7 +33,9 @@ def run_tests(session): session.install("-e", ".[test]", silent=False) else: session.install("-e", ".[test,scipy]", silent=False) - session.run("pytest", "--cov=autograd", "--cov-report=xml", "--cov-append", *session.posargs) + session.run( + "pytest", "-n", "auto", "--cov=autograd", "--cov-report=xml", "--cov-append", *session.posargs + ) @nox.session(name="lint", reuse_venv=True) @@ -47,6 +48,7 @@ def ruff(session): @nox.session(name="nightly-tests", tags=["tests"]) def run_nightly_tests(session): """Run tests against nightly versions of dependencies""" + session.run("python", "-VV") session.install("-e", ".[test]", silent=False) # SciPy doesn't have wheels on PyPy if platform.python_implementation() == "PyPy": @@ -57,4 +59,84 @@ def run_nightly_tests(session): session.install( "numpy", "scipy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS ) - session.run("pytest", "--cov=autograd", "--cov-report=xml", "--cov-append", *session.posargs) + session.run( + "pytest", "-n", "auto", "--cov=autograd", "--cov-report=xml", "--cov-append", *session.posargs + ) + + +# Wheels for NumPy and SciPy are available as nightly builds, so we test +# against them on Python 3.13t, which is the only version that supports +# free-threaded Python. This session is similar to the "nightly-tests" +# session, but it uses a free-threaded Python interpreter. Also, we don't +# the "test" extra but install the test dependencies manually. +# +# When the PYTHON_GIL environment variable is set to 0, we enforce that +# extension modules that haven't declared themselves as safe to not rely +# on the GIL are run with the GIL disabled. +@nox.session(name="free-threading", python=["3.13t"]) +def run_with_free_threaded_python(session): + """Run tests with free threaded Python (no-GIL)""" + session.run("python", "-VV") + session.install("-e", ".", silent=False) + session.install("pytest", silent=False) + + # SciPy doesn't have wheels on PyPy + if platform.python_implementation() == "PyPy": + session.install( + "numpy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS + ) + else: + session.install( + "numpy", "scipy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS + ) + session.run( + "pytest", + *session.posargs, + env={"PYTHON_GIL": "0"}, + ) + + +@nox.session(name="free-threading-pytest-run-parallel", python=["3.13t"]) +def run_pytest_run_in_parallel_plugin(session): + """Run stress tests with free threaded Python (no-GIL) using the pytest-run-in-parallel plugin""" + session.run("python", "-VV") + session.install("-e", ".", silent=False) + session.install("pytest", "pytest-run-parallel", silent=False) + + # SciPy doesn't have wheels on PyPy + if platform.python_implementation() == "PyPy": + session.install( + "numpy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS + ) + else: + session.install( + "numpy", "scipy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS + ) + session.run( + "pytest", + *session.posargs, + env={"PYTHON_GIL": "0"}, + ) + + +@nox.session(name="free-threading-pytest-freethreaded", python=["3.13t"]) +def run_pytest_freethreaded(session): + """Run stress tests with free threaded Python (no-GIL) using the pytest-freethreaded plugin""" + session.run("python", "-VV") + session.install("-e", ".", silent=False) + session.install("pytest", "pytest-freethreaded", silent=False) + + # SciPy doesn't have wheels on PyPy + if platform.python_implementation() == "PyPy": + session.install( + "numpy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS + ) + else: + session.install( + "numpy", "scipy", "--upgrade", "--only-binary", ":all:", silent=False, env=UV_NIGHTLY_ENV_VARS + ) + session.run( + "pytest", + *session.posargs, + env={"PYTHON_GIL": "0"}, + ) diff --git a/pyproject.toml b/pyproject.toml index 42e31a2c..5423b2dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,10 +67,9 @@ source = ["autograd"] [tool.coverage.report] show_missing = true -[tool.pytest.ini_options] -required_plugins = ["pytest-cov", "pytest-xdist"] # TODO: generate HTML report, upload to CodeCov -addopts = "--color=yes -sra -n auto --cov=autograd --cov-report=xml --cov-report=term" +[tool.pytest.ini_options] +addopts = "--color=yes -sra" [tool.ruff] extend-exclude = []