summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Pentchev <roam@debian.org>2024-02-09 19:31:06 +0200
committerPeter Pentchev <roam@debian.org>2024-02-09 19:31:06 +0200
commiteb5e5cae263361cbf603791851f5abc12f619726 (patch)
tree6e97d8785f65c06c6cc6e20cbc3dd60ee72ca646
parentdf8368969be08197bbe2e74a8b25e10e6d1d1479 (diff)
New upstream version 0.1.6
-rw-r--r--PKG-INFO3
-rw-r--r--config/ruff-all/pyproject.toml4
-rw-r--r--config/ruff-base/pyproject.toml16
-rw-r--r--docs/changes.md26
-rw-r--r--docs/cmd/tox-stages.md13
-rw-r--r--docs/download.md13
-rw-r--r--docs/man/tox-stages.113
-rwxr-xr-xnix/cleanpy.sh17
-rw-r--r--nix/mkdocs.nix27
-rw-r--r--nix/python-vetox.nix21
-rwxr-xr-xnix/reformat.sh10
-rwxr-xr-xnix/run-vetox.sh17
-rw-r--r--pyproject.toml7
-rw-r--r--requirements/docs.txt2
-rw-r--r--requirements/ruff.txt2
-rw-r--r--src/selftest/__main__.py36
-rw-r--r--src/test_stages/__init__.py2
-rw-r--r--src/test_stages/cmd.py33
-rw-r--r--src/test_stages/tox_stages/__main__.py4
-rw-r--r--src/tox_trivtags/__init__.py5
-rw-r--r--tests/unit/__init__.py (renamed from unit_tests/__init__.py)0
-rw-r--r--tests/unit/test_functional.py (renamed from unit_tests/test_functional.py)0
-rw-r--r--tests/vetox.py263
-rw-r--r--tox.ini8
24 files changed, 509 insertions, 33 deletions
diff --git a/PKG-INFO b/PKG-INFO
index f12d7b4..d8c9d48 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: test_stages
-Version: 0.1.5
+Version: 0.1.6
Summary: Group Tox, Nox, etc environments into stages, run them in parallel
Project-URL: Homepage, https://devel.ringlet.net/devel/test-stages
Project-URL: Changes, https://devel.ringlet.net/devel/test-stages/changes/
@@ -26,6 +26,7 @@ Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Software Development :: Testing :: Unit
diff --git a/config/ruff-all/pyproject.toml b/config/ruff-all/pyproject.toml
index 3463fb8..32a941b 100644
--- a/config/ruff-all/pyproject.toml
+++ b/config/ruff-all/pyproject.toml
@@ -3,5 +3,7 @@
[tool.ruff]
extend = "../ruff-base/pyproject.toml"
-select = ["ALL"]
preview = true
+
+[tool.ruff.lint]
+select = ["ALL"]
diff --git a/config/ruff-base/pyproject.toml b/config/ruff-base/pyproject.toml
index bd952ab..f3db562 100644
--- a/config/ruff-base/pyproject.toml
+++ b/config/ruff-base/pyproject.toml
@@ -4,14 +4,10 @@
[tool.ruff]
target-version = "py38"
line-length = 100
+
+[tool.ruff.lint]
select = []
ignore = [
- # We know what "self" is... I hope
- "ANN101",
-
- # We let the "black" tool take care of most of the formatting
- "COM812",
-
# No blank lines before the class docstring, TYVM
"D203",
@@ -19,16 +15,16 @@ ignore = [
"D213",
]
-[tool.ruff.flake8-copyright]
+[tool.ruff.lint.flake8-copyright]
notice-rgx = "(?x) SPDX-FileCopyrightText: \\s \\S"
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
force-single-line = true
known-first-party = ["test_stages", "tox_trivtags"]
lines-after-imports = 2
single-line-exclusions = ["collections.abc", "typing"]
-[tool.ruff.per-file-ignores]
+[tool.ruff.lint.per-file-ignores]
# The self-test tool uses subprocess responsibly.
"src/selftest/__main__.py" = ["S404", "S603", "S607"]
@@ -38,4 +34,4 @@ single-line-exclusions = ["collections.abc", "typing"]
# This is a unit test suite, it can output diagnostic messages.
# Also, we try to use subprocess responsibly.
-"unit_tests/**.py" = ["S101", "S404", "S603", "S607", "T201"]
+"tests/unit/**.py" = ["S101", "S404", "S603", "S607", "T201"]
diff --git a/docs/changes.md b/docs/changes.md
index b2867e4..23c2b99 100644
--- a/docs/changes.md
+++ b/docs/changes.md
@@ -12,6 +12,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.1.6] - 2024-02-08
+
+### Additions
+
+- add the `--match-spec` / `-m` command-line option to further limit
+ the Tox environments that will be run
+- add a Nix expression that builds the documentation
+- tentatively declare Python 3.13 as supported
+- testing framework:
+ - vendor-import [the vetox testing tool](https://devel.ringlet.net/devel/vetox/)
+ - add a Nix expression that runs `vetox`
+
+### Other changes
+
+- documentation:
+ - use mkdocstrings 0.24 with no changes
+- testing framework:
+ - use Ruff 0.2.1:
+ - push some Ruff configuration settings into the `ruff.lint.*` hierarchy
+ - let Ruff insist on trailing commas, reformat the source files accordingly
+ - push the unit tests into the `tests/unit/` directory
+ - put the Tox stage specifications in the `pyproject.toml` file on separate lines
+
## [0.1.5] - 2024-01-19
### Fixes
@@ -215,7 +238,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[ringlet-test-stages]: https://devel.ringlet.net/devel/test-stages/ "The Ringlet test-stages homepage"
[tool-mkdocs]: https://www.mkdocs.org/ "Project documentation with Markdown"
-[Unreleased]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.5...main
+[Unreleased]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.6...main
+[0.1.6]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.5...release%2F0.1.6
[0.1.5]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.4...release%2F0.1.5
[0.1.4]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.3...release%2F0.1.4
[0.1.3]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.2...release%2F0.1.3
diff --git a/docs/cmd/tox-stages.md b/docs/cmd/tox-stages.md
index c98bdea..0603188 100644
--- a/docs/cmd/tox-stages.md
+++ b/docs/cmd/tox-stages.md
@@ -9,7 +9,7 @@ SPDX-License-Identifier: BSD-2-Clause
``` sh
tox-stages [-f filename] available
-tox-stages [-f filename] run [-A arg...] [-p spec] stage...
+tox-stages [-f filename] run [-A arg...] [-m match_spec] [-p spec] stage...
```
## Description
@@ -64,6 +64,9 @@ The `run` subcommand accepts the following options:
Pass an additional command-line argument to each Tox invocation.
This option may be specified more than once, and the arguments will be
passed in the order given.
+- `--match-spec spec` / `-m spec` <br/>
+ Pass an additional specification for Tox environments to satisfy,
+ e.g. `-m '@check'` to only run static checkers and not unit tests.
- `--parallel spec` / `-p spec` <br/>
Specify which stages to run in parallel.
The `spec` parameter is a list of stage indices (1, 2, etc.) or
@@ -97,6 +100,14 @@ Run all the stages as defined in the `pyproject.toml` file's
tox-stages run
```
+Group Tox environments into stages as defined in the `pyproject.toml` file,
+but then only run the ones marked with the "check" tag that also have
+names containing the string "format":
+
+``` sh
+tox-stages run -m '@check and format'
+```
+
Run a specific set of stages, passing `-- -k slug` as additional
Tox arguments so that e.g. a `pytest` environment that uses the Tox
`{posargs}` variable may only run a selected subset of tests:
diff --git a/docs/download.md b/docs/download.md
index 8e50175..4d990e6 100644
--- a/docs/download.md
+++ b/docs/download.md
@@ -7,6 +7,18 @@ SPDX-License-Identifier: BSD-2-Clause
These are the released versions of [test-stages](index.md) available for download.
+## [0.1.6] - 2024-01-19
+
+### Source tarball
+
+- [test_stages-0.1.6.tar.gz](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.6.tar.gz)
+ (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.6.tar.gz.asc))
+
+### Python wheel
+
+- [test_stages-0.1.6-py3-none-any.whl](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.6-py3-none-any.whl)
+ (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.6-py3-none-any.whl.asc))
+
## [0.1.5] - 2024-01-19
### Source tarball
@@ -31,5 +43,6 @@ These are the released versions of [test-stages](index.md) available for downloa
- [test_stages-0.1.4-py3-none-any.whl](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.4-py3-none-any.whl)
(with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.4-py3-none-any.whl.asc))
+[0.1.6]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.1.6
[0.1.5]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.1.5
[0.1.4]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.1.4
diff --git a/docs/man/tox-stages.1 b/docs/man/tox-stages.1
index 8e05bb7..8b04815 100644
--- a/docs/man/tox-stages.1
+++ b/docs/man/tox-stages.1
@@ -14,6 +14,7 @@
.Op Fl f Ar filename
.Cm run
.Op Fl Fl arg Ar arg... | Fl A Ar arg...
+.Op Fl Fl match\-spec Ar spec | Fl m Ar spec
.Op Fl Fl parallel Ar spec | Fl p Ar spec
.Op Ar stage...
.Sh DESCRIPTION
@@ -83,6 +84,11 @@ subcommand accepts the following options:
Pass an additional command-line argument to each Tox invocation.
This option may be specified more than once, and the arguments will be
passed in the order given.
+.It Fl Fl match\-spec Ar spec | Fl m Ar spec
+Pass an additional specification for Tox environments to satisfy,
+e.g.
+.Dq -m '@check'
+to only run static checkers and not unit tests.
.It Fl Fl parallel Ar spec | Fl p Ar spec
Specify which stages to run in parallel.
The
@@ -136,6 +142,13 @@ parameter:
.Pp
.Dl tox-stages run
.Pp
+Group Tox environments into stages as defined in the
+.Pa pyproject.toml
+file, but then only run the ones marked with the "check" tag that also have
+names containing the string "format":
+.Pp
+.Dl tox-stages run -m '@check and format'
+.Pp
Run a specific set of stages, passing
.Ar -- -k slug
as additional
diff --git a/nix/cleanpy.sh b/nix/cleanpy.sh
new file mode 100755
index 0000000..7a5490b
--- /dev/null
+++ b/nix/cleanpy.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+#
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+set -e
+
+find . -mindepth 1 -maxdepth 1 -type d \( \
+ -name '.tox' \
+ -or -name '.mypy_cache' \
+ -or -name '.pytest_cache' \
+ -or -name '.nox' \
+ -or -name '.ruff_cache' \
+\) -exec rm -rf -- '{}' +
+find . -type d -name '__pycache__' -exec rm -rfv -- '{}' +
+find . -type f -name '*.pyc' -delete -print
+find . -mindepth 1 -maxdepth 2 -type d -name '*.egg-info' -exec rm -rfv -- '{}' +
diff --git a/nix/mkdocs.nix b/nix/mkdocs.nix
new file mode 100644
index 0000000..b445ea3
--- /dev/null
+++ b/nix/mkdocs.nix
@@ -0,0 +1,27 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+{ pkgs ? import <nixpkgs> { }
+, py-ver ? 311
+}:
+let
+ python-name = "python${toString py-ver}";
+ python = builtins.getAttr python-name pkgs;
+ python-pkgs = python.withPackages (p: with p;
+ [
+ mkdocs
+ mkdocs-material
+ mkdocstrings
+ mkdocstrings-python
+ ]
+ );
+in
+pkgs.mkShell {
+ buildInputs = [ python-pkgs ];
+ shellHook = ''
+ set -e
+ rm -rf site
+ mkdocs build
+ exit
+ '';
+}
diff --git a/nix/python-vetox.nix b/nix/python-vetox.nix
new file mode 100644
index 0000000..9b1b648
--- /dev/null
+++ b/nix/python-vetox.nix
@@ -0,0 +1,21 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+{ pkgs ? import <nixpkgs> { }
+, py-ver ? 311
+}:
+let
+ python-name = "python${toString py-ver}";
+ python = builtins.getAttr python-name pkgs;
+in
+pkgs.mkShell {
+ buildInputs = [
+ pkgs.gitMinimal
+ python
+ ];
+ shellHook = ''
+ set -e
+ python3 tests/vetox.py run-parallel
+ exit
+ '';
+}
diff --git a/nix/reformat.sh b/nix/reformat.sh
new file mode 100755
index 0000000..83bd84d
--- /dev/null
+++ b/nix/reformat.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+#
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+set -e
+
+script_path="$(readlink -f -- "$0")"
+nix_dir="$(dirname -- "$script_path")"
+nix-shell --pure -p nixpkgs-fmt --run "nixpkgs-fmt '$nix_dir'/*.nix"
diff --git a/nix/run-vetox.sh b/nix/run-vetox.sh
new file mode 100755
index 0000000..6b4d93e
--- /dev/null
+++ b/nix/run-vetox.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+#
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+
+set -e
+
+: "${PY_MINVER_MIN:=8}"
+: "${PY_MINVER_MAX:=13}"
+
+for minor in $(seq -- "$PY_MINVER_MIN" "$PY_MINVER_MAX"); do
+ pyver="3$minor"
+ nix/cleanpy.sh
+ printf -- '\n===== Running tests for %s\n\n\n' "$pyver"
+ nix-shell --pure --arg py-ver "$pyver" nix/python-vetox.nix
+ printf -- '\n===== Done with %s\n\n' "$pyver"
+done
diff --git a/pyproject.toml b/pyproject.toml
index b3e5111..58a1095 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Software Development",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Testing :: Unit",
@@ -73,4 +74,8 @@ path = "src/test_stages/__init__.py"
strict = true
[tool.test-stages]
-stages = ["@check and @quick and not @manual", "@check and not @manual", "@tests and not @manual"]
+stages = [
+ "@check and @quick and not @manual",
+ "@check and not @manual",
+ "@tests and not @manual",
+]
diff --git a/requirements/docs.txt b/requirements/docs.txt
index e961640..d94c288 100644
--- a/requirements/docs.txt
+++ b/requirements/docs.txt
@@ -3,5 +3,5 @@
mkdocs >= 1.4.2, < 2
mkdocs-material >= 9.1.2, < 10
-mkdocstrings >= 0.23, < 0.24
+mkdocstrings >= 0.24, < 0.25
mkdocstrings-python >= 1, < 2
diff --git a/requirements/ruff.txt b/requirements/ruff.txt
index 4639591..7ed062c 100644
--- a/requirements/ruff.txt
+++ b/requirements/ruff.txt
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: BSD-2-Clause
-ruff == 0.1.13
+ruff == 0.2.1
diff --git a/src/selftest/__main__.py b/src/selftest/__main__.py
index f16a433..f0e9a6a 100644
--- a/src/selftest/__main__.py
+++ b/src/selftest/__main__.py
@@ -35,7 +35,7 @@ def validate_srcdir(srcdir: pathlib.Path) -> None:
for relpath in (
"requirements/install.txt",
"src/test_stages/tox_stages/__main__.py",
- "unit_tests/test_functional.py",
+ "tests/unit/test_functional.py",
):
path = srcdir / relpath
if not path.is_file():
@@ -74,7 +74,7 @@ def safe_extract_all(star: tarfile.TarFile, topdir: pathlib.Path) -> None:
if bad_paths:
sys.exit(
f"Bad paths in the source archive, expected all of them to "
- f"start with {base_path}: {bad_paths}"
+ f"start with {base_path}: {bad_paths}",
)
bad_dirs: Final = [path for path in paths if ".." in path.parts]
@@ -145,21 +145,47 @@ def run_tox(testdir: pathlib.Path) -> None:
if marker.is_symlink() or marker.exists():
sys.exit(f"A `--notest` run still created {marker}")
+ subprocess.check_call(
+ ["python3", "-m", "test_stages.tox_stages", "run", "(@docs or not @manual) and @selftest"],
+ cwd=testdir,
+ env=env,
+ )
+ if not marker.is_file():
+ sys.exit(f"`tox-stages run (@docs or not @manual) and @selftest` did not create {marker}")
+
+ marker.unlink()
+ subprocess.check_call(
+ ["python3", "-m", "test_stages.tox_stages", "run", "-m", "@selftest", "not @manual"],
+ cwd=testdir,
+ env=env,
+ )
+ if not marker.is_file():
+ sys.exit(f"`tox-stages run -m @selftest not @manual` did not create {marker}")
+
utf8_env = dict(env)
utf8_env.update(utf8_locale.UTF8Detect().detect().env_vars)
blurb = "import pathlib"
if blurb in subprocess.check_output(
- ["tox-stages", "run", "@selftest"], cwd=testdir, encoding="UTF-8", env=utf8_env
+ ["tox-stages", "run", "@selftest"],
+ cwd=testdir,
+ encoding="UTF-8",
+ env=utf8_env,
):
sys.exit("A run without any -p option output {blurb!r}")
if blurb in subprocess.check_output(
- ["tox-stages", "run", "@selftest", "-p", "1"], cwd=testdir, encoding="UTF-8", env=utf8_env
+ ["tox-stages", "run", "@selftest", "-p", "1"],
+ cwd=testdir,
+ encoding="UTF-8",
+ env=utf8_env,
):
sys.exit("A `-p 1` run did not output {blurb!r}")
if blurb not in subprocess.check_output(
- ["tox-stages", "run", "@selftest", "-p", "7"], cwd=testdir, encoding="UTF-8", env=utf8_env
+ ["tox-stages", "run", "@selftest", "-p", "7"],
+ cwd=testdir,
+ encoding="UTF-8",
+ env=utf8_env,
):
sys.exit("A `-p 7` run output {blurb!r}")
diff --git a/src/test_stages/__init__.py b/src/test_stages/__init__.py
index 9a4543d..1534e84 100644
--- a/src/test_stages/__init__.py
+++ b/src/test_stages/__init__.py
@@ -2,4 +2,4 @@
# SPDX-License-Identifier: BSD-2-Clause
"""Run `tox` on several groups of environments, stopping on errors."""
-VERSION = "0.1.5"
+VERSION = "0.1.6"
diff --git a/src/test_stages/cmd.py b/src/test_stages/cmd.py
index 68e9723..4c90cdb 100644
--- a/src/test_stages/cmd.py
+++ b/src/test_stages/cmd.py
@@ -65,9 +65,10 @@ class Config:
filename: pathlib.Path
get_all_envs: Callable[[Config], list[TestEnv]]
+ match_spec: parse.BoolExpr | None = None
stages: list[Stage] = dataclasses.field(default_factory=list)
utf8_env: dict[str, str] = dataclasses.field(
- default_factory=lambda: utf8_locale.UTF8Detect().detect().env
+ default_factory=lambda: utf8_locale.UTF8Detect().detect().env,
)
@@ -93,7 +94,8 @@ def select_stages(cfg: Config, all_stages: list[TestEnv]) -> list[TestStage]:
"""Group the stages as specified."""
def process_stage(
- acc: tuple[list[TestStage], list[TestEnv]], stage: Stage
+ acc: tuple[list[TestStage], list[TestEnv]],
+ stage: Stage,
) -> tuple[list[TestStage], list[TestEnv]]:
"""Stash the environments matched by a stage specification."""
res, current = acc
@@ -106,7 +108,19 @@ def select_stages(cfg: Config, all_stages: list[TestEnv]) -> list[TestStage]:
return res, left
res_init: Final[list[TestStage]] = []
- return functools.reduce(process_stage, cfg.stages, (res_init, list(all_stages)))[0]
+ selected: Final = functools.reduce(process_stage, cfg.stages, (res_init, list(all_stages)))[0]
+ match_spec: Final = cfg.match_spec
+ if match_spec is None:
+ return selected
+
+ matched_all: Final = [
+ stage._replace(envlist=[env for env in stage.envlist if match_spec.evaluate(env)])
+ for stage in selected
+ ]
+ matched: Final = [stage for stage in matched_all if stage.envlist]
+ if not matched:
+ sys.exit("None of the selected environments satisfied the additional match condition")
+ return matched
def extract_cfg(ctx: click.Context) -> Config:
@@ -175,6 +189,12 @@ def click_run() -> Callable[[Callable[[Config, list[TestStage], list[str]], None
),
)
@click.option(
+ "-m",
+ "--match-spec",
+ type=str,
+ help="additional stage specifications for the tests to run",
+ )
+ @click.option(
"-p",
"--parallel",
type=StagesList,
@@ -183,7 +203,11 @@ def click_run() -> Callable[[Callable[[Config, list[TestStage], list[str]], None
@click.argument("stages_spec", nargs=-1, required=False, type=str)
@click.pass_context
def real_run(
- ctx: click.Context, arg: list[str], parallel: StagesList | None, stages_spec: list[str]
+ ctx: click.Context,
+ arg: list[str],
+ match_spec: str | None,
+ parallel: StagesList | None,
+ stages_spec: list[str],
) -> None:
"""Run the test environments in stages."""
cfg_base: Final = extract_cfg(ctx)
@@ -196,6 +220,7 @@ def click_run() -> Callable[[Callable[[Config, list[TestStage], list[str]], None
pstages: Final = set(range(len(stages_spec))) if parallel is None else parallel.stages
cfg: Final = dataclasses.replace(
cfg_base,
+ match_spec=parse.parse_spec(match_spec) if match_spec is not None else None,
stages=[
Stage(spec, parse.parse_spec(spec), idx in pstages)
for idx, spec in enumerate(stages_spec)
diff --git a/src/test_stages/tox_stages/__main__.py b/src/test_stages/tox_stages/__main__.py
index ec53985..e30c04f 100644
--- a/src/test_stages/tox_stages/__main__.py
+++ b/src/test_stages/tox_stages/__main__.py
@@ -79,7 +79,9 @@ def _tox_get_envs(cfg: cmd.Config) -> list[cmd.TestEnv]:
if cfg.tox_program is None:
sys.exit("No tox program found or specified")
tcfg: Final = ttt_parse.parse_showconfig(
- filename=cfg.filename, env=cfg.utf8_env, tox_invoke=cfg.tox_program
+ filename=cfg.filename,
+ env=cfg.utf8_env,
+ tox_invoke=cfg.tox_program,
)
return [cmd.TestEnv(name, env.tags) for name, env in tcfg.items()]
diff --git a/src/tox_trivtags/__init__.py b/src/tox_trivtags/__init__.py
index e0e54e5..0b74e48 100644
--- a/src/tox_trivtags/__init__.py
+++ b/src/tox_trivtags/__init__.py
@@ -46,7 +46,10 @@ if HAVE_MOD_TOX_3:
def tox_addoption(parser: tox.config.Parser) -> None:
"""Parse a testenv's "tags" attribute as a list of lines."""
parser.add_testenv_attribute(
- "tags", "line-list", "A list of tags describing this test environment", default=[]
+ "tags",
+ "line-list",
+ "A list of tags describing this test environment",
+ default=[],
)
diff --git a/unit_tests/__init__.py b/tests/unit/__init__.py
index f32c1ef..f32c1ef 100644
--- a/unit_tests/__init__.py
+++ b/tests/unit/__init__.py
diff --git a/unit_tests/test_functional.py b/tests/unit/test_functional.py
index 2ea478e..2ea478e 100644
--- a/unit_tests/test_functional.py
+++ b/tests/unit/test_functional.py
diff --git a/tests/vetox.py b/tests/vetox.py
new file mode 100644
index 0000000..11b4a86
--- /dev/null
+++ b/tests/vetox.py
@@ -0,0 +1,263 @@
+# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
+# SPDX-License-Identifier: BSD-2-Clause
+"""Create a virtual environment, install Tox, run it."""
+
+from __future__ import annotations
+
+import argparse
+import configparser
+import dataclasses
+import functools
+import json
+import logging
+import pathlib
+import shlex
+import subprocess
+import sys
+import tempfile
+import typing
+import venv
+
+
+if typing.TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Final
+
+
+VERSION: Final = "0.1.2"
+"""The vetox library version."""
+
+
+TOX_MIN_VERSION: Final = "4.1"
+"""The minimum version of Tox needed to run our tests."""
+
+
+@dataclasses.dataclass(frozen=True)
+class Config:
+ """Runtime configuration for the venv-tox tool."""
+
+ conf: pathlib.Path
+ """The path to the `tox.ini` file to use."""
+
+ log: logging.Logger
+ """The logger to send diagnostic, informational, warning, and error messages to."""
+
+ tempd: pathlib.Path
+ """The temporary directory to operate in."""
+
+ tox_req: str | None
+ """The PEP508 version requirements for Tox itself if specified."""
+
+
+# Shamelessly stolen from the logging-std module
+@functools.lru_cache
+def build_logger() -> logging.Logger:
+ """Build a logger object, send info messages to stdout, everything else to stderr."""
+ logger: Final = logging.getLogger("vetox")
+ logger.setLevel(logging.DEBUG)
+ logger.propagate = False
+
+ h_out: Final = logging.StreamHandler(sys.stdout)
+ h_out.setLevel(logging.INFO)
+ h_out.addFilter(lambda rec: rec.levelno == logging.INFO)
+ logger.addHandler(h_out)
+
+ h_err: Final = logging.StreamHandler(sys.stderr)
+ h_err.setLevel(logging.INFO)
+ h_err.addFilter(lambda rec: rec.levelno != logging.INFO)
+ logger.addHandler(h_err)
+
+ return logger
+
+
+def create_and_update_venv(cfg: Config) -> pathlib.Path:
+ """Create a virtual environment, update all the packages within."""
+ penv: pathlib.Path = cfg.tempd / "venv"
+ cfg.log.info("About to create the %(penv)s virtual environment", {"penv": penv})
+ if sys.version_info >= (3, 9):
+ cfg.log.info("- using venv.create(upgrade_deps) directly")
+ venv.create(penv, with_pip=True, upgrade_deps=True)
+ return penv
+
+ cfg.log.info("- no venv.create(upgrade_deps)")
+ venv.create(penv, with_pip=True)
+
+ cfg.log.info("- obtaining the list of packages in the virtual environment")
+ contents: Final = subprocess.check_output(
+ [penv / "bin/python3", "-m", "pip", "list", "--format=json"],
+ encoding="UTF-8",
+ )
+ pkgs: Final = json.loads(contents)
+ if (
+ not isinstance(pkgs, list)
+ or not pkgs
+ or not all(isinstance(pkg, dict) and "name" in pkg for pkg in pkgs)
+ ):
+ sys.exit(f"Unexpected `pip list --format=json` output: {pkgs!r}")
+
+ names: Final = sorted(pkg["name"] for pkg in pkgs)
+ cfg.log.info(
+ "- upgrading the %(names)s package%(plu)s in the virtual environment",
+ {"names": ", ".join(names), "plu": "" if len(names) == 1 else "s"},
+ )
+ subprocess.check_call([penv / "bin/python3", "-m", "pip", "install", "-U", "--", *names])
+ return penv
+
+
+@functools.lru_cache
+def get_tox_min_version(cfg: Config) -> str:
+ """Look for a minimum Tox version in the tox.ini file, fall back to TOX_MIN_VERSION."""
+ cfgp: Final = configparser.ConfigParser(interpolation=None)
+ with cfg.conf.open(encoding="UTF-8") as tox_ini:
+ cfgp.read_file(tox_ini)
+
+ return cfgp["tox"].get("min_version", cfgp["tox"].get("minversion", TOX_MIN_VERSION))
+
+
+def install_tox(cfg: Config, penv: pathlib.Path) -> None:
+ """Install Tox into the virtual environment."""
+ if cfg.tox_req is not None:
+ tox_req = f"tox {cfg.tox_req}"
+ else:
+ minver: Final = get_tox_min_version(cfg)
+ tox_req = f"tox >= {minver}"
+
+ cfg.log.info("Installing Tox %(tox_req)s", {"tox_req": tox_req})
+ subprocess.check_call([penv / "bin/python3", "-m", "pip", "install", tox_req])
+
+
+def get_tox_cmdline(
+ cfg: Config,
+ penv: pathlib.Path,
+ *,
+ parallel: bool = True,
+ args: list[str],
+) -> list[pathlib.Path | str]:
+ """Get the Tox command with arguments."""
+ penv_py3: Final = penv / "bin/python3"
+
+ def get_run_command() -> list[str]:
+ """Get the appropriate command to run Tox in parallel or not."""
+ if not parallel:
+ return ["run"]
+
+ vers: Final = subprocess.check_output(
+ [penv_py3, "-m", "tox", "--version"],
+ encoding="UTF-8",
+ )
+ if vers.startswith("3"):
+ return ["run", "-p", "all"]
+
+ return ["run-parallel"]
+
+ cfg.log.info(
+ "Running Tox%(parallel)s with %(args)s",
+ {
+ "parallel": " in parallel" if parallel else "",
+ "args": ("additional arguments: " + shlex.join(args))
+ if args
+ else "no additional arguments",
+ },
+ )
+ run_cmd: Final = get_run_command()
+ return [penv_py3, "-m", "tox", "-c", cfg.conf, *run_cmd, *args]
+
+
+def run_tox(cfg: Config, penv: pathlib.Path, *, parallel: bool = True, args: list[str]) -> None:
+ """Run Tox from the virtual environment."""
+ subprocess.check_call(get_tox_cmdline(cfg, penv, parallel=parallel, args=args))
+
+
+def run(cfg_no_tempd: Config, *, parallel: bool, args: list[str]) -> None:
+ """Create the virtual environment, install Tox, run it."""
+ with tempfile.TemporaryDirectory() as tempd_obj:
+ cfg: Final = dataclasses.replace(cfg_no_tempd, tempd=pathlib.Path(tempd_obj))
+ penv: Final = create_and_update_venv(cfg)
+ install_tox(cfg, penv)
+ run_tox(cfg, penv, parallel=parallel, args=args)
+
+
+def cmd_run(cfg_no_tempd: Config, args: list[str]) -> None:
+ """Run the Tox tests sequentially."""
+ run(cfg_no_tempd, parallel=False, args=args)
+
+
+def cmd_run_parallel(cfg_no_tempd: Config, args: list[str]) -> None:
+ """Run the Tox tests in parallel."""
+ run(cfg_no_tempd, parallel=True, args=args)
+
+
+def cmd_features(_cfg_no_tempd: Config, _args: list[str]) -> None:
+ """Display the list of features supported by the program."""
+ print(f"Features: vetox={VERSION} tox=0.1 tox-parallel=0.1")
+
+
+def cmd_version(_cfg_no_tempd: Config, _args: list[str]) -> None:
+ """Display the vetox version."""
+ print(f"vetox {VERSION}")
+
+
+def parse_args() -> tuple[Config, Callable[[Config, list[str]], None], list[str]]:
+ """Parse the command-line arguments."""
+ parser: Final = argparse.ArgumentParser(prog="vetox")
+ parser.add_argument(
+ "-c",
+ "--conf",
+ type=pathlib.Path,
+ default=pathlib.Path.cwd() / "tox.ini",
+ help="The path to the tox.ini file",
+ )
+
+ subp: Final = parser.add_subparsers()
+ p_run: Final = subp.add_parser("run", help="Run tests sequentially")
+ p_run.add_argument(
+ "-t",
+ "--tox-req",
+ type=str,
+ help="specify the PEP508 version requirement for Tox itself",
+ )
+ p_run.add_argument("args", type=str, nargs="*", help="Additional arguments to pass to Tox")
+ p_run.set_defaults(func=cmd_run)
+
+ p_run_p: Final = subp.add_parser("run-parallel", help="Run tests in parallel")
+ p_run_p.add_argument(
+ "-t",
+ "--tox-req",
+ type=str,
+ help="specify the PEP508 version requirement for Tox itself",
+ )
+ p_run_p.add_argument("args", type=str, nargs="*", help="Additional arguments to pass to Tox")
+ p_run_p.set_defaults(func=cmd_run_parallel)
+
+ p_features: Final = subp.add_parser("features", help="Display the supported program features")
+ p_features.set_defaults(func=cmd_features)
+
+ p_version: Final = subp.add_parser("version", help="Display the vetox version")
+ p_version.set_defaults(func=cmd_version)
+
+ args: Final = parser.parse_args()
+
+ func: Final[Callable[[Config, list[str]], None] | None] = getattr(args, "func", None)
+ if func is None:
+ sys.exit("No subcommand specified; use `--help` for a list")
+
+ return (
+ Config(
+ conf=args.conf,
+ log=build_logger(),
+ tempd=pathlib.Path("/nonexistent"),
+ tox_req=args.tox_req,
+ ),
+ func,
+ getattr(args, "args", []),
+ )
+
+
+def main() -> None:
+ """Parse command-line arguments, create a virtual environment, run Tox."""
+ cfg_no_tempd, func, args = parse_args()
+ func(cfg_no_tempd, args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tox.ini b/tox.ini
index 31052c4..6768bd4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,7 +19,7 @@ isolated_build = True
pyfiles_mypy =
src/selftest \
src/test_stages \
- unit_tests
+ tests/unit
pyfiles =
{[defs]pyfiles_mypy} \
@@ -100,7 +100,7 @@ deps =
commands =
tox-stages --help
tox-stages available
- pytest {posargs} unit_tests
+ pytest {posargs} tests/unit
[testenv:unit-tests-tox-4]
tags =
@@ -114,7 +114,7 @@ allowlist_externals =
commands =
tox-stages --help
tox-stages available
- pytest {posargs} unit_tests
+ pytest {posargs} tests/unit
[testenv:selftest]
tags =
@@ -140,7 +140,7 @@ deps =
allowlist_externals =
sh
commands =
- sh -c 'pyupgrade --py38-plus src/test_stages/*.py src/test_stages/tox_stages/*.py src/tox_trivtags/*.py unit_tests/*.py'
+ sh -c 'pyupgrade --py38-plus src/test_stages/*.py src/test_stages/tox_stages/*.py src/tox_trivtags/*.py tests/unit/*.py'
[testenv:reuse]
skip_install = True