diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 303d582..1e50c57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,12 +20,13 @@ jobs: test_spin: strategy: matrix: - python_version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] + python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 + id: setup-python with: python-version: ${{ matrix.python_version }} allow-prereleases: true @@ -37,4 +38,4 @@ jobs: sudo apt-get install -y gdb lcov - name: Tests PyTest run: | - pipx run nox --forcecolor -s test + pipx run --python '${{ steps.setup-python.outputs.python-path }}' nox --forcecolor -s test diff --git a/README.md b/README.md index 78dfa63..4c5b534 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,9 @@ nox -s test -- -v nox -s test -- -v spin/tests/test_meson.py ``` +`spin` takes a slightly more conservative approach than [SPEC 0](https://scientific-python.org/specs/spec-0000/), and +supports all non-EOL versions of Python. + ## History The `dev.py` tool was [proposed for SciPy](https://github.com/scipy/scipy/issues/15489) by Ralf Gommers and [implemented](https://github.com/scipy/scipy/pull/15959) by Sayantika Banik, Eduardo Naufel Schettino, and Ralf Gommers (also see [Sayantika's blog post](https://labs.quansight.org/blog/the-evolution-of-the-scipy-developer-cli)). diff --git a/example_pkg_src/pyproject.toml b/example_pkg_src/pyproject.toml index b1ef1c2..c090ee7 100644 --- a/example_pkg_src/pyproject.toml +++ b/example_pkg_src/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "example_pkg" version = "0.0dev0" -requires-python = ">=3.9" +requires-python = ">=3.10" description = "spin Example Package" [build-system] diff --git a/pyproject.toml b/pyproject.toml index 5f3e99b..12d3281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "spin" -requires-python = ">=3.9" +requires-python = ">=3.10" # Oldest non-EOL Python here description = "Developer tool for scientific Python libraries" readme = "README.md" license = {file = "LICENSE"} @@ -15,7 +15,6 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/spin/__main__.py b/spin/__main__.py index d771f09..6f665f8 100644 --- a/spin/__main__.py +++ b/spin/__main__.py @@ -6,7 +6,6 @@ import sys import textwrap import traceback -from typing import Union import click @@ -31,7 +30,7 @@ ) -def _detect_config_dir(path: pathlib.Path) -> Union[pathlib.Path, None]: +def _detect_config_dir(path: pathlib.Path) -> pathlib.Path | None: path = path.resolve() files = os.listdir(path) if any(f in files for f in config_filenames): diff --git a/spin/cmds/meson.py b/spin/cmds/meson.py index cf4848d..340ee4a 100644 --- a/spin/cmds/meson.py +++ b/spin/cmds/meson.py @@ -8,7 +8,6 @@ import sys from enum import Enum from pathlib import Path -from typing import Union import click @@ -36,7 +35,7 @@ def _meson_cli(): return [meson_cli] -def editable_install_path(distname: str) -> Union[str, None]: +def editable_install_path(distname: str) -> str | None: """Return path of the editable install for package `distname`. If the package is not an editable install, return None. @@ -191,7 +190,7 @@ def _get_site_packages(build_dir: str) -> str: return site_packages -def _meson_version() -> Union[str, None]: +def _meson_version() -> str | None: try: p = _run(_meson_cli() + ["--version"], output=False, echo=False) return p.stdout.decode("ascii").strip() @@ -199,7 +198,7 @@ def _meson_version() -> Union[str, None]: return None -def _meson_version_configured(build_dir: str) -> Union[str, None]: +def _meson_version_configured(build_dir: str) -> str | None: try: meson_info_fn = os.path.join(build_dir, "meson-info", "meson-info.json") with open(meson_info_fn) as f: diff --git a/spin/tests/test_build_cmds.py b/spin/tests/test_build_cmds.py index 649b730..d6a059e 100644 --- a/spin/tests/test_build_cmds.py +++ b/spin/tests/test_build_cmds.py @@ -2,7 +2,7 @@ import subprocess import sys import tempfile -from pathlib import Path +from pathlib import Path, PureWindowsPath import pytest @@ -19,6 +19,10 @@ ) +def unix_path(p: str) -> str: + return PureWindowsPath(p).as_posix() + + def test_basic_build(example_pkg): """Does the package build?""" spin("build") @@ -39,7 +43,7 @@ def test_debug_builds(example_pkg): def test_prefix_builds(example_pkg): """does spin build --prefix create a build-install directory with the correct structure?""" - spin("build", "--prefix=/foobar/") + spin("build", f"--prefix={os.path.abspath('/foobar')}") assert (Path("build-install") / Path("foobar")).exists() @@ -178,7 +182,7 @@ def test_parallel_builds(example_pkg): spin("build") spin("build", "-C", "parallel/build") p = spin("python", "--", "-c", "import example_pkg; print(example_pkg.__file__)") - example_pkg_path = stdout(p).split("\n")[-1] + example_pkg_path = unix_path(stdout(p).split("\n")[-1]) p = spin( "python", "-C", @@ -187,7 +191,7 @@ def test_parallel_builds(example_pkg): "-c", "import example_pkg; print(example_pkg.__file__)", ) - example_pkg_parallel_path = stdout(p).split("\n")[-1] + example_pkg_parallel_path = unix_path(stdout(p).split("\n")[-1]) assert "build-install" in example_pkg_path assert "parallel/build-install" in example_pkg_parallel_path assert "parallel/build-install" not in example_pkg_path diff --git a/spin/tests/testutil.py b/spin/tests/testutil.py index bec3c7e..e27d98c 100644 --- a/spin/tests/testutil.py +++ b/spin/tests/testutil.py @@ -24,12 +24,21 @@ def spin(*args, **user_kwargs): args = (str(el) for el in args) + # Capture stdout, stderr separately default_kwargs = { "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, - "sys_exit": True, + "sys_exit": False, } - return run(["spin"] + list(args), **{**default_kwargs, **user_kwargs}) + p = run(["spin"] + list(args), **{**default_kwargs, **user_kwargs}) + if p.returncode != 0: + print(p.stdout.decode("utf-8"), end="") + print(p.stderr.decode("utf-8"), end="") + # Exit unless the spin call explicitly asks us not to + # by setting sys_exit=False + if user_kwargs.get("sys_exit", True): + sys.exit(p.returncode) + return p def stdout(p):