test_stages-0.2.0/.editorconfig0000644000000000000000000000102213615410400013426 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause # # https://editorconfig.org/ root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 [*.md] indent_style = space indent_size = 2 [*.py] indent_style = space indent_size = 4 [*.pyi] indent_style = space indent_size = 4 [*.sh] indent_style = tab tab_size = 8 [*.toml] indent_style = space indent_size = 2 [setup.cfg] indent_style = space indent_size = 4 [tox.ini] indent_style = space indent_size = 2 test_stages-0.2.0/.readthedocs.yaml0000644000000000000000000000043213615410400014204 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause version: 2 build: os: ubuntu-22.04 tools: python: "3.12" mkdocs: configuration: mkdocs.yml python: install: - requirements: requirements/docs.txt test_stages-0.2.0/mkdocs.yml0000644000000000000000000000241113615410400012757 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause theme: name: material features: - navigation.instant - navigation.tracking - toc.integrate - toc.follow - content.code.copy palette: - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/weather-night name: Switch to light mode site_name: test-stages repo_url: https://gitlab.com/ppentchev/test-stages repo_name: test-stages site_author: ppentchev site_url: https://devel.ringlet.net/devel/test-stages/ site_dir: site/docs nav: - 'index.md' - 'Download': 'download.md' - 'Changelog': 'changes.md' - 'Command-line tools': 'cmd/index.md' - 'The tox-stages tool': 'cmd/tox-stages.md' markdown_extensions: - toc: - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite: - pymdownx.superfences: plugins: - mkdocstrings: handlers: python: paths: [src] options: heading_level: 3 show_root_heading: true - search watch: - 'src/test_stages' - 'src/tox_trivtags' test_stages-0.2.0/ruff-base.toml0000644000000000000000000000176613615410400013537 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause target-version = "py310" line-length = 100 [lint] select = [] ignore = [ # No blank lines before the class docstring, TYVM "D203", # The multi-line docstring summary starts on the same line "D213", # We do not document everything in the docstrings "DOC201", "DOC402", "DOC501", ] [lint.flake8-copyright] notice-rgx = "(?x) SPDX-FileCopyrightText: \\s \\S" [lint.isort] force-single-line = true known-first-party = ["test_stages", "tox_trivtags"] lines-after-imports = 2 single-line-exclusions = ["collections.abc", "typing"] [lint.per-file-ignores] # The self-test tool uses subprocess responsibly. "src/selftest/__main__.py" = ["S404", "S603", "S607"] # We have our own idea of what the `cmd` module is. "src/test_stages/cmd.py" = ["A005"] # This is a unit test suite, it can output diagnostic messages. # Also, we try to use subprocess responsibly. "tests/unit/**.py" = ["S101", "T201"] test_stages-0.2.0/tox.ini0000644000000000000000000000675013615410400012301 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause [tox] minversion = 4.1 envlist = ruff format mypy reuse unit-tests-no-tox unit-tests-tox-4 selftest selftest-uv docs isolated_build = True [defs] pyfiles_mypy = src/selftest \ src/test_stages \ tests/unit pyfiles = {[defs]pyfiles_mypy} \ src/tox_trivtags [testenv:ruff] skip_install = True tags = check ruff quick deps = -r requirements/ruff.txt commands = ruff check -- {[defs]pyfiles} [testenv:format] skip_install = True tags = check quick deps = -r requirements/ruff.txt commands = ruff check --config ruff-base.toml --select=D,I --diff -- {[defs]pyfiles} ruff format --check --config ruff-base.toml --diff -- {[defs]pyfiles} [testenv:reformat] skip_install = True tags = format manual deps = -r requirements/ruff.txt commands = ruff check --config ruff-base.toml --select=D,I --fix -- {[defs]pyfiles} ruff format --config ruff-base.toml -- {[defs]pyfiles} [testenv:mypy] skip_install = True tags = check deps = -r requirements/install.txt -r requirements/selftest.txt -r requirements/test.txt mypy >= 1, < 2 # There seems to be some misunderstanding between mypy 1.4.1 and click 8.1.4 click != 8.1.4 # pytest still needs this one... types-setuptools >= 20 setenv = MYPYPATH = {toxinidir}/stubs commands = mypy --follow-imports silent --exclude tox_trivtags {[defs]pyfiles_mypy} [testenv:unit-tests-no-tox] tags = tests deps = -r requirements/install.txt -r requirements/test.txt allowlist_externals = sh commands = tox-stages --help sh -c 'if tox-stages available; then echo Waat; exit 1; else echo Not available; fi' [testenv:unit-tests-tox-4] tags = tests deps = -r requirements/install.txt -r requirements/test.txt tox >= 4, < 5 allowlist_externals = sh commands = tox-stages --help tox-stages available pytest {posargs} tests/unit [testenv:selftest] tags = tests deps = -r requirements/install.txt -r requirements/selftest.txt hatchling >= 1.14.1, < 2 hatch-requirements-txt >= 0.4, < 0.5 setenv = PYTHONPATH = {toxinidir}/src commands = python3 -m selftest [testenv:selftest-uv] tags = tests deps = -r requirements/install.txt -r requirements/selftest-uv.txt hatchling >= 1.14.1, < 2 hatch-requirements-txt >= 0.4, < 0.5 setenv = PYTHONPATH = {toxinidir}/src commands = python3 -m selftest # The pyupgrade tool does not seem to have a "check only" mode [testenv:pyupgrade] skip_install = True tags = check manual deps = pyupgrade >= 3, < 4 allowlist_externals = sh commands = sh -c 'pyupgrade --py310-plus src/test_stages/*.py src/test_stages/tox_stages/*.py src/tox_trivtags/*.py tests/unit/*.py' [testenv:reuse] skip_install = True tags = check quick deps = reuse >= 2, < 3 allowlist_externals = sh commands = sh -c 'if [ -d .git ]; then reuse lint; else echo skipped; fi' [testenv:docs] skip_install = True tags = check docs deps = -r requirements/docs.txt commands = mkdocs build [testenv:t-single] tags = something commands = python3 -c 'raise NotImplementedError()' [testenv:t-several] tags = all the things commands = python3 -c 'raise NotImplementedError()' [testenv:t-special] tags = So, how many $tags is "too many", 'eh"? commands = python3 -c 'raise NotImplementedError()' [testenv:t-selftest-marker] tags = selftest commands = python3 -c 'import pathlib; pathlib.Path("selftest-marker.txt").write_text("", encoding="UTF-8")' test_stages-0.2.0/.reuse/dep50000644000000000000000000000050213615410400012734 0ustar00Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: test-stages Upstream-Contact: Peter Pentchev Source: https://gitlab.com/ppentchev/test-stages Files: stubs/contextlib_chdir.pyi Copyright: Álvaro Mondéjar Rubio License: BSD-3-Clause test_stages-0.2.0/LICENSES/BSD-2-Clause.txt0000644000000000000000000000236313615410400014711 0ustar00Copyright (c) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. test_stages-0.2.0/LICENSES/BSD-3-Clause.txt0000644000000000000000000000266413615410400014716 0ustar00Copyright (c) . Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. test_stages-0.2.0/docs/changes.md0000644000000000000000000003023613615410400013644 0ustar00 # Changelog All notable changes to the test-stages project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.2.0] - 2024-08-10 ### Breaking changes - drop support for Tox 3.x ### Semi-incompatible changes - drop support for Python 3.8 and 3.9 ### Fixes - test suite: - pass `filter="data"` to `TarFile.extractall()` ### Other changes - simplify some code using Python 3.10 structural pattern matching - replace `NamedTuple` with frozen dataclasses - documentation: - correct the 0.1.8 tag link on the downloads page - use mkdocstrings 0.25 with no changes - test suite: - Ruff: - use Ruff 0.5.7 - override some docstrings-related checks, we do not document everything - override a `cmd` module check, there is not much potential for confusion - vendor-import vetox 0.2.0 now that we no longer support Tox 3.x - Nix expressions: - pass the Python version as a string, not as an integer - only pass the minor Python version, we only use 3.x - explicitly invoke `python3.X` so that we do not accidentally pick up a "more preferable" Python version that is also installed in the Nix environment ## [0.1.8] - 2024-05-25 ### Fixes - run `tox config` with the `-q` option to avoid diagnostic output from Tox plugins to mix in with the actual configuration settings - documentation: - correct the 0.1.7 tag link on the downloads page ### Additions - add `publync` configuration to the `pyproject.toml` file ### Other changes - test suite: - Ruff: - use Ruff 0.4.5 with no changes ## [0.1.7] - 2024-03-17 ### Fixes - documentation: - correct the 0.1.6 release date on the download page ### Other changes - build system: - allow packaging 24.x with no changes - test suite: - Ruff: - fold the "all" configuration into the `pyproject.toml` file and move the "base" one to the `ruff-base.toml` file in the top-level directory - use Ruff 0.3.3: - add the forgotten `f` prefix to some f-strings - use the concise output even in preview mode - allow pytest 8.x with no changes - Nix expressions: - drop Python 3.8 from the `run-vetox.sh` helper, it was dropped form nixpkgs/unstable - update the vendored copy of vetox to version 0.1.3 - run vetox with support for [uv](https://github.com/astral-sh/uv) and [tox-uv](https://github.com/tox-dev/tox-uv) - when running `uv`, use `/etc/ssl/certs/ca-certificates.crt` as the path to the system-wide certificates file; allow it to be overridden using the `VETOX_CERT_FILE` environment variable ## [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 - documentation: - add the "build" section to the ReadTheDocs configuration - refer to version 1.1.0 of the "Keep a Changelog" specification in the changelog file - selftest: - validate the list of archive files before passing it to `TarFile.extractall()` ### Additions - documentation: - add a "Download" page linking to the various files at the Ringlet website ### Other changes - switch from `black` to `ruff` for source code formatting - documentation: - use `mkdocstrings` 0.23 and `mkdocstrings-python` 1.x with no changes - testing framework: - also run the "reuse" test in the first Tox stage - also build the documentation in the second Tox stage - Ruff: - use Ruff 0.1.13 - disable another subprocess-related check (`S404`) - let Ruff also validate the docstrings - mypy: - do not explicitly install `tomli`, it is brought in as a runtime dependency ## [0.1.4] - 2023-10-19 ### Fixes - fix the rendering of long options in the `tox-stages` manual page ### Additions - tox_trivtags: - automatically tell a bootstrapped Tox to install a recent enough version of `test-stages` into its own virtual environment so that it may output tags ### Other changes - drop the documentation section on requiring `test-stages` in the Tox configuration - build system: - refer to Tox 4.x in the "tox" optional dependency group - selftest: - use `pathlib.Path.cwd()` instead of `pathlib.Path().resolve()` - testing framework: - Ruff: - use Ruff 0.1.0 - let it know how to check for our SPDX copyright tags - enable its preview mode - reuse: - use reuse 2.x - only run if a Git checkout subdirectory is present - with the above in mind, add it to the list of default Tox environments - mypy: - do not install `tomli` in the virtual environment on recent Python versions ## [0.1.3] - 2023-10-01 ### Fixes - fix a typographical error in the `tox-stages` manual page - tox_trivtags: - fix parsing of the Tox output when the `tox.ini` file contains a `min_version` / `minversion` specification and Tox bootstraps a new version into a virtual environment: - ignore all output lines until one that starts with a `[` character - do not depend on the version of Tox that we can see installed as a library for determining the format used to output the list of tags; a different version may have been installed and invoked - testing framework: - do not pass the `python_version` option to mypy, let it use the current interpreter's version for the checks - temporarily run `mypy` on `click` version 8.1.3, there seems to be some trouble with the decorator changes in 8.1.4 ### Additions - document the `requires` directive that is needed if the `tox.ini` file specifies a minimum Tox version requirement ### Other changes - build system: - add Python 3.12 as a supported version - tox_trivtags: - switch from `distlib` to `importlib.metadata` and `packaging` for simpler handling - testing framework: - pin the Ruff version to avoid breakage with new checks enabled in the future - use Ruff 0.0.291, do not pass the current directory to the `pathlib.Path` constructor - use black 23.7 and add "py312" to the list of target versions - drop the `pylint` test environment, we depend on Ruff instead - run the `format` test environment in the first Tox stage ## [0.1.2] - 2023-03-13 ### Incompatible changes - tox-trivtags: - drop the `tox_trivtags.parse.parse_config()` function, running `tox --showconfig` is the only supported method now ### Fixes - tox-stages: - minor refactoring and fixes suggested by Ruff - tox-trivtags: - use the correct way to ignore a specific Ruff check for the whole file instead of telling Ruff to skip that file entirely! - minor fixes suggested by Ruff - testing framework: - correct the `tox.envlist` list in the `tox.ini` file ### Additions - add the beginnings of [MkDocs-based][tool-mkdocs] documentation, hosted [at the Ringlet test-stages webpage][ringlet-test-stages] for the latest release and [at ReadTheDocs][readthedocs] for the latest version from the Git repository - add manual page for the `tox-stages` tool in the mdoc format - add a `.gitignore` file, mainly so that the `reuse` tool can be run even in the presence of some test-related files and directories - add a `selftest` module (not installed in the wheel) that runs the `tox-stages` tool itself on a copy of the source tree - build system: - add the "Typing :: Typed" PyPI trove classifier - specify the project's two-clause BSD license - tox-stages: - add support for Tox 4.x - allow the `tox-stages` command-line tool to be invoked via `python3 -m` - add the `--arg` / `-A` option to pass additional arguments to Tox - add the `--parallel` / `-p` option to specify which stages should run their tests in parallel - testing framework: - add the `reuse` Tox test environment for checking the SPDX tags manually ### Other changes - use SPDX license tags - move the changelog file into the MkDocs-managed `docs/` directory - point to the Ringlet homepage in the package metadata and the README file - tox-stages: - reformat the import statements using Ruff's isort implementation - use `tox run-parallel` when running with Tox 4.x - tox-trivtags: - reformat the import statements using Ruff's isort implementation - use `tox config` when running with Tox 4.x - build system: - switch to hatch/hatchling for the PEP517 build - move the `contextlib-chdir` module from the installation requirements to the test ones, since we do not use it in the installed library - bump the `parse-stages` dependency version to 0.1.4 so that an empty set may be specified as an argument to the `--parallel` option - testing framework: - Ruff: - move the Ruff configuration files from `.config/` to `config/` - run `ruff check ...` explicitly - enable all of the Ruff checks in the default (`ruff`) test environment - use ruff 0.0.265 and ignore some subprocess checks: we do check - remove them `EM` checks override, we do not raise any exceptions - Formatting: - rename the `black` and `black-reformat` Tox environments to `format` and `reformat` respectively and invoke Ruff's isort implementation in both - specify Python 3.8 as the target version - Pylint: - remove the `empty-comment` plugin override, the SPDX license tags no longer cause it to complain - specify Python 3.8 as the target version - use pylint 2.17.x with no changes - update the `tox.ini` file for Tox 4.x (mostly a multiline list) and make the unit tests that run Tox 3.x revert those adaptations - use the `@manual` tag for Tox test environments that should only be run manually with care - drop the Tox environment that runs `flake8` and `pycodestyle`, we depend on Ruff for that ## [0.1.1] - 2023-02-07 ### Fixes - Include the changelog file and the `.config/ruff-*/pyproject.toml` files in the PyPI source distribution tarball. ## [0.1.0] - 2023-02-07 ### Started - First public release. [readthedocs]: https://test-stages.readthedocs.io/en/latest/ [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.2.0...main [0.2.0]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.8...release%2F0.2.0 [0.1.8]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.7...release%2F0.1.8 [0.1.7]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.6...release%2F0.1.7 [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 [0.1.2]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.1...release%2F0.1.2 [0.1.1]: https://gitlab.com/ppentchev/test-stages/-/compare/release%2F0.1.0...release%2F0.1.1 [0.1.0]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.1.0 test_stages-0.2.0/docs/download.md0000644000000000000000000000725213615410400014045 0ustar00 # Download These are the released versions of [test-stages](index.md) available for download. ## [0.2.0] - 2024-08-10 ### Source tarball - [test_stages-0.2.0.tar.gz](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.2.0.tar.gz) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.2.0.tar.gz.asc)) ### Python wheel - [test_stages-0.2.0-py3-none-any.whl](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.2.0-py3-none-any.whl) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.2.0-py3-none-any.whl.asc)) ## [0.1.8] - 2024-05-25 ### Source tarball - [test_stages-0.1.8.tar.gz](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.8.tar.gz) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.8.tar.gz.asc)) ### Python wheel - [test_stages-0.1.8-py3-none-any.whl](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.8-py3-none-any.whl) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.8-py3-none-any.whl.asc)) ## [0.1.7] - 2024-03-17 ### Source tarball - [test_stages-0.1.7.tar.gz](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.7.tar.gz) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.7.tar.gz.asc)) ### Python wheel - [test_stages-0.1.7-py3-none-any.whl](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.7-py3-none-any.whl) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.7-py3-none-any.whl.asc)) ## [0.1.6] - 2024-02-08 ### 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 - [test_stages-0.1.5.tar.gz](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.5.tar.gz) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.5.tar.gz.asc)) ### Python wheel - [test_stages-0.1.5-py3-none-any.whl](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.5-py3-none-any.whl) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.5-py3-none-any.whl.asc)) ## [0.1.4] - 2023-12-19 ### Source tarball - [test_stages-0.1.4.tar.gz](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.4.tar.gz) (with [a PGP signature](https://devel.ringlet.net/files/devel/test-stages/test_stages-0.1.4.tar.gz.asc)) ### Python wheel - [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.2.0]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.2.0 [0.1.8]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.1.8 [0.1.7]: https://gitlab.com/ppentchev/test-stages/-/tags/release%2F0.1.7 [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 test_stages-0.2.0/docs/index.md0000644000000000000000000000511413615410400013340 0ustar00 # Run Tox tests in groups, stopping on errors \[[Home][ringlet-test-stages] | [GitLab][gitlab] | [PyPI][pypi] | [ReadTheDocs][readthedocs]\] The `test-stages` library provides command-line tools that wrap Python test environment runners such as [Tox][tox] or [Nox][nox], invoking them so as the various tests are run in parallel, in groups, as specified on the command line. This allows the fastest tests to be run first, and the slower ones to only be started if it makes sense (e.g. if tools like [ruff] or [flake8] did not uncover any trivial syntax errors). The `tox-stages` tool runs Tox with the specified groups of test environments, stopping if any of the tests in a group should fail. This allows quick static check tools like e.g. `ruff` to stop the testing process early, and also allows scenarios like running all the static check tools before the package's unit or functional tests to avoid unnecessary failures on simple errors. The syntax for grouping the test environments to be run is described in the [parse-stages] library's documentation. [flake8]: https://github.com/pycqa/flake8 "The flake8 Python syntax and style checker" [nox]: https://nox.thea.codes/ "The Nox test runner" [parse-stages]: https://devel.ringlet.net/devel/parse-stages "Parse a mini-language for selecting objects by tag or name" [ruff]: https://github.com/charliermarsh/ruff "Ruff, the extremely fast Python linter" [tox]: https://tox.wiki/ "The Tox automation project" ## Running Tox tests in groups The `tox-stages` tool may be invoked with a list of stages specified on the command line: ``` sh tox-stages run @check @tests ``` If the `tox-stages run` command is invoked without any stage specifications, the tool looks for the `stages` list of strings in the `[tool.test-stages]` section of the `pyproject.toml` file: ``` toml [tool.test-stages] stages = ["ruff and not @manual", "@check", "@tests"] ``` ## Author The `test-stages` library is developed by [Peter Pentchev][roam] in [a GitLab repository][gitlab]. This documentation is hosted at [Ringlet][ringlet-test-stages] with a copy at [ReadTheDocs][readthedocs]. [gitlab]: https://gitlab.com/ppentchev/test-stages "The test-stages GitLab repository" [pypi]: https://pypi.org/project/test-stages/ "The test-stages Python Package Index page" [roam]: mailto:roam@ringlet.net "Peter Pentchev" [readthedocs]: https://test-stages.readthedocs.io/en/latest/ [ringlet-test-stages]: https://devel.ringlet.net/devel/test-stages/ "The Ringlet test-stages homepage" test_stages-0.2.0/docs/cmd/index.md0000644000000000000000000000027513615410400014106 0ustar00 # The command-line tools in the test-stages package - [tox-stages](tox-stages.md) test_stages-0.2.0/docs/cmd/tox-stages.md0000644000000000000000000001260213615410400015072 0ustar00 # tox-stages - run Tox environments in groups, stop on failure ## Synopsis ``` sh tox-stages [-f filename] available tox-stages [-f filename] run [-A arg...] [-m match_spec] [-p spec] stage... ``` ## Description The `tox-stages` tool is used to run Tox test environments in several stages, one or more environments running in parallel at each stage. If any of the test environments run at some stage should fail, `tox-stages` will stop, not run anything further, and exit with a non-zero exit code. This allows quick static check tools like e.g. `ruff` to stop the testing process early, and also allows scenarios like running all the static check tools before the package's unit or functional tests to avoid unnecessary failures on simple errors. ## Tagging Tox test environments The `tox-stages` tool expects to be able to invoke an installation of Tox that will load the `tox_trivtags` plugin module distributed as part of the `test-stages` library. This module will add a "tags" list of strings to the definition of each Tox environment; those tags can be specified in the `tox.ini` file as follows: ``` ini [testenv:format] skip_install = True tags = check format deps = ... ``` ## Subcommands ### available - can the tox-stages tool be run on this system The `tox-stages available` subcommand exits with a code of zero (indicating success) if there is a suitable version of Tox installed in the same Python execution environment as the `tox-stages` tool itself. ### run - run some Tox environments in stages The `tox-stages run` subcommand starts the process of running Tox test environments, grouped in stages. If any of the test environments run at some stage should fail, `tox-stages` will stop, not run anything further, and exit with a non-zero exit code. The `run` subcommand accepts the following options: - `--arg argument` / `-A argument`
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`
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`
Specify which stages to run in parallel. The `spec` parameter is a list of stage indices (1, 2, etc.) or ranges (4-6); the tests in the specified stages will be run in parallel, while the tests in the rest of the stages will not. By default, all tests are run in parallel. The special values "" (an empty string), "0" (a single character, the digit zero), or "none" will be treated as an empty set, and no tests will be run in parallel. - `stage...`
The positional arguments to the `run` subcommand are interpreted as test stage specifications as described in [the parse-stages library's documentation][ringlet-parse-stages]. If no stage specifications are given on the command line, `tox-stages` will read the `pyproject.toml` file in the same directory as the `tox.ini` file, and will look for a `tool.test-stages.stages` list of strings to use. ## Files If no stage specifications are given to the `run` subcommand, the `pyproject.toml` file is read and its `tool.test-stages.stages` variable (expected to be a list of strings) is used instead. ## Examples Run all the stages as defined in the `pyproject.toml` file's `tool.test-stages.stages` parameter: ``` sh 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: ``` sh tox-stages -A -- -A -k -A slug @check unit-tests ``` Execute a somewhat more complicated recipe: - first, run all test environments with names containing "ruff" in parallel - then, run the rest of the test environments marked with the "check" tag, but not marked with the "manual" tag, one by one - then, run all test environments with names containing "unit" in parallel - finally, run the rest of the test environments marked with the "tests" tag, but not marked with the "manual" tag, in parallel ``` sh tox-stages -p 1,3-4 ruff '@check and not @manual' unit '@tests and not @manual' ``` Make sure `tox-stages` can be invoked under a bootstrapped newer version of Tox if needed: ``` ini [tox] minversion = 4.1 requires = test-stages >= 0.1.3 ``` ## Author The `tox-stages` tool, along with its documentation, is developed as part of the `test-stages` library by [Peter Pentchev][roam] in [a GitLab repository][gitlab]. This documentation is hosted at [Ringlet][ringlet-test-stages] with a copy at [ReadTheDocs][readthedocs]. [gitlab]: https://gitlab.com/ppentchev/test-stages "The test-stages GitLab repository" [roam]: mailto:roam@ringlet.net "Peter Pentchev" [readthedocs]: https://test-stages.readthedocs.io/en/latest/ [ringlet-parse-stages]: https://devel.ringlet.net/devel/parse-stages "Parse a mini-language for selecting objects by tag or name" [ringlet-test-stages]: https://devel.ringlet.net/devel/test-stages/ "The Ringlet test-stages homepage" test_stages-0.2.0/docs/man/tox-stages.10000644000000000000000000001151713615410400014646 0ustar00.\" SPDX-FileCopyrightText: Peter Pentchev .\" SPDX-License-Identifier: BSD-2-Clause .Dd May 13, 2023 .Dt TOX-STAGES 1 .Os .Sh NAME .Nm tox-stages .Nd run Tox environments in groups, stop on failure .Sh SYNOPSIS .Nm .Op Fl f Ar filename .Cm available .Nm .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 The .Nm tool is used to run Tox test environments in several stages, one or more environments running in parallel at each stage. If any of the test environments run at some stage should fail, .Nm will stop, not run anything further, and exit with a non-zero exit code. This allows quick static check tools like e.g. .Cm ruff to stop the testing process early, and also allows scenarios like running all the static check tools before the package's unit or functional tests to avoid unnecessary failures on simple errors. .Sh TAGGING TOX TEST ENVIRONMENTS The .Nm tool expects to be able to invoke an installation of Tox that will load the .Cm tox_trivtags plugin module distributed as part of the .Nm test-stages library. This module will add a .Va tags list of strings to the definition of each Tox environment; those tags can be specified in the .Pa tox.ini file as follows: .Pp .Dl [testenv:format] .Dl skip_install = True .Dl tags = .Dl " check" .Dl " format" .Dl deps = .Dl " ..." .Sh SUBCOMMANDS .Ss available - can the tox-stages tool be run on this system The .Nm .Cm available subcommand exits with a code of zero (indicating success) if there is a suitable version of Tox installed in the same Python execution environment as the .Nm tool itself. .Ss run - run some Tox environments in stages The .Nm .Cm run subcommand starts the process of running Tox test environments, grouped in stages. If any of the test environments run at some stage should fail, .Nm will stop, not run anything further, and exit with a non-zero exit code. .Pp The .Cm run subcommand accepts the following options: .Bl -tag -width indent .It Fl Fl arg Ar argument | Fl A Ar argument 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 .Ar spec parameter is a list of stage indices (1, 2, etc.) or ranges (4-6); the tests in the specified stages will be run in parallel, while the tests in the rest of the stages will not. By default, all tests are run in parallel. The special values .Dq "" (an empty string), .Dq 0 (a single character, the digit zero), or .Dq none will be treated as an empty set, and no tests will be run in parallel. .El .Pp The positional arguments to the .Cm run subcommand are interpreted as test stage specifications as described in the parse-stages library's documentation. If no stage specifications are given on the command line, .Nm will read the .Pa pyproject.toml file in the same directory as the .Pa tox.ini file, and will look for a .Va tool.test-stages.stages list of strings to use. .Sh FILES If no stage specifications are given on the command line, .Nm will read the .Pa pyproject.toml file in the same directory as the .Pa tox.ini file, and will look for a .Va tool.test-stages.stages list of strings to use. .Sh EXAMPLES Run all the stages as defined in the .Pa pyproject.toml file's .Va tool.test-stages.stages 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 Tox arguments so that e.g. a .Cm pytest environment that uses the Tox .Va {posargs} variable may only run a selected subset of tests: .Pp .Dl tox-stages -A -- -A -k -A slug @check unit-tests .Pp Execute a somewhat more complicated recipe: .Bl -tag -width \- .It - first, run all test environments with names containing "ruff" in parallel .It - then, run the rest of the test environments marked with the "check" tag, but not marked with the "manual" tag, one by one .It - then, run all test environments with names containing "unit" in parallel .It - finally, run the rest of the test environments marked with the "tests" tag, but not marked with the "manual" tag, in parallel .El .Pp .Dl tox-stages -p 1,3-4 ruff '@check and not @manual' unit '@tests and not @manual' .Sh AUTHORS The .Nm tool, along with its documentation, is developed as part of the .Nm test-stages library by .An Peter Pentchev .Aq roam@ringlet.net . test_stages-0.2.0/nix/cleanpy.sh0000755000000000000000000000075713615410400013557 0ustar00#!/bin/sh # # SPDX-FileCopyrightText: Peter Pentchev # 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 -- '{}' + test_stages-0.2.0/nix/mkdocs.nix0000644000000000000000000000100513615410400013550 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause { pkgs ? import { } , 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 ''; } test_stages-0.2.0/nix/python-vetox.nix0000644000000000000000000000107313615410400014761 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause { pkgs ? import { } , py-ver ? "11" }: let python-name = "python3${py-ver}"; python = builtins.getAttr python-name pkgs; in pkgs.mkShell { buildInputs = [ pkgs.gitMinimal pkgs.uv python ]; shellHook = '' set -e if [ -z "$VETOX_CERT_FILE" ]; then VETOX_CERT_FILE='/etc/ssl/certs/ca-certificates.crt' fi env SSL_CERT_FILE="$VETOX_CERT_FILE" python3.${py-ver} tests/vetox.py run-parallel --tox-uv --uv exit ''; } test_stages-0.2.0/nix/reformat.sh0000755000000000000000000000041213615410400013727 0ustar00#!/bin/sh # # SPDX-FileCopyrightText: Peter Pentchev # 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" test_stages-0.2.0/nix/run-vetox.sh0000755000000000000000000000070113615410400014060 0ustar00#!/bin/sh # # SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause set -e : "${PY_MINVER_MIN:=10}" : "${PY_MINVER_MAX:=13}" for pyver in $(seq -- "$PY_MINVER_MIN" "$PY_MINVER_MAX"); do nix/cleanpy.sh printf -- '\n===== Running tests for 3.%s\n\n\n' "$pyver" nix-shell --pure --keep VETOX_CERT_FILE --argstr py-ver "$pyver" nix/python-vetox.nix printf -- '\n===== Done with 3.%s\n\n' "$pyver" done test_stages-0.2.0/requirements/docs.txt0000644000000000000000000000032413615410400015171 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause mkdocs >= 1.4.2, < 2 mkdocs-material >= 9.1.2, < 10 mkdocstrings >= 0.25, < 0.26 mkdocstrings-python >= 1, < 2 test_stages-0.2.0/requirements/install.txt0000644000000000000000000000037313615410400015713 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause click >= 8, < 9 packaging >= 17, < 25 parse-stages >= 0.1.4, < 0.2 pyparsing >= 3, < 4 tomli >= 2, < 3; python_version < '3.11' utf8-locale >= 1, < 2 test_stages-0.2.0/requirements/ruff.txt0000644000000000000000000000016313615410400015204 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause ruff == 0.5.7 test_stages-0.2.0/requirements/selftest-uv.txt0000644000000000000000000000020713615410400016522 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause -r selftest.txt tox-uv >= 1, < 2 test_stages-0.2.0/requirements/selftest.txt0000644000000000000000000000032313615410400016071 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause contextlib-chdir >= 1, < 2; python_version < '3.11' pyproject_hooks >= 1, < 2 tomli-w >= 1, < 2 tox >= 4, < 5 test_stages-0.2.0/requirements/test.txt0000644000000000000000000000025213615410400015220 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause contextlib-chdir >= 1, < 2; python_version < '3.11' pytest >= 6, < 9 test_stages-0.2.0/src/selftest/__init__.py0000644000000000000000000000025313615410400015507 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """A self-test for the `test-stages` library's `tox-stages` runner.""" test_stages-0.2.0/src/selftest/__main__.py0000644000000000000000000001722313615410400015475 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Run a test for the `test-stages` library's `tox-stages` runner.""" from __future__ import annotations import os import pathlib import subprocess import sys import tarfile import tempfile import typing import pyproject_hooks import tomli_w import utf8_locale if sys.version_info >= (3, 11): import contextlib as contextlib_chdir import tomllib else: import contextlib_chdir import tomli as tomllib if typing.TYPE_CHECKING: from typing import Final def validate_srcdir(srcdir: pathlib.Path) -> None: """Make sure we can find a couple of files in the source directory.""" for relpath in ( "requirements/install.txt", "src/test_stages/tox_stages/__main__.py", "tests/unit/test_functional.py", ): path = srcdir / relpath if not path.is_file(): sys.exit(f"Expected to find {relpath} in {srcdir}, but {path} is not a regular file") def build_sdist(srcdir: pathlib.Path, tempd: pathlib.Path) -> pathlib.Path: """Build a source distribution tarball.""" with contextlib_chdir.chdir(tempd): distdir: Final = tempd / "dist" distdir.mkdir(mode=0o755) backend: Final = tomllib.loads((srcdir / "pyproject.toml").read_text(encoding="UTF-8"))[ "build-system" ]["build-backend"] caller: Final = pyproject_hooks.BuildBackendHookCaller(srcdir, backend) fname: Final = caller.build_sdist(distdir) sdist: Final = distdir / fname if sdist.parent != distdir: sys.exit(f"The PEP517 build returned {fname} which does not seem to be a pure filename") return sdist def safe_extract_all(star: tarfile.TarFile, topdir: pathlib.Path) -> None: """Validate the member names in the archive and extract them all.""" members: Final = star.getmembers() paths: Final = [pathlib.Path(member.name) for member in members] match paths: case []: sys.exit("Expected at least one member in the source archive") case [first, *_] if first.is_absolute(): sys.exit(f"Did not expect an absolute path {first} in the source archive") case [first, *_]: base_path: Final = first.parts[0] bad_paths: Final = [path for path in paths if path.parts[0] != base_path] if bad_paths: sys.exit( f"Bad paths in the source archive, expected all of them to " f"start with {base_path}: {bad_paths}", ) bad_dirs: Final = [path for path in paths if ".." in path.parts] if bad_dirs: sys.exit( f"Bad paths in the source archive, " f"none of them should contain '..': {bad_paths}", ) if sys.version_info >= (3, 12): # We *sincerely* hope our own sdist tarball does not contain anything weird star.extractall(topdir, members=members, filter="data") else: star.extractall(topdir, members=members) # noqa: S202 def extract_sdist(sdist: pathlib.Path, tempd: pathlib.Path) -> pathlib.Path: """Extract the sdist tarball.""" if not sdist.name.endswith(".tar.gz"): sys.exit(f"The PEP517 build generated a non-.tar.gz file: {sdist}") topdir: Final = tempd / "src" topdir.mkdir(mode=0o755) with tarfile.open(sdist, mode="r") as star: safe_extract_all(star, topdir) match sorted(path for path in topdir.iterdir()): case [testdir] if not testdir.is_dir() or not testdir.name.startswith( ("test_stages-", "test-stages-"), ): sys.exit( f"Expected {sdist} to contain a single `test-stages-*` directory, " f"got {testdir}", ) case [testdir]: return testdir case entries: sys.exit(f"Expected {sdist} to contain a single directory, got {entries!r}") def adapt_pyproject(testdir: pathlib.Path) -> None: """Disable this selftest to avoid infinite recursion.""" projfile: Final = testdir / "pyproject.toml" projdata: Final = tomllib.loads(projfile.read_text(encoding="UTF-8")) test_stages: Final[dict[str, list[str]]] = projdata["tool"]["test-stages"] match test_stages["stages"]: case [*others, str(last)] if last.startswith("@tests"): test_stages["stages"] = [*others, f"{last} and not selftest"] case stages: sys.exit(f"Expected a `@tests...` test-stages entry, got {stages!r}") projfile.write_text(tomli_w.dumps(projdata), encoding="UTF-8") def run_tox(testdir: pathlib.Path) -> None: """Clean up the environment a bit, then run Tox.""" env: Final = dict(item for item in os.environ.items() if not item[0].startswith("TOX")) subprocess.check_call(["pwd"], cwd=testdir, env=env) subprocess.check_call(["cat", "pyproject.toml"], cwd=testdir, env=env) subprocess.check_call(["tox-stages", "available"], cwd=testdir, env=env) marker: Final = testdir / "selftest-marker.txt" if marker.is_symlink() or marker.exists(): sys.exit(f"Did not expect {marker} to exist") subprocess.check_call( ["python3", "-m", "test_stages.tox_stages", "run", "@selftest"], cwd=testdir, env=env, ) if not marker.is_file(): sys.exit(f"`tox-stages run @selftest` did not create {marker}") marker.unlink() subprocess.check_call( ["python3", "-m", "test_stages.tox_stages", "run", "--arg", "--notest", "@selftest"], cwd=testdir, env=env, ) 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, ): sys.exit(f"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, ): sys.exit(f"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, ): sys.exit(f"A `-p 7` run output {blurb!r}") subprocess.check_call(["tox-stages", "run"], cwd=testdir, env=env) def main() -> None: """Build a source distribution, extract it, run some tests.""" srcdir: Final = pathlib.Path.cwd() validate_srcdir(srcdir) with tempfile.TemporaryDirectory() as tempd_name: tempd: Final = pathlib.Path(tempd_name) sdist: Final = build_sdist(srcdir, tempd) testdir: Final = extract_sdist(sdist, tempd) adapt_pyproject(testdir) run_tox(testdir) if __name__ == "__main__": main() test_stages-0.2.0/src/test_stages/__init__.py0000644000000000000000000000027613615410400016210 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Run `tox` on several groups of environments, stopping on errors.""" VERSION = "0.2.0" test_stages-0.2.0/src/test_stages/cmd.py0000644000000000000000000002061613615410400015214 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Command-line tool helpers for the various test-stages implementations.""" from __future__ import annotations import dataclasses import functools import pathlib import sys import typing import click import parse_stages as parse import utf8_locale if typing.TYPE_CHECKING: from collections.abc import Callable from typing import Any, Final, TypeVar _T = TypeVar("_T") if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib TestEnv = parse.TaggedFrozen @dataclasses.dataclass(frozen=True) class Stage: """A stage specification and its boolean expression.""" spec: str expr: parse.BoolExpr parallel: bool @dataclasses.dataclass(frozen=True) class TestStage: """A final representation of a test stage: the environments to run and some attributes.""" envlist: list[TestEnv] """The list of environments to run at this stage.""" parallel: bool """Run the environments in parallel.""" class StagesList: """Parse the `--parallel` command-line option argument into a set.""" stages: set[int] """The selected stages, 1-based.""" def __init__(self, value: str) -> None: """Record the selected set of stages.""" self.stages = set(parse.parse_stage_ids(value)) @dataclasses.dataclass(frozen=True) class Config: """Runtime configuration for the test runner tool.""" 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, ) @dataclasses.dataclass class ConfigHolder: """Hold a Config object.""" cfg: Config | None = None def _split_by(current: list[_T], func: Callable[[_T], bool]) -> tuple[list[_T], list[_T]]: """Split an ordered list of items in two by the given predicate.""" res: Final[tuple[list[_T], list[_T]]] = ([], []) for stage in current: if func(stage): res[1].append(stage) else: res[0].append(stage) return res 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, ) -> tuple[list[TestStage], list[TestEnv]]: """Stash the environments matched by a stage specification.""" res, current = acc if not current: sys.exit(f"No test environments left for {stage.spec}") left, matched = _split_by(current, stage.expr.evaluate) if not matched: sys.exit(f"No test environments matched by {stage.spec}") res.append(TestStage(envlist=matched, parallel=stage.parallel)) return res, left res_init: Final[list[TestStage]] = [] 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 = [ dataclasses.replace( stage, 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: """Extract the Config object from the ConfigHolder.""" cfg_hold: Final = ctx.find_object(ConfigHolder) # mypy needs these assertions assert cfg_hold is not None # noqa: S101 cfg: Final = cfg_hold.cfg assert cfg is not None # noqa: S101 return cfg def _find_and_load_pyproject(startdir: pathlib.Path) -> dict[str, Any]: """Look for a pyproject.toml file, load it if found.""" def _find_and_load(path: pathlib.Path) -> dict[str, Any] | None: """Check for a pyproject.toml file in the specified directory.""" proj_file: Final = path / "pyproject.toml" if not proj_file.is_file(): return None return tomllib.loads(proj_file.read_text(encoding="UTF-8")) # Maybe we should look in the parent directories, too... later. for path in (startdir,): found = _find_and_load(path) if found is not None: return found # No pyproject.toml file found, nothing to parse return {} def click_available() -> Callable[[Callable[[Config], bool]], click.Command]: """Wrap an available() function, checking whether the test runner can be invoked.""" def inner(handler: Callable[[Config], bool]) -> click.Command: """Wrap the available check function.""" @click.command(name="available") @click.pass_context def real_available(ctx: click.Context) -> None: """Check whether the test runner is available.""" sys.exit(0 if handler(extract_cfg(ctx)) else 1) return real_available return inner def click_run() -> Callable[[Callable[[Config, list[TestStage], list[str]], None]], click.Command]: """Wrap a run() function, preparing the configuration.""" def inner(handler: Callable[[Config, list[TestStage], list[str]], None]) -> click.Command: """Wrap the run function.""" @click.command(name="run") @click.option( "-A", "--arg", type=str, multiple=True, help=( "an additional argument to pass to the test runner; " "may be specified multiple times" ), ) @click.option( "-m", "--match-spec", type=str, help="additional stage specifications for the tests to run", ) @click.option( "-p", "--parallel", type=StagesList, help="specify which stages to run in parallel (e.g. '1,4-6')", ) @click.argument("stages_spec", nargs=-1, required=False, type=str) @click.pass_context def real_run( 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) if not stages_spec: pyproj: Final = _find_and_load_pyproject(cfg_base.filename.parent) stages_spec = pyproj.get("tool", {}).get("test-stages", {}).get("stages", []) if not stages_spec: sys.exit("No stages specified either on the command line or in pyproject.toml") 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) ], ) ctx.obj.cfg = cfg handler(cfg, select_stages(cfg, cfg.get_all_envs(cfg)), arg) return real_run return inner def click_main( prog: str, prog_help: str, filename: str, filename_help: str, get_all_envs: Callable[[Config], list[TestEnv]], ) -> Callable[[Callable[[Config], Config]], click.Group]: """Wrap a main() function, parsing the top-level options.""" def inner(main: Callable[[Config], Config]) -> click.Group: """Wrap the main function.""" @click.group(name=prog, help=prog_help) @click.option( "-f", "--filename", type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=pathlib.Path), default=filename, help=filename_help, ) @click.pass_context def real_main(ctx: click.Context, filename: pathlib.Path) -> None: """Run Tox environments in groups, stop on failure.""" ctx.ensure_object(ConfigHolder) ctx.obj.cfg = main(Config(filename=filename, get_all_envs=get_all_envs)) return real_main return inner test_stages-0.2.0/src/test_stages/py.typed0000644000000000000000000000000013615410400015557 0ustar00test_stages-0.2.0/src/test_stages/tox_stages/__init__.py0000644000000000000000000000043413615410400020364 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """A `test-stages` implementation for the Tox test runner. This module contains the configuration parsing and runtime glue to let Tox run its test environments grouped in stages. """ test_stages-0.2.0/src/test_stages/tox_stages/__main__.py0000644000000000000000000000720713615410400020352 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """The main tox-stages command-line executable.""" from __future__ import annotations import dataclasses import subprocess # noqa: S404 import sys from typing import TYPE_CHECKING import tox_trivtags from test_stages import cmd if tox_trivtags.HAVE_MOD_TOX_4: from tox_trivtags import parse as ttt_parse if TYPE_CHECKING: import pathlib from typing import Final @dataclasses.dataclass(frozen=True) class Config(cmd.Config): """Also store the path to the Tox executable if found.""" tox_program: list[str | pathlib.Path] | None = None @cmd.click_available() def _cmd_available(cfg: cmd.Config) -> bool: """Check whether we can parse the Tox configuration in any of the supported ways. Currently the only supported way is `tox --showconfig`. """ assert isinstance(cfg, Config) # noqa: S101 # mypy needs this return cfg.tox_program is not None @cmd.click_run() def _cmd_run(cfg: cmd.Config, stages: list[cmd.TestStage], extra_args: list[str]) -> None: """Run the Tox environments in groups.""" toxdir = cfg.filename.parent def run_group(group: list[cmd.TestEnv], *, parallel: bool) -> None: """Run the stages in a single group.""" if not isinstance(cfg, Config) or cfg.tox_program is None: # _tox_get_envs() really should have taken care of that sys.exit(f"Internal error: tox-stages run_group: Config? {cfg!r}") names: Final = ",".join(env.name for env in group) print(f"\n=== Running Tox environments: {names}\n") # noqa: T201 run_parallel = ["run-parallel"] if parallel else ["run"] tox_cmd: Final = [*cfg.tox_program, *run_parallel, "-e", names, *extra_args] res: Final = subprocess.run( # noqa: S603 tox_cmd, check=False, cwd=toxdir, env=cfg.utf8_env, shell=False, ) if res.returncode != 0: sys.exit(f"Tox failed for the {names} environments") for desc in stages: run_group(desc.envlist, parallel=desc.parallel) print("\n=== All Tox environment groups passed!") # noqa: T201 def _tox_get_envs(cfg: cmd.Config) -> list[cmd.TestEnv]: """Get all the Tox environments from the config file.""" assert isinstance(cfg, Config) # noqa: S101 # mypy needs this 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, ) return [cmd.TestEnv(name, env.tags) for name, env in tcfg.items()] def _find_tox_program() -> list[str | pathlib.Path] | None: """Figure out how to invoke Tox. For the present, only a Tox installation in the current Python interpreter's package directories is supported, since we need to be sure that we can rely on the `tox-trivtags` package being installed. Also, we only support Tox 3.x for the present. """ if not tox_trivtags.HAVE_MOD_TOX_4: return None return [sys.executable, "-m", "tox"] @cmd.click_main( prog="tox-stages", prog_help="Run Tox environments in groups, stop on failure.", filename="tox.ini", filename_help="the path to the Tox config file to parse", get_all_envs=_tox_get_envs, ) def main(cfg: cmd.Config) -> cmd.Config: """Return our `Config` object with the path to Tox if found.""" return Config(**dataclasses.asdict(cfg), tox_program=_find_tox_program()) main.add_command(_cmd_available) main.add_command(_cmd_run) if __name__ == "__main__": main() test_stages-0.2.0/src/tox_trivtags/__init__.py0000644000000000000000000000273513615410400016422 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Parse a list of tags in the Tox configuration. Inspired by https://github.com/tox-dev/tox-tags """ from __future__ import annotations import importlib.metadata import typing import packaging.version as pver if typing.TYPE_CHECKING: from typing import Final def _get_tox_version() -> str | None: """Figure out which version of Tox is installed.""" try: dist: Final = importlib.metadata.metadata("tox") except ModuleNotFoundError: return None return dist.get("Version") _TOX_VERSION: Final = _get_tox_version() if _TOX_VERSION is None: HAVE_MOD_TOX_4 = False else: HAVE_MOD_TOX_4 = pver.Version("4") <= pver.Version(_TOX_VERSION) < pver.Version("5") if HAVE_MOD_TOX_4: from typing import List # noqa: UP035 # see below import tox.plugin as t_plugin if typing.TYPE_CHECKING: import tox.config.sets as t_sets import tox.session.state as t_state @t_plugin.impl def tox_add_env_config( env_conf: t_sets.EnvConfigSet, state: t_state.State, # noqa: ARG001 ) -> None: """Parse a testenv's "tags" attribute as a list of lines.""" env_conf.add_config( keys=["tags"], of_type=List[str], # noqa: UP006 # list[int] is not a real type, is it now... default=[], desc="A list of tags describing this test environment", ) test_stages-0.2.0/src/tox_trivtags/parse.py0000644000000000000000000001122613615410400015770 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Query Tox for the tags defined in the specified file.""" from __future__ import annotations import ast import configparser import dataclasses import itertools import pathlib import subprocess # noqa: S404 import sys import typing if typing.TYPE_CHECKING: from typing import Final, TypeVar _T = TypeVar("_T") DEFAULT_FILENAME = pathlib.Path("tox.ini") @dataclasses.dataclass(frozen=True) class TestenvTags: """A Tox environment along with its tags.""" cfg_name: str name: str tags: list[str] def _validate_constant(value: ast.expr, expected: _T) -> _T: """Match an AST constant of the specified type.""" match value: case ast.Constant as c_value: match c_value.value: case expected() as inner: return inner case _: raise ValueError(value) case _: raise ValueError(value) def _validate_parsed_bool(value: ast.expr) -> bool: """Make sure a boolean value is indeed a boolean value.""" return _validate_constant(value, bool) def _validate_parsed_str(value: ast.expr) -> str: """Make sure a string is indeed a string.""" return _validate_constant(value, str) def _validate_parsed_strlist(value: ast.expr) -> list[str]: """Make sure a list of strings is indeed a list of strings.""" return [_validate_parsed_str(item) for item in _validate_constant(value, ast.List).elts] def _parse_bool(value: str) -> bool: """Parse a Python-esque representation of a boolean value without eval().""" return _validate_parsed_bool(_validate_constant(value, ast.Expr).value) def _parse_strlist(value: str) -> list[str]: """Parse a Python-esque representation of a list of strings without eval().""" return _validate_parsed_strlist(_validate_constant(value, ast.Expr).value) def _parse_lines(value: str) -> list[str]: """Parse a list of text lines as Tox 4 wants to output the tags.""" return [line for line in value.splitlines() if line] def _parse_tags(value: str) -> list[str]: """Invoke `_parse_lines()` or `_parse_strlist()` as appropriate.""" return _parse_strlist(value) if value.lstrip().startswith("[") else _parse_lines(value) def remove_prefix(value: str, prefix: str) -> str: """Remove a string's prefix if it is there. Will be replaced with str.removeprefix() once we can depend on Python 3.9+. """ parts: Final = value.partition(prefix) return parts[2] if parts[1] and not parts[0] else value def parse_showconfig( filename: pathlib.Path = DEFAULT_FILENAME, *, env: dict[str, str] | None = None, tox_invoke: list[str | pathlib.Path] | None = None, ) -> dict[str, TestenvTags]: """Run `tox --showconfig` and look for tags in its output.""" if tox_invoke is None: tox_invoke = [sys.executable, "-u", "-m", "tox"] tox_cmd: Final = [ *tox_invoke, "-q", "config", "-c", filename, "-e", "ALL", ] def parse_output() -> configparser.ConfigParser: """Run Tox, parse its output as an INI-style file.""" lines: Final = subprocess.run( # noqa: S603 tox_cmd, check=True, encoding="UTF-8", env=env, shell=False, stdout=subprocess.PIPE, ).stdout.splitlines() # Drop the lines that Tox outputs at the start if it needs to bootstrap # a more recent version due to a `tox.minversion` specification. lines_real: Final = itertools.dropwhile(lambda line: not line.startswith("["), lines) cfgp: Final = configparser.ConfigParser(interpolation=None) cfgp.read_string("\n".join(lines_real) + "\n") return cfgp def process_config(cfgp: configparser.ConfigParser) -> dict[str, TestenvTags]: """Build the result dictionary.""" return { name: TestenvTags( cfg_name=cfg_name, name=name, tags=_parse_tags(tags), ) for cfg_name, name, tags in ( (cfg_name, name, env["tags"]) for cfg_name, name, env in ( (cfg_name, remove_prefix(cfg_name, "testenv:"), env) for cfg_name, env in cfgp.items() ) if cfg_name != name ) } cfgp_no_req: Final = parse_output() if any("tags" in env for env in cfgp_no_req.values()): return process_config(cfgp_no_req) tox_cmd.extend(["-x", "tox.requires=test-stages >= 0.1.3"]) return process_config(parse_output()) test_stages-0.2.0/src/tox_trivtags/py.typed0000644000000000000000000000000013615410400015767 0ustar00test_stages-0.2.0/stubs/contextlib_chdir.pyi0000644000000000000000000000070113615410400016163 0ustar00from _typeshed import StrOrBytesPath from typing import Generic, TypeVar from contextlib import AbstractContextManager _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=int | StrOrBytesPath) class chdir(AbstractContextManager[None], Generic[_T_fd_or_any_path]): path: _T_fd_or_any_path def __init__(self, path: _T_fd_or_any_path) -> None: ... def __enter__(self) -> None: ... def __exit__(self, *excinfo: object) -> None: ... test_stages-0.2.0/stubs/pyproject_hooks.pyi0000644000000000000000000000044313615410400016064 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause import pathlib class BuildBackendHookCaller: def __init__(self, source_dir: pathlib.Path, build_backend: str) -> None: ... def build_sdist(self, distdir: pathlib.Path) -> str: ... test_stages-0.2.0/stubs/tox/__init__.pyi0000644000000000000000000000052113615410400015210 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause from collections.abc import Callable from typing import TypeVar import tox.config TParserHook = Callable[[tox.config.Parser], None] # This only handles the parser hook right now. def hookimpl(func: TParserHook) -> TParserHook: ... test_stages-0.2.0/stubs/tox/config.pyi0000644000000000000000000000104713615410400014722 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause from collections.abc import Iterable from typing import Any, Dict, List class Parser: def add_testenv_attribute( self, name: str, type: str, help: str, default: Any = None, postprocess: Any = None, ) -> None: ... class TestenvConfig: tags: List[str] class Config: envconfigs: Dict[str, TestenvConfig] def parseconfig(args: List[str], plugins: Iterable[str] = ()) -> Config: ... test_stages-0.2.0/tests/vetox.py0000644000000000000000000002201013615410400013632 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # 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 logging import os 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.2.0" """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.""" env: dict[str, str] """The cleaned-up environment variables to pass to child processes.""" 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.""" tox_uv: bool """Install `tox-uv` ito the virtual environment.""" uv: bool """Use `uv` to create the ephemeral Tox environment.""" # 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 clean_env() -> dict[str, str]: """Clean up any variables related to the current virtual environment.""" return { name: value for name, value in os.environ.items() if not name.startswith(("PYTEST", "PYTHON", "TOX", "VIRTUAL_ENV")) } def create_and_update_uv_venv(cfg: Config, penv: pathlib.Path) -> pathlib.Path: """Use `uv` to create a virtual environment.""" cfg.log.info("About to create the %(penv)s virtual environment using uv", {"penv": penv}) subprocess.check_call(["uv", "venv", "-p", sys.executable, "--", penv], env=cfg.env) return penv def create_and_update_venv(cfg: Config) -> pathlib.Path: """Create a virtual environment, update all the packages within.""" penv: pathlib.Path = cfg.tempd / "venv" if cfg.uv: return create_and_update_uv_venv(cfg, penv) cfg.log.info("About to create the %(penv)s virtual environment", {"penv": penv}) cfg.log.info("- using venv.create(upgrade_deps) directly") venv.create(penv, with_pip=True, upgrade_deps=True) return penv @functools.lru_cache def get_tox_min_version(conf: pathlib.Path) -> 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 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.conf) tox_req = f"tox >= {minver}" tox_uv: Final = ["tox-uv"] if cfg.tox_uv else [] cfg.log.info( "Installing Tox %(tox_req)s%(tox_uv)s", {"tox_req": tox_req, "tox_uv": " and tox-uv" if cfg.tox_uv else ""}, ) if cfg.uv: subprocess.check_call( ["env", f"VIRTUAL_ENV={penv}", "uv", "pip", "install", tox_req, *tox_uv], env=cfg.env, ) else: subprocess.check_call( [penv / "bin/python3", "-m", "pip", "install", tox_req, *tox_uv], env=cfg.env, ) 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" 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 = "run-parallel" if parallel else "run" 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), env=cfg.env) 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=1.0 tox-parallel=0.1 tox-uv=0.1 uv=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( "--tox-uv", action="store_true", help="Install `tox-uv` into the virtual environment", ) p_run.add_argument( "--uv", action="store_true", help="Use `uv` to create the ephemeral virtual environment", ) 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( "--tox-uv", action="store_true", help="Install `tox-uv` into the virtual environment", ) p_run_p.add_argument( "--uv", action="store_true", help="Use `uv` to create the ephemeral virtual environment", ) 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") # Things would be a bit simpler with `click`, but we don't want any dependencies return ( Config( conf=args.conf, env=clean_env(), log=build_logger(), tempd=pathlib.Path("/nonexistent"), tox_req=getattr(args, "tox_req", False), # type: ignore[arg-type] tox_uv=getattr(args, "tox_uv", False), uv=getattr(args, "uv", False), ), 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() test_stages-0.2.0/tests/unit/__init__.py0000644000000000000000000000026313615410400015211 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Unit tests for the `test-stages` library and its runner implementations.""" test_stages-0.2.0/tests/unit/test_functional.py0000644000000000000000000000463313615410400016660 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause """Load the Tox configuration, look for our tags thing.""" from __future__ import annotations import contextlib import pathlib import sys import tempfile from typing import TYPE_CHECKING import pytest import utf8_locale import tox_trivtags.parse as ttt_parse if sys.version_info >= (3, 11): import contextlib as contextlib_chdir else: import contextlib_chdir if TYPE_CHECKING: from collections.abc import Callable, Iterator from contextlib import AbstractContextManager from typing import Final _EXPECTED: Final[dict[str, list[str]]] = { "format": ["check", "quick"], "reformat": ["format", "manual"], "unit-tests-no-tox": ["tests"], "unit-tests-tox-4": ["tests"], ".pkg": [], "t-single": ["something"], "t-several": ["all", "the", "things"], "t-special": ["So,", "how many", "$tags", 'is "too many",', "'eh\"?"], } @contextlib.contextmanager def _cfg_filename_cwd() -> Iterator[pathlib.Path]: """No arguments, parse the tox.ini file in the current directory.""" yield pathlib.Path("tox.ini") @contextlib.contextmanager def _cfg_filename_tempdir() -> Iterator[pathlib.Path]: """Create a temporary directory, enter it, pass `-c` with the original cwd.""" cwd: Final = pathlib.Path().absolute() with tempfile.TemporaryDirectory() as tempd: print(f"Temporary directory: {tempd}; current directory: {cwd}") with contextlib_chdir.chdir(tempd): yield cwd / "tox.ini" def _do_test_run_showconfig(filename: pathlib.Path) -> None: """Parse the `tox --showconfig` output.""" u8env: Final = utf8_locale.UTF8Detect().detect().env print(f"Using {u8env['LC_ALL']} as a UTF-8-capable locale") envs: Final = ttt_parse.parse_showconfig(filename, env=u8env) print(f"Got some Tox config sections: {' '.join(sorted(envs))}") for envname, expected in _EXPECTED.items(): print(f"- envname {envname!r} expected {expected!r}") assert envs[envname].tags == expected @pytest.mark.parametrize("cfg_filename", [_cfg_filename_cwd, _cfg_filename_tempdir]) def test_run_showconfig(cfg_filename: Callable[[], AbstractContextManager[pathlib.Path]]) -> None: """Run `tox --showconfig` expecting tox.ini to be in the specified directory.""" print() with cfg_filename() as filename: _do_test_run_showconfig(filename) test_stages-0.2.0/.gitignore0000644000000000000000000000023113615410400012742 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause .mypy_cache .ruff_cache site/ .tox **/__pycache__/ test_stages-0.2.0/README.md0000644000000000000000000000415313615410400012240 0ustar00 # Run Tox tests in groups, stopping on errors The `test-stages` library provides command-line tools that wrap Python test environment runners such as [Tox][tox] or [Nox][nox], invoking them so as the various tests are run in parallel, in groups, as specified on the command line. This allows the fastest tests to be run first, and the slower ones to only be started if it makes sense (e.g. if tools like [ruff] or [flake8] did not uncover any trivial syntax errors). The `tox-stages` tool runs Tox with the specified groups of test environments, stopping if any of the tests in a group should fail. This allows quick static check tools like e.g. `ruff` to stop the testing process early, and also allows scenarios like running all the static check tools before the package's unit or functional tests to avoid unnecessary failures on simple errors. The syntax for grouping the test environments to be run is described in the [parse-stages] library's documentation. ## Running Tox tests in groups The `tox-stages` tool may be invoked with a list of stages specified on the command line: tox-stages run @check @tests If the `tox-stages run` command is invoked without any stage specifications, the tool looks for the `stages` list of strings in the `[tool.test-stages]` section of the `pyproject.toml` file: [tool.test-stages] stages = ["ruff and not @manual", "@check", "@tests"] ## Author The `test-stages` library is developed by [Peter Pentchev][roam] in [a GitLab repository][gitlab]. [flake8]: https://github.com/pycqa/flake8 "The flake8 Python syntax and style checker" [gitlab]: https://gitlab.com/ppentchev/test-stages "The test-stages GitLab repository" [nox]: https://nox.thea.codes/ "The Nox test runner" [parse-stages]: https://devel.ringlet.net/devel/parse-stages "Parse a mini-language for selecting objects by tag or name" [roam]: mailto:roam@ringlet.net "Peter Pentchev" [ruff]: https://github.com/charliermarsh/ruff "Ruff, the extremely fast Python linter" [tox]: https://tox.wiki/ "The Tox automation project" test_stages-0.2.0/pyproject.toml0000644000000000000000000000470113615410400013674 0ustar00# SPDX-FileCopyrightText: Peter Pentchev # SPDX-License-Identifier: BSD-2-Clause [build-system] requires = [ "hatchling >= 1.8, < 2", "hatch-requirements-txt >= 0.3, < 0.5", ] build-backend = "hatchling.build" [project] name = "test_stages" description = "Group Tox, Nox, etc environments into stages, run them in parallel" readme = "README.md" license = {text = "BSD-2-Clause"} requires-python = ">= 3.10" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Framework :: tox", "Intended Audience :: Developers", "License :: DFSG approved", "License :: Freely Distributable", "License :: OSI Approved", "License :: OSI Approved :: BSD License", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "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", "Topic :: Utilities", "Typing :: Typed", ] dynamic = ["dependencies", "version"] [[project.authors]] name = "Peter Pentchev" email = "roam@ringlet.net" [project.entry-points.tox] trivtags = "tox_trivtags" [project.optional-dependencies] tox = ["tox >= 4, < 5"] [project.scripts] tox-stages = "test_stages.tox_stages.__main__:main" [project.urls] Homepage = "https://devel.ringlet.net/devel/test-stages" Changes = "https://devel.ringlet.net/devel/test-stages/changes/" "Issue Tracker" = "https://gitlab.com/ppentchev/test-stages/-/issues" "Source Code" = "https://gitlab.com/ppentchev/test-stages" [tool.hatch.build.targets.wheel] packages = ["src/test_stages", "src/tox_trivtags"] [tool.hatch.metadata.hooks.requirements_txt] files = ["requirements/install.txt"] [tool.hatch.version] path = "src/test_stages/__init__.py" [tool.mypy] strict = true [tool.publync.format.version] major = 0 minor = 1 [tool.publync.build.tox] [tool.publync.sync.rsync] remote = "marla.ludost.net:vhosts/devel.ringlet.net/public_html/devel/test-stages" [tool.ruff] extend = "ruff-base.toml" output-format = "concise" preview = true [tool.ruff.lint] select = ["ALL"] [tool.test-stages] stages = [ "@check and @quick and not @manual", "@check and not @manual", "@tests and not @manual", ] test_stages-0.2.0/PKG-INFO0000644000000000000000000000755113615410400012063 0ustar00Metadata-Version: 2.3 Name: test_stages Version: 0.2.0 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/ Project-URL: Issue Tracker, https://gitlab.com/ppentchev/test-stages/-/issues Project-URL: Source Code, https://gitlab.com/ppentchev/test-stages Author-email: Peter Pentchev License: BSD-2-Clause Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Framework :: tox Classifier: Intended Audience :: Developers Classifier: License :: DFSG approved Classifier: License :: Freely Distributable Classifier: License :: OSI Approved Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only 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 Classifier: Topic :: Utilities Classifier: Typing :: Typed Requires-Python: >=3.10 Requires-Dist: click<9,>=8 Requires-Dist: packaging<25,>=17 Requires-Dist: parse-stages<0.2,>=0.1.4 Requires-Dist: pyparsing<4,>=3 Requires-Dist: tomli<3,>=2; python_version < '3.11' Requires-Dist: utf8-locale<2,>=1 Provides-Extra: tox Requires-Dist: tox<5,>=4; extra == 'tox' Description-Content-Type: text/markdown # Run Tox tests in groups, stopping on errors The `test-stages` library provides command-line tools that wrap Python test environment runners such as [Tox][tox] or [Nox][nox], invoking them so as the various tests are run in parallel, in groups, as specified on the command line. This allows the fastest tests to be run first, and the slower ones to only be started if it makes sense (e.g. if tools like [ruff] or [flake8] did not uncover any trivial syntax errors). The `tox-stages` tool runs Tox with the specified groups of test environments, stopping if any of the tests in a group should fail. This allows quick static check tools like e.g. `ruff` to stop the testing process early, and also allows scenarios like running all the static check tools before the package's unit or functional tests to avoid unnecessary failures on simple errors. The syntax for grouping the test environments to be run is described in the [parse-stages] library's documentation. ## Running Tox tests in groups The `tox-stages` tool may be invoked with a list of stages specified on the command line: tox-stages run @check @tests If the `tox-stages run` command is invoked without any stage specifications, the tool looks for the `stages` list of strings in the `[tool.test-stages]` section of the `pyproject.toml` file: [tool.test-stages] stages = ["ruff and not @manual", "@check", "@tests"] ## Author The `test-stages` library is developed by [Peter Pentchev][roam] in [a GitLab repository][gitlab]. [flake8]: https://github.com/pycqa/flake8 "The flake8 Python syntax and style checker" [gitlab]: https://gitlab.com/ppentchev/test-stages "The test-stages GitLab repository" [nox]: https://nox.thea.codes/ "The Nox test runner" [parse-stages]: https://devel.ringlet.net/devel/parse-stages "Parse a mini-language for selecting objects by tag or name" [roam]: mailto:roam@ringlet.net "Peter Pentchev" [ruff]: https://github.com/charliermarsh/ruff "Ruff, the extremely fast Python linter" [tox]: https://tox.wiki/ "The Tox automation project"