Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: tox-dev/tox
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 4.23.2
Choose a base ref
...
head repository: tox-dev/tox
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4.24.0
Choose a head ref

Commits on Oct 23, 2024

  1. fix docs config typo (#3424)

    * fix docs config typo
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    wooshaun53 and pre-commit-ci[bot] authored Oct 23, 2024
    Copy the full SHA
    ab51e14 View commit details

Commits on Oct 28, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#3427)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Oct 28, 2024
    Copy the full SHA
    fc5ea9f View commit details

Commits on Oct 29, 2024

  1. Allow users to disable use of pre-commit-uv (#3430)

    ssbarnea authored Oct 29, 2024

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    hiyuki2578 Shota Tsunehiro
    Copy the full SHA
    7b4d7ed View commit details

Commits on Oct 30, 2024

  1. Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.11.0 (#3431)

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Oct 30, 2024

    Partially verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    5d880fc View commit details

Commits on Nov 1, 2024

  1. Pass nix-ld related variables by default in pass_env (fixes #3425) (#…

    albertodonato authored Nov 1, 2024
    Copy the full SHA
    324dfe5 View commit details

Commits on Nov 7, 2024

  1. Update tox.toml

    gaborbernat authored Nov 7, 2024
    Copy the full SHA
    2310010 View commit details
  2. [pre-commit.ci] pre-commit autoupdate (#3437)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Nov 7, 2024
    Copy the full SHA
    a240d79 View commit details
  3. Bump pypa/gh-action-pypi-publish from 1.11.0 to 1.12.2 (#3439)

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 7, 2024
    Copy the full SHA
    a442878 View commit details

Commits on Nov 8, 2024

  1. Improve testenv docs consistency (#3440)

    thatch authored Nov 8, 2024
    Copy the full SHA
    08f2ac5 View commit details

Commits on Nov 11, 2024

  1. [pre-commit.ci] pre-commit autoupdate (#3441)

    updates:
    - [github.com/abravalheri/validate-pyproject: v0.22 → v0.23](abravalheri/validate-pyproject@v0.22...v0.23)
    - [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.7.3](astral-sh/ruff-pre-commit@v0.7.2...v0.7.3)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Nov 11, 2024
    Copy the full SHA
    343fe92 View commit details

Commits on Nov 19, 2024

  1. Display exception name when subprocesses raise them (#3450)

    ssbarnea authored Nov 19, 2024
    Copy the full SHA
    dbbb043 View commit details

Commits on Nov 26, 2024

  1. Bump astral-sh/setup-uv from 3 to 4 (#3451)

    Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4.
    - [Release notes](https://github.com/astral-sh/setup-uv/releases)
    - [Commits](astral-sh/setup-uv@v3...v4)
    
    ---
    updated-dependencies:
    - dependency-name: astral-sh/setup-uv
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Nov 26, 2024
    Copy the full SHA
    9200e11 View commit details

Commits on Nov 27, 2024

  1. Fix the CI after setuptools 75.6 change (#3452)

    Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
    gaborbernat authored Nov 27, 2024
    Copy the full SHA
    5b76cdd View commit details
  2. [pre-commit.ci] pre-commit autoupdate (#3448)

    updates:
    - [github.com/astral-sh/ruff-pre-commit: v0.7.3 → v0.8.0](astral-sh/ruff-pre-commit@v0.7.3...v0.8.0)
    
    Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Nov 27, 2024
    Copy the full SHA
    a81c2cb View commit details

Commits on Dec 3, 2024

  1. Update pre-commit hooks with mypy fix (#3454)

    * Update pre-commit.com hooks
    
    Fixes: #453
    
    * Address mypy failure
    ssbarnea authored Dec 3, 2024
    Copy the full SHA
    0e33d24 View commit details

Commits on Dec 10, 2024

  1. Bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3 (#3459)

    Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.2 to 1.12.3.
    - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
    - [Commits](pypa/gh-action-pypi-publish@v1.12.2...v1.12.3)
    
    ---
    updated-dependencies:
    - dependency-name: pypa/gh-action-pypi-publish
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 10, 2024
    Copy the full SHA
    c7f2caf View commit details
  2. Fix a typo in a code block in the User Guide (#3462)

    bryant1410 authored Dec 10, 2024
    Copy the full SHA
    fbac078 View commit details

Commits on Dec 20, 2024

  1. Update pre-commit hooks (#3460)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](python-jsonschema/check-jsonschema@0.29.4...0.30.0)
    - [github.com/astral-sh/ruff-pre-commit: v0.8.0 → v0.8.3](astral-sh/ruff-pre-commit@v0.8.0...v0.8.3)
    - [github.com/rbubley/mirrors-prettier: v3.3.3 → v3.4.2](rbubley/mirrors-prettier@v3.3.3...v3.4.2)
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    ssbarnea and pre-commit-ci[bot] authored Dec 20, 2024
    Copy the full SHA
    c0b490d View commit details

Commits on Dec 30, 2024

  1. Bump astral-sh/setup-uv from 4 to 5 (#3463)

    Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 4 to 5.
    - [Release notes](https://github.com/astral-sh/setup-uv/releases)
    - [Commits](astral-sh/setup-uv@v4...v5)
    
    ---
    updated-dependencies:
    - dependency-name: astral-sh/setup-uv
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Dec 30, 2024
    Copy the full SHA
    e3e77a6 View commit details

Commits on Jan 16, 2025

  1. 💅 Make SVG image compatible with Firefox (#3466)

    It appears that Firefox has bugs around interpreting `<feBlend>`
    filters in SVG documents. This makes it render the logo as a
    transparent image in the docs, both in the mobile and the desktop
    versions.
    
    This patch applies a workaround [[1]] found on the internet to make it
    work without waiting for Firefox to fix their bug.
    
    Resolves #3465.
    
    [1]: svg/svgo#732
    webknjaz authored Jan 16, 2025
    Copy the full SHA
    fccbe2a View commit details

Commits on Jan 21, 2025

  1. feat: adding a json schema command (#3446)

    * feat: adding a schema command
    
    Now running this passes:
    
        uvx check-jsonschema --schemafile src/tox/tox.schema.json tox.toml
    
    Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
    
    * refactor: leave access private
    
    Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
    
    * fix: changelog and update test list
    
    Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
    
    ---------
    
    Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
    henryiii authored Jan 21, 2025
    Copy the full SHA
    825c68b View commit details
  2. [pre-commit.ci] pre-commit autoupdate (#3464)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.0](python-jsonschema/check-jsonschema@0.30.0...0.31.0)
    - [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.9.2](astral-sh/ruff-pre-commit@v0.8.3...v0.9.2)
    
    * Fix failures
    
    Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
    
    ---------
    
    Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Bernát Gábor <bgabor8@bloomberg.net>
    pre-commit-ci[bot] and gaborbernat authored Jan 21, 2025
    Copy the full SHA
    bbd9663 View commit details
  3. release 4.24.0

    gaborbernat committed Jan 21, 2025
    Copy the full SHA
    eca61ed View commit details
Showing with 928 additions and 219 deletions.
  1. +2 −2 .github/workflows/check.yaml
  2. +2 −2 .github/workflows/release.yaml
  3. +8 −8 .pre-commit-config.yaml
  4. +12 −2 docs/_static/img/tox.svg
  5. +24 −0 docs/changelog.rst
  6. +1 −1 docs/conf.py
  7. +9 −1 docs/config.rst
  8. +2 −2 docs/tox_conf.py
  9. +3 −5 docs/user_guide.rst
  10. +62 −62 pyproject.toml
  11. +1 −1 src/tox/config/cli/parse.py
  12. +3 −3 src/tox/config/cli/parser.py
  13. +1 −1 src/tox/config/loader/convert.py
  14. +2 −2 src/tox/config/loader/toml/__init__.py
  15. +8 −7 src/tox/config/loader/toml/_replace.py
  16. +1 −1 src/tox/config/loader/toml/_validate.py
  17. +1 −1 src/tox/config/of_type.py
  18. +11 −5 src/tox/config/sets.py
  19. +1 −1 src/tox/config/source/toml_pyproject.py
  20. +12 −5 src/tox/execute/api.py
  21. +3 −0 src/tox/execute/local_sub_process/__init__.py
  22. +3 −3 src/tox/execute/request.py
  23. +2 −0 src/tox/plugin/manager.py
  24. +3 −3 src/tox/provision.py
  25. +2 −2 src/tox/pytest.py
  26. +2 −2 src/tox/session/cmd/depends.py
  27. +4 −4 src/tox/session/cmd/legacy.py
  28. +8 −8 src/tox/session/cmd/run/common.py
  29. +1 −1 src/tox/session/cmd/run/parallel.py
  30. +3 −3 src/tox/session/cmd/run/single.py
  31. +176 −0 src/tox/session/cmd/schema.py
  32. +5 −3 src/tox/session/cmd/version_flag.py
  33. +5 −5 src/tox/session/env_select.py
  34. +439 −0 src/tox/tox.schema.json
  35. +19 −12 src/tox/tox_env/api.py
  36. +3 −3 src/tox/tox_env/package.py
  37. +3 −3 src/tox/tox_env/python/api.py
  38. +2 −2 src/tox/tox_env/python/package.py
  39. +1 −1 src/tox/tox_env/python/pip/req/file.py
  40. +3 −3 src/tox/tox_env/python/virtual_env/api.py
  41. +2 −2 src/tox/tox_env/python/virtual_env/package/cmd_builder.py
  42. +5 −5 src/tox/tox_env/python/virtual_env/package/pyproject.py
  43. +4 −4 src/tox/tox_env/python/virtual_env/package/util.py
  44. +1 −1 src/tox/tox_env/runner.py
  45. +1 −1 src/tox/tox_env/util.py
  46. +2 −0 tests/config/cli/conftest.py
  47. +1 −1 tests/config/loader/ini/replace/test_replace_tox_env.py
  48. +2 −2 tests/config/loader/test_toml_loader.py
  49. +1 −1 tests/config/source/test_toml_pyproject.py
  50. +16 −11 tests/execute/local_subprocess/test_local_subprocess.py
  51. +19 −0 tests/session/cmd/test_schema.py
  52. +1 −1 tests/session/cmd/test_sequential.py
  53. +3 −1 tests/session/cmd/test_show_config.py
  54. +4 −4 tests/test_provision.py
  55. +3 −3 tests/test_run.py
  56. +2 −2 tests/tox_env/python/pip/req/test_file.py
  57. +3 −2 tests/tox_env/python/virtual_env/package/test_python_package_util.py
  58. +2 −2 tests/tox_env/python/virtual_env/test_setuptools.py
  59. +3 −6 tox.toml
4 changes: 2 additions & 2 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ jobs:
with:
fetch-depth: 0
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
@@ -76,7 +76,7 @@ jobs:
with:
fetch-depth: 0
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ jobs:
with:
fetch-depth: 0
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
@@ -43,6 +43,6 @@ jobs:
name: ${{ env.dists-artifact-name }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.10.3
uses: pypa/gh-action-pypi-publish@v1.12.3
with:
attestations: true
16 changes: 8 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -5,40 +5,40 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.4
rev: 0.31.0
hooks:
- id: check-github-workflows
args: ["--verbose"]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies: ["tomli>=2.0.1"]
additional_dependencies: ["tomli>=2.1"]
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "2.4.3"
rev: "v2.5.0"
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: "v0.21"
rev: "v0.23"
hooks:
- id: validate-pyproject
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.7.0"
rev: "v0.9.2"
hooks:
- id: ruff-format
- id: ruff
args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"]
- repo: https://github.com/asottile/blacken-docs
rev: 1.19.0
rev: 1.19.1
hooks:
- id: blacken-docs
additional_dependencies: [black==24.8]
additional_dependencies: [black==24.10]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: rst-backticks
- repo: https://github.com/rbubley/mirrors-prettier
rev: "v3.3.3"
rev: "v3.4.2"
hooks:
- id: prettier
- repo: local
14 changes: 12 additions & 2 deletions docs/_static/img/tox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -4,6 +4,30 @@ Release History

.. towncrier release notes start
v4.24.0 (2025-01-21)
--------------------

Features - 4.24.0
~~~~~~~~~~~~~~~~~
- Add a ``schema`` command to produce a JSON Schema for tox and the current plugins.

- by :user:`henryiii` (:issue:`3446`)

Bugfixes - 4.24.0
~~~~~~~~~~~~~~~~~
- Log exception name when subprocess execution produces one.

- by :user:`ssbarnea` (:issue:`3450`)

Improved Documentation - 4.24.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Fix typo in ``docs/config.rst`` from ``{}`` to ``{:}``.

- by :user:`wooshaun53` (:issue:`3424`)
- Pass ``NIX_LD`` and ``NIX_LD_LIBRARY_PATH`` variables by default in ``pass_env`` to make generic binaries work under Nix/NixOS.

- by :user:`albertodonato` (:issue:`3425`)

v4.23.2 (2024-10-22)
--------------------

2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
@@ -88,7 +88,7 @@ def process_signature( # noqa: PLR0913
options: Options,
args: str, # noqa: ARG001
retann: str | None, # noqa: ARG001
) -> None | tuple[None, None]:
) -> tuple[None, None] | None:
# skip-member is not checked for class level docs, so disable via signature processing
return (None, None) if objtype == "class" and "__init__" in options.get("exclude-members", set()) else None

10 changes: 9 additions & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
@@ -534,6 +534,14 @@ Base options
- ✅
- ✅
- ✅
* - NIX_LD*
- ✅
- ✅
- ❌
* - NIX_LD_LIBRARY_PATH
- ✅
- ✅
- ❌



@@ -1698,5 +1706,5 @@ filesystem to become a path relative to the ``changedir`` setting.
Other Substitutions
~~~~~~~~~~~~~~~~~~~

* ``{}`` - replaced as ``os.pathsep``
* ``{:}`` - replaced as ``os.pathsep``
* ``{/}`` - replaced as ``os.sep``
4 changes: 2 additions & 2 deletions docs/tox_conf.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@
from docutils.nodes import Element, Node, Text, container, fully_normalize_name, literal, paragraph, reference, strong
from docutils.parsers.rst.directives import flag, unchanged, unchanged_required
from docutils.statemachine import StringList, string2lines
from sphinx.domains.std import StandardDomain
from sphinx.locale import __
from sphinx.util.docutils import SphinxDirective
from sphinx.util.logging import getLogger
@@ -14,6 +13,7 @@
from typing import Final

from docutils.parsers.rst.states import RSTState, RSTStateMachine
from sphinx.domains.std import StandardDomain

LOGGER = getLogger(__name__)

@@ -53,7 +53,7 @@ def __init__( # noqa: PLR0913
state,
state_machine,
)
self._std_domain: StandardDomain = cast(StandardDomain, self.env.get_domain("std"))
self._std_domain: StandardDomain = cast("StandardDomain", self.env.get_domain("std"))

def run(self) -> list[Node]:
self.env.note_reread() # this document needs to be always updated
8 changes: 3 additions & 5 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ these. The canonical file for this is either a ``tox.toml`` or ``tox.ini`` file.

.. code-block:: ini
[tox]
[tox]
requires =
tox>=4
env_list = lint, type, 3.1{3,2,1}
@@ -92,7 +92,7 @@ Core settings
~~~~~~~~~~~~~

Core settings that affect all test environments or configure how tox itself is invoked are defined under the root table
in ``tox.toml`` and ``tox`` table in ``tox.ini`` section.
in ``tox.toml`` and ``tox`` section in ``tox.ini``.

.. tab:: TOML

@@ -202,8 +202,6 @@ Basic example
env_list = ["format", "3.13"]
format
py310
[env.format]
description = "install black in a virtual environment and invoke it on the current folder"
@@ -247,7 +245,7 @@ two run environments named ``format`` and ``3.13`` that should be run by default
specific environment.

The formatting environment and test environment are defined separately (via the ``env.format`` and ``env."3.13"`` in
TOML file; ``testenv:format`` and ``testenv:py310`` in INI file). For example to format the project we:
TOML file; ``testenv:format`` and ``testenv:py313`` in INI file). For example to format the project we:

- add a description (visible when you type ``tox list`` into the command line) via the :ref:`description` setting
- define that it requires the :pypi:`black` dependency with version ``22.3.0`` via the :ref:`deps` setting
124 changes: 62 additions & 62 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
build-backend = "hatchling.build"
requires = [
"hatch-vcs>=0.4",
"hatchling>=1.25",
"hatchling>=1.26.3",
]

[project]
@@ -54,13 +54,13 @@ dependencies = [
"chardet>=5.2",
"colorama>=0.4.6",
"filelock>=3.16.1",
"packaging>=24.1",
"packaging>=24.2",
"platformdirs>=4.3.6",
"pluggy>=1.5",
"pyproject-api>=1.8",
"tomli>=2.0.1; python_version<'3.11'",
"tomli>=2.1; python_version<'3.11'",
"typing-extensions>=4.12.2; python_version<'3.11'",
"virtualenv>=20.26.6",
"virtualenv>=20.27.1",
]
optional-dependencies.test = [
"devpi-process>=1.0.2",
@@ -74,6 +74,63 @@ urls.Source = "https://github.com/tox-dev/tox"
urls.Tracker = "https://github.com/tox-dev/tox/issues"
scripts.tox = "tox.run:run"

[dependency-groups]
dev = [
{ include-group = "docs" },
{ include-group = "test" },
{ include-group = "type" },
]
test = [
"build[virtualenv]>=1.2.2.post1",
"covdefaults>=2.3",
"detect-test-pollution>=1.2",
"devpi-process>=1.0.2",
"diff-cover>=9.2",
"distlib>=0.3.9",
"flaky>=3.8.1",
"hatch-vcs>=0.4",
"hatchling>=1.26.3",
"psutil>=6.1",
"pytest>=8.3.3",
"pytest-cov>=5",
"pytest-mock>=3.14",
"pytest-xdist>=3.6.1",
"re-assert>=1.1",
"setuptools>=75.1; python_version<='3.8'",
"setuptools>=75.6; python_version>'3.8'",
"time-machine>=2.15; implementation_name!='pypy'",
"wheel>=0.45",
]
type = [
"mypy==1.13",
"types-cachetools>=5.5.0.20240820",
"types-chardet>=5.0.4.6",
{ include-group = "test" },
]
docs = [
"furo>=2024.8.6",
"sphinx>=8.1.3",
"sphinx-argparse-cli>=1.18.2",
"sphinx-autodoc-typehints>=2.5",
"sphinx-copybutton>=0.5.2",
"sphinx-inline-tabs>=2023.4.21",
"sphinxcontrib-towncrier>=0.2.1a0",
"towncrier>=24.8",
]
fix = [
"pre-commit-uv>=4.1.4",
]
pkg-meta = [
"check-wheel-contents>=0.6",
"twine>=5.1.1",
"uv>=0.5.3",
]
release = [
"gitpython>=3.1.43",
"packaging>=24.2",
"towncrier>=24.8",
]

[tool.hatch]
build.dev-mode-dirs = [
"src",
@@ -96,8 +153,6 @@ lint.select = [
"ALL",
]
lint.ignore = [
"ANN101", # Missing type annotation for `self` in method
"ANN102", # Missing type annotation for `cls` in classmethod"
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `arg`"
"COM812", # conflicts with formatter
"CPY", # No copyright header
@@ -109,6 +164,7 @@ lint.ignore = [
"DOC501", # broken with sphinx docs
"INP001", # no implicit namespaces here
"ISC001", # conflicts with formatter
"LOG015", # we require use of the root logger for reporting
"PLR0914", ## Too many local variables
"PLR0917", ## Too many positional arguments
"S104", # Possible binding to all interfaces
@@ -197,59 +253,3 @@ overrides = [
"virtualenv.*",
], ignore_missing_imports = true },
]

[dependency-groups]
dev = [
{ include-group = "docs" },
{ include-group = "test" },
{ include-group = "type" },
]
docs = [
"furo>=2024.8.6",
"sphinx>=8.0.2",
"sphinx-argparse-cli>=1.18.2",
"sphinx-autodoc-typehints>=2.4.4",
"sphinx-copybutton>=0.5.2",
"sphinx-inline-tabs>=2023.4.21",
"sphinxcontrib-towncrier>=0.2.1a0",
"towncrier>=24.8",
]
fix = [
"pre-commit-uv>=4.1.3",
]
pkg-meta = [
"check-wheel-contents>=0.6",
"twine>=5.1.1",
"uv>=0.4.17",
]
release = [
"gitpython>=3.1.43",
"packaging>=24.1",
"towncrier>=24.8",
]
test = [
"build[virtualenv]>=1.2.2",
"covdefaults>=2.3",
"detect-test-pollution>=1.2",
"devpi-process>=1.0.2",
"diff-cover>=9.2",
"distlib>=0.3.8",
"flaky>=3.8.1",
"hatch-vcs>=0.4",
"hatchling>=1.25",
"psutil>=6",
"pytest>=8.3.3",
"pytest-cov>=5",
"pytest-mock>=3.14",
"pytest-xdist>=3.6.1",
"re-assert>=1.1",
"setuptools>=75.1",
"time-machine>=2.15; implementation_name!='pypy'",
"wheel>=0.44",
]
type = [
"mypy==1.11.2",
"types-cachetools>=5.5.0.20240820",
"types-chardet>=5.0.4.6",
{ include-group = "test" },
]
2 changes: 1 addition & 1 deletion src/tox/config/cli/parse.py
Original file line number Diff line number Diff line change
@@ -67,7 +67,7 @@ def _get_base(args: Sequence[str]) -> tuple[int, ToxHandler, Source]:
def _get_all(args: Sequence[str]) -> tuple[Parsed, dict[str, Callable[[State], int]]]:
"""Parse all the options."""
tox_parser = _get_parser()
parsed = cast(Parsed, tox_parser.parse_args(args))
parsed = cast("Parsed", tox_parser.parse_args(args))
handlers = {k: p for k, (_, p) in tox_parser.handlers.items()}
return parsed, handlers

6 changes: 3 additions & 3 deletions src/tox/config/cli/parser.py
Original file line number Diff line number Diff line change
@@ -79,7 +79,7 @@ def parse_args( # type: ignore[override] # avoid defining all overloads
res, argv = self.parse_known_args(args, namespace)
if argv:
self.error(
f'unrecognized arguments: {" ".join(argv)}\n'
f"unrecognized arguments: {' '.join(argv)}\n"
"hint: if you tried to pass arguments to a command use -- to separate them from tox ones",
)
return res
@@ -123,7 +123,7 @@ def verbosity(self) -> int:
@property
def is_colored(self) -> bool:
""":return: flag indicating if the output is colored or not"""
return cast(bool, self.colored == "yes")
return cast("bool", self.colored == "yes")

exit_and_dump_after: int

@@ -205,7 +205,7 @@ def __call__(
result = None
else:
try:
result = int(cast(str, values))
result = int(cast("str", values))
if result <= 0:
msg = "must be greater than zero"
raise ValueError(msg) # noqa: TRY301
2 changes: 1 addition & 1 deletion src/tox/config/loader/convert.py
Original file line number Diff line number Diff line change
@@ -87,7 +87,7 @@ def _to_typing(self, raw: T, of_type: type[V], factory: Factory[V]) -> V: # noq
raise ValueError(msg)
result = raw
if result is not _NO_MAPPING:
return cast(V, result)
return cast("V", result)
msg = f"{raw} cannot cast to {of_type!r}"
raise TypeError(msg)

4 changes: 2 additions & 2 deletions src/tox/config/loader/toml/__init__.py
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> TomlT
return self.content[key]

def load_raw_from_root(self, path: str) -> TomlTypes:
current = cast(TomlTypes, self._root_content)
current = cast("TomlTypes", self._root_content)
for key in path.split(self.section.SEP):
if isinstance(current, dict):
current = current[key]
@@ -98,7 +98,7 @@ def to_path(value: TomlTypes) -> Path:
@staticmethod
def to_command(value: TomlTypes) -> Command | None:
if value:
return Command(args=cast(List[str], value)) # validated during load in _ensure_type_correct
return Command(args=cast("List[str]", value)) # validated during load in _ensure_type_correct
return None

@staticmethod
15 changes: 8 additions & 7 deletions src/tox/config/loader/toml/_replace.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@
from tox.config.loader.replacer import MatchRecursionError, ReplaceReference, load_posargs, replace, replace_env
from tox.config.loader.stringify import stringify

from ._api import TomlTypes
from ._validate import validate

if TYPE_CHECKING:
@@ -16,6 +15,8 @@
from tox.config.sets import ConfigSet
from tox.config.source.toml_pyproject import TomlSection

from ._api import TomlTypes


class Unroll:
def __init__(self, conf: Config | None, loader: TomlLoader, args: ConfigLoadArgs) -> None:
@@ -39,7 +40,7 @@ def __call__(self, value: TomlTypes, depth: int = 0) -> TomlTypes: # noqa: C901
for val in value: # apply replacement for every entry
got = self(val, depth)
if isinstance(val, dict) and val.get("replace") and val.get("extend"):
res_list.extend(cast(List[Any], got))
res_list.extend(cast("List[Any]", got))
else:
res_list.append(got)
value = res_list
@@ -49,16 +50,16 @@ def __call__(self, value: TomlTypes, depth: int = 0) -> TomlTypes: # noqa: C901
if replace_type == "posargs" and self.conf is not None:
got_posargs = load_posargs(self.conf, self.args)
return (
[self(v, depth) for v in cast(List[str], value.get("default", []))]
[self(v, depth) for v in cast("List[str]", value.get("default", []))]
if got_posargs is None
else list(got_posargs)
)
if replace_type == "env":
return replace_env(
self.conf,
[
cast(str, validate(value["name"], str)),
cast(str, validate(self(value.get("default", ""), depth), str)),
cast("str", validate(value["name"], str)),
cast("str", validate(self(value.get("default", ""), depth), str)),
],
self.args,
)
@@ -73,9 +74,9 @@ def __call__(self, value: TomlTypes, depth: int = 0) -> TomlTypes: # noqa: C901

def _replace_ref(self, value: dict[str, TomlTypes], depth: int) -> TomlTypes:
if self.conf is not None and (env := value.get("env")) and (key := value.get("key")):
return cast(TomlTypes, self.conf.get_env(cast(str, env))[cast(str, key)])
return cast("TomlTypes", self.conf.get_env(cast("str", env))[cast("str", key)])
if of := value.get("of"):
validated_of = cast(List[str], validate(of, List[str]))
validated_of = cast("List[str]", validate(of, List[str]))
loaded = self.loader.load_raw_from_root(self.loader.section.SEP.join(validated_of))
return self(loaded, depth)
return value
2 changes: 1 addition & 1 deletion src/tox/config/loader/toml/_validate.py
Original file line number Diff line number Diff line change
@@ -76,7 +76,7 @@ def validate(val: TomlTypes, of_type: type[T]) -> TypeGuard[T]: # noqa: C901, P
msg = f"{val!r} is not of type {of_type.__name__!r}"
if msg:
raise TypeError(msg)
return cast(T, val) # type: ignore[return-value] # logic too complicated for mypy
return cast("T", val) # type: ignore[return-value] # logic too complicated for mypy


__all__ = [
2 changes: 1 addition & 1 deletion src/tox/config/of_type.py
Original file line number Diff line number Diff line change
@@ -118,7 +118,7 @@ def __call__(
if self.post_process is not None:
value = self.post_process(value)
self._cache = value
return cast(T, self._cache)
return cast("T", self._cache)

def __repr__(self) -> str:
values = ((k, v) for k, v in vars(self).items() if k not in {"post_process", "_cache"} and v is not None)
16 changes: 11 additions & 5 deletions src/tox/config/sets.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
import sys
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Iterator, Mapping, Sequence, TypeVar, cast
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Mapping, Sequence, TypeVar, cast

from .of_type import ConfigConstantDefinition, ConfigDefinition, ConfigDynamicDefinition, ConfigLoadArgs
from .set_env import SetEnv
@@ -33,6 +33,12 @@ def __init__(self, conf: Config, section: Section, env_name: str | None) -> None
self._final = False
self.register_config()

def get_configs(self) -> Generator[ConfigDefinition[Any], None, None]:
""":return: a mapping of config keys to their definitions"""
for k, v in self._defined.items():
if k == next(iter(v.keys)):
yield v

@abstractmethod
def register_config(self) -> None:
raise NotImplementedError
@@ -67,7 +73,7 @@ def add_config( # noqa: PLR0913
keys_ = self._make_keys(keys)
definition = ConfigDynamicDefinition(keys_, desc, of_type, default, post_process, factory)
result = self._add_conf(keys_, definition)
return cast(ConfigDynamicDefinition[V], result)
return cast("ConfigDynamicDefinition[V]", result)

def add_constant(self, keys: str | Sequence[str], desc: str, value: V) -> ConfigConstantDefinition[V]:
"""
@@ -84,7 +90,7 @@ def add_constant(self, keys: str | Sequence[str], desc: str, value: V) -> Config
keys_ = self._make_keys(keys)
definition = ConfigConstantDefinition(keys_, desc, value)
result = self._add_conf(keys_, definition)
return cast(ConfigConstantDefinition[V], result)
return cast("ConfigConstantDefinition[V]", result)

@staticmethod
def _make_keys(keys: str | Sequence[str]) -> Sequence[str]:
@@ -182,10 +188,10 @@ def __init__(self, conf: Config, section: Section, root: Path, src_path: Path) -
self.add_config(keys=["env_list", "envlist"], of_type=EnvList, default=EnvList([]), desc=desc)

def _default_work_dir(self, conf: Config, env_name: str | None) -> Path: # noqa: ARG002
return cast(Path, self["tox_root"] / ".tox")
return cast("Path", self["tox_root"] / ".tox")

def _default_temp_dir(self, conf: Config, env_name: str | None) -> Path: # noqa: ARG002
return cast(Path, self["work_dir"] / ".tmp")
return cast("Path", self["work_dir"] / ".tmp")

def _work_dir_post_process(self, folder: Path) -> Path:
return self._conf.work_dir if self._conf.options.work_dir else folder
2 changes: 1 addition & 1 deletion src/tox/config/source/toml_pyproject.py
Original file line number Diff line number Diff line change
@@ -92,7 +92,7 @@ def transform_section(self, section: Section) -> Section:

def get_loader(self, section: Section, override_map: OverrideMap) -> Loader[Any] | None:
current = self._our_content
sec = cast(TomlSection, section)
sec = cast("TomlSection", section)
for key in sec.keys:
if key in current:
current = current[key]
17 changes: 12 additions & 5 deletions src/tox/execute/api.py
Original file line number Diff line number Diff line change
@@ -52,15 +52,15 @@ def register_conf(cls, env: ToxEnv) -> None:

@property
def suicide_timeout(self) -> float:
return cast(float, self._env.conf["suicide_timeout"])
return cast("float", self._env.conf["suicide_timeout"])

@property
def interrupt_timeout(self) -> float:
return cast(float, self._env.conf["interrupt_timeout"])
return cast("float", self._env.conf["interrupt_timeout"])

@property
def terminate_timeout(self) -> float:
return cast(float, self._env.conf["terminate_timeout"])
return cast("float", self._env.conf["terminate_timeout"])


class ExecuteStatus(ABC):
@@ -125,8 +125,15 @@ def call(
try:
# collector is what forwards the content from the file streams to the standard streams
out, err = out_err[0].buffer, out_err[1].buffer
out_sync = SyncWrite(out.name, out if show else None)
err_sync = SyncWrite(err.name, err if show else None, Fore.RED if self._colored else None)
out_sync = SyncWrite(
out.name,
out if show else None, # type: ignore[arg-type]
)
err_sync = SyncWrite(
err.name,
err if show else None, # type: ignore[arg-type]
Fore.RED if self._colored else None,
)
with out_sync, err_sync:
instance = self.build_instance(request, self._option_class(env), out_sync, err_sync)
with instance as status:
3 changes: 3 additions & 0 deletions src/tox/execute/local_sub_process/__init__.py
Original file line number Diff line number Diff line change
@@ -214,6 +214,9 @@ def __enter__(self) -> ExecuteStatus:
env=self.request.env,
)
except OSError as exception:
# We log a nice error message to avout returning opaque error codes,
# like exit code 2 (filenotfound).
logging.error("Exception running subprocess %s", str(exception)) # noqa: TRY400
return LocalSubprocessExecuteFailedStatus(self.options, self._out, self._err, exception.errno)

status = LocalSubprocessExecuteStatus(self.options, self._out, self._err, process)
6 changes: 3 additions & 3 deletions src/tox/execute/request.py
Original file line number Diff line number Diff line change
@@ -59,9 +59,9 @@ def shell_cmd(self) -> str:
exe = str(Path(self.cmd[0]).relative_to(self.cwd))
except ValueError:
exe = self.cmd[0]
_cmd = [exe]
_cmd.extend(self.cmd[1:])
return shell_cmd(_cmd)
cmd = [exe]
cmd.extend(self.cmd[1:])
return shell_cmd(cmd)

def __repr__(self) -> str:
return f"{self.__class__.__name__}(cmd={self.cmd!r}, cwd={self.cwd!r}, env=..., stdin={self.stdin!r})"
2 changes: 2 additions & 0 deletions src/tox/plugin/manager.py
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
legacy,
list_env,
quickstart,
schema,
show_config,
version_flag,
)
@@ -60,6 +61,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
exec_,
quickstart,
show_config,
schema,
devenv,
list_env,
depends,
6 changes: 3 additions & 3 deletions src/tox/provision.py
Original file line number Diff line number Diff line change
@@ -20,12 +20,12 @@
from tox.report import HandledError
from tox.tox_env.errors import Skip
from tox.tox_env.python.pip.req_file import PythonDeps
from tox.tox_env.python.runner import PythonRun

if TYPE_CHECKING:
from argparse import ArgumentParser

from tox.session.state import State
from tox.tox_env.python.runner import PythonRun


@impl
@@ -141,7 +141,7 @@ def _get_missing(requires: list[Requirement]) -> list[tuple[Requirement, str | N


def run_provision(name: str, state: State) -> int:
tox_env: PythonRun = cast(PythonRun, state.envs[name])
tox_env: PythonRun = cast("PythonRun", state.envs[name])
env_python = tox_env.env_python()
logging.info("will run in a automatically provisioned python environment under %s", env_python)
try:
@@ -152,4 +152,4 @@ def run_provision(name: str, state: State) -> int:
args: list[str] = [str(env_python), "-m", "tox"]
args.extend(state.args)
outcome = tox_env.execute(cmd=args, stdin=StdinSource.user_only(), show=True, run_id="provision", cwd=Path.cwd())
return cast(int, outcome.exit_code)
return cast("int", outcome.exit_code)
4 changes: 2 additions & 2 deletions src/tox/pytest.py
Original file line number Diff line number Diff line change
@@ -291,7 +291,7 @@ def our_setup_state(value: Sequence[str]) -> State:
msg = "exit code not set"
raise RuntimeError(msg)
out, err = self._capfd.readouterr()
return ToxRunOutcome(args, self.path, cast(int, code), out, err, state)
return ToxRunOutcome(args, self.path, cast("int", code), out, err, state)

def __repr__(self) -> str:
return f"{type(self).__name__}(path={self.path}) at {id(self)}"
@@ -488,7 +488,7 @@ def pypi_server(tmp_path_factory: pytest.TempPathFactory) -> Iterator[IndexServe
def _invalid_index_fake_port() -> int:
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as socket_handler:
socket_handler.bind(("", 0))
return cast(int, socket_handler.getsockname()[1])
return cast("int", socket_handler.getsockname()[1])


@pytest.fixture(autouse=True)
4 changes: 2 additions & 2 deletions src/tox/session/cmd/depends.py
Original file line number Diff line number Diff line change
@@ -4,11 +4,11 @@

from tox.plugin import impl
from tox.session.cmd.run.common import env_run_create_flags, run_order
from tox.tox_env.runner import RunToxEnv

if TYPE_CHECKING:
from tox.config.cli.parser import ToxParser
from tox.session.state import State
from tox.tox_env.runner import RunToxEnv


@impl
@@ -34,7 +34,7 @@ def _handle(at: int, env: str) -> None:
print(" " * at, end="") # noqa: T201
print(env, end="") # noqa: T201
if env != "ALL":
run_env = cast(RunToxEnv, state.envs[env])
run_env = cast("RunToxEnv", state.envs[env])
packager_list: list[str] = []
try:
for pkg_env in run_env.package_envs:
8 changes: 4 additions & 4 deletions src/tox/session/cmd/legacy.py
Original file line number Diff line number Diff line change
@@ -7,20 +7,20 @@

from tox.config.cli.parser import DEFAULT_VERBOSITY, Parsed, ToxParser
from tox.config.loader.memory import MemoryLoader
from tox.config.set_env import SetEnv
from tox.plugin import impl
from tox.session.cmd.run.common import env_run_create_flags
from tox.session.cmd.run.parallel import OFF_VALUE, parallel_flags, run_parallel
from tox.session.cmd.run.sequential import run_sequential
from tox.session.env_select import CliEnv, EnvSelector, register_env_select_flags
from tox.tox_env.python.pip.req_file import PythonDeps

from .devenv import devenv
from .list_env import list_env
from .show_config import show_config

if TYPE_CHECKING:
from tox.config.set_env import SetEnv
from tox.session.state import State
from tox.tox_env.python.pip.req_file import PythonDeps


@impl
@@ -131,10 +131,10 @@ def _handle_legacy_only_flags(option: Parsed, envs: EnvSelector) -> None: # noq
if override:
env_conf.loaders.insert(0, MemoryLoader(**override))
if set_env:
cast(SetEnv, env_conf["set_env"]).update(set_env, override=True)
cast("SetEnv", env_conf["set_env"]).update(set_env, override=True)
if forced:
to_force = forced.copy()
deps = cast(PythonDeps, env_conf["deps"])
deps = cast("PythonDeps", env_conf["deps"])
as_root_args = deps.as_root_args
for at, entry in enumerate(as_root_args):
try:
16 changes: 8 additions & 8 deletions src/tox/session/cmd/run/common.py
Original file line number Diff line number Diff line change
@@ -14,17 +14,17 @@

from colorama import Fore

from tox.config.types import EnvList
from tox.execute import Outcome
from tox.journal import write_journal
from tox.session.cmd.run.single import ToxEnvRunResult, run_one
from tox.tox_env.runner import RunToxEnv
from tox.util.graph import stable_topological_sort
from tox.util.spinner import MISS_DURATION, Spinner

if TYPE_CHECKING:
from tox.config.types import EnvList
from tox.session.state import State
from tox.tox_env.api import ToxEnv
from tox.tox_env.runner import RunToxEnv


class SkipMissingInterpreterAction(Action):
@@ -51,7 +51,7 @@ def __call__(
) -> None:
if not values:
raise ArgumentError(self, "cannot be empty")
path = Path(cast(str, values)).absolute()
path = Path(cast("str", values)).absolute()
if not path.exists():
raise ArgumentError(self, f"{path} does not exist")
if not path.is_file():
@@ -167,7 +167,7 @@ def execute(state: State, max_workers: int | None, has_spinner: bool, live: bool
state.envs.ensure_only_run_env_is_active()
to_run_list: list[str] = list(state.envs.iter())
for name in to_run_list:
cast(RunToxEnv, state.envs[name]).mark_active()
cast("RunToxEnv", state.envs[name]).mark_active()
previous, has_previous = None, False
try:
spinner = ToxSpinner(has_spinner, state, len(to_run_list))
@@ -260,7 +260,7 @@ def _run(tox_env: RunToxEnv) -> ToxEnvRunResult:
env_list: list[str] = []
while True:
for env in env_list: # queue all available
tox_env_to_run = cast(RunToxEnv, state.envs[env])
tox_env_to_run = cast("RunToxEnv", state.envs[env])
if interrupt.is_set(): # queue the rest as failed upfront
tox_env_to_run.teardown()
future: Future[ToxEnvRunResult] = Future()
@@ -322,7 +322,7 @@ def _handle_one_run_done(
) -> None:
success = result.code == Outcome.OK
spinner.update_spinner(result, success)
tox_env = cast(RunToxEnv, state.envs[result.name])
tox_env = cast("RunToxEnv", state.envs[result.name])
if tox_env.journal: # add overall journal entry
tox_env.journal["result"] = {
"success": success,
@@ -362,8 +362,8 @@ def run_order(state: State, to_run: list[str]) -> tuple[list[str], dict[str, set
to_run_set = set(to_run)
todo: dict[str, set[str]] = {}
for env in to_run:
run_env = cast(RunToxEnv, state.envs[env])
depends = set(cast(EnvList, run_env.conf["depends"]).envs)
run_env = cast("RunToxEnv", state.envs[env])
depends = set(cast("EnvList", run_env.conf["depends"]).envs)
todo[env] = to_run_set & depends
order = stable_topological_sort(todo)
return order, todo
2 changes: 1 addition & 1 deletion src/tox/session/cmd/run/parallel.py
Original file line number Diff line number Diff line change
@@ -62,7 +62,7 @@ def parallel_flags(
help="run tox environments in parallel, the argument controls limit: all,"
" auto - cpu count, some positive number, zero is turn off",
action="store",
type=parse_num_processes, # type: ignore[arg-type] # nargs confuses it
type=parse_num_processes,
default=default_parallel,
metavar="VAL",
**({"nargs": "?"} if no_args else {}), # type: ignore[arg-type] # type checker can't unroll it
6 changes: 3 additions & 3 deletions src/tox/session/cmd/run/single.py
Original file line number Diff line number Diff line change
@@ -62,7 +62,7 @@ def _evaluate(tox_env: RunToxEnv, no_test: bool) -> tuple[bool, int, list[Outcom
finally:
tox_env.teardown()
except SystemExit as exception: # setup command fails (interrupted or via invocation)
code = cast(int, exception.code)
code = cast("int", exception.code)
return skipped, code, outcomes


@@ -122,9 +122,9 @@ def run_command_set(
continue
if ignore_errors:
if exit_code == Outcome.OK:
exit_code = cast(int, exception.code) # ignore errors continues ahead but saves the exit code
exit_code = cast("int", exception.code) # ignore errors continues ahead but saves the exit code
continue
return cast(int, exception.code)
return cast("int", exception.code)
return exit_code


176 changes: 176 additions & 0 deletions src/tox/session/cmd/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Generate schema for tox configuration, respecting the current plugins."""

from __future__ import annotations

import json
import sys
import typing
from pathlib import Path
from typing import TYPE_CHECKING

import packaging.requirements
import packaging.version

import tox.config.set_env
import tox.config.types
import tox.tox_env.python.pip.req_file
from tox.plugin import impl

if TYPE_CHECKING:
from tox.config.cli.parser import ToxParser
from tox.config.sets import ConfigSet
from tox.session.state import State


@impl
def tox_add_option(parser: ToxParser) -> None:
our = parser.add_command("schema", [], "Generate schema for tox configuration", gen_schema)
our.add_argument("--strict", action="store_true", help="Disallow extra properties in configuration")


def _process_type(of_type: typing.Any) -> dict[str, typing.Any]: # noqa: C901, PLR0911
if of_type in {
Path,
str,
packaging.version.Version,
packaging.requirements.Requirement,
tox.tox_env.python.pip.req_file.PythonDeps,
}:
return {"type": "string"}
if typing.get_origin(of_type) is typing.Union:
types = [x for x in typing.get_args(of_type) if x is not type(None)]
if len(types) == 1:
return _process_type(types[0])
msg = f"Union types are not supported: {of_type}"
raise ValueError(msg)
if of_type is bool:
return {"type": "boolean"}
if of_type is float:
return {"type": "number"}
if typing.get_origin(of_type) is typing.Literal:
return {"enum": list(typing.get_args(of_type))}
if of_type in {tox.config.types.Command, tox.config.types.EnvList}:
return {"type": "array", "items": {"$ref": "#/definitions/subs"}}
if typing.get_origin(of_type) in {list, set}:
if typing.get_args(of_type)[0] in {str, packaging.requirements.Requirement}:
return {"type": "array", "items": {"$ref": "#/definitions/subs"}}
if typing.get_args(of_type)[0] is tox.config.types.Command:
return {"type": "array", "items": _process_type(typing.get_args(of_type)[0])}
msg = f"Unknown list type: {of_type}"
raise ValueError(msg)
if of_type is tox.config.set_env.SetEnv:
return {
"type": "object",
"additionalProperties": {"$ref": "#/definitions/subs"},
}
if typing.get_origin(of_type) is dict:
return {
"type": "object",
"additionalProperties": {**_process_type(typing.get_args(of_type)[1])},
}
msg = f"Unknown type: {of_type}"
raise ValueError(msg)


def _get_schema(conf: ConfigSet, path: str) -> dict[str, dict[str, typing.Any]]:
properties = {}
for x in conf.get_configs():
name, *aliases = x.keys
of_type = getattr(x, "of_type", None)
if of_type is None:
continue
desc = getattr(x, "desc", None)
try:
properties[name] = {**_process_type(of_type), "description": desc}
except ValueError:
print(name, "has unrecoginsed type:", of_type, file=sys.stderr) # noqa: T201
for alias in aliases:
properties[alias] = {"$ref": f"{path}/{name}"}
return properties


def gen_schema(state: State) -> int:
core = state.conf.core
strict = state.conf.options.strict

# Accessing this adds extra stuff to core, so we need to do it first
env_properties = _get_schema(state.envs["py"].conf, path="#/properties/env_run_base/properties")

properties = _get_schema(core, path="#/properties")

# This accesses plugins that register new sections (like tox-gh)
# Accessing a private member since this is not exposed yet and the
# interface includes the internal storage tuple
sections = {
key: conf
for s, conf in state.conf._key_to_conf_set.items() # noqa: SLF001
if (key := s[0].split(".")[0]) not in {"env_run_base", "env_pkg_base", "env"}
}
for key, conf in sections.items():
properties[key] = {
"type": "object",
"additionalProperties": not strict,
"properties": _get_schema(conf, path=f"#/properties/{key}/properties"),
}

json_schema = {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://github.com/tox-dev/tox/blob/main/src/tox/util/tox.schema.json",
"type": "object",
"properties": {
**properties,
"env_run_base": {
"type": "object",
"properties": env_properties,
"additionalProperties": not strict,
},
"env_pkg_base": {
"$ref": "#/properties/env_run_base",
"additionalProperties": not strict,
},
"env": {"type": "object", "patternProperties": {"^.*$": {"$ref": "#/properties/env_run_base"}}},
"legacy_tox_ini": {"type": "string"},
},
"additionalProperties": not strict,
"definitions": {
"subs": {
"anyOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"replace": {"type": "string"},
"name": {"type": "string"},
"default": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"$ref": "#/definitions/subs"}},
]
},
"extend": {"type": "boolean"},
},
"required": ["replace"],
"additionalProperties": False,
},
{
"type": "object",
"properties": {
"replace": {"type": "string"},
"of": {"type": "array", "items": {"type": "string"}},
"default": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"$ref": "#/definitions/subs"}},
]
},
"extend": {"type": "boolean"},
},
"required": ["replace", "of"],
"additionalProperties": False,
},
],
},
},
}
print(json.dumps(json_schema, indent=2)) # noqa: T201
return 0
8 changes: 5 additions & 3 deletions src/tox/session/cmd/version_flag.py
Original file line number Diff line number Diff line change
@@ -5,14 +5,16 @@
import sys
from argparse import SUPPRESS, Action, ArgumentParser, Namespace
from pathlib import Path
from typing import Any, Sequence, cast
from typing import TYPE_CHECKING, Any, Sequence, cast

import tox
from tox.config.cli.parser import HelpFormatter, ToxParser
from tox.plugin import impl
from tox.plugin.manager import MANAGER
from tox.version import version

if TYPE_CHECKING:
from tox.config.cli.parser import HelpFormatter, ToxParser


@impl
def tox_add_option(parser: ToxParser) -> None:
@@ -28,7 +30,7 @@ def __call__(
values: str | Sequence[Any] | None, # noqa: ARG002
option_string: str | None = None, # noqa: ARG002
) -> None:
formatter = cast(HelpFormatter, parser._get_formatter()) # noqa: SLF001
formatter = cast("HelpFormatter", parser._get_formatter()) # noqa: SLF001
formatter.add_raw_text(get_version_info())
parser._print_message(formatter.format_help(), sys.stdout) # noqa: SLF001
parser.exit()
10 changes: 5 additions & 5 deletions src/tox/session/env_select.py
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ class CliEnv: # noqa: PLW1641
as ``<env_list>``.
"""

def __init__(self, value: None | list[str] | str = None) -> None:
def __init__(self, value: list[str] | str | None = None) -> None:
if isinstance(value, str):
value = StrConvert().to(value, of_type=List[str], factory=None)
self._names: list[str] | None = value
@@ -148,14 +148,14 @@ def __init__(self, state: State) -> None:
self.on_empty_fallback_py = True
self._warned_about: set[str] = set() #: shared set of skipped environments that were already warned about
self._state = state
self._defined_envs_: None | dict[str, _ToxEnvInfo] = None
self._defined_envs_: dict[str, _ToxEnvInfo] | None = None
self._pkg_env_counter: Counter[str] = Counter()
from tox.plugin.manager import MANAGER # noqa: PLC0415

self._manager = MANAGER
self._log_handler = self._state._options.log_handler # noqa: SLF001
self._journal = self._state._journal # noqa: SLF001
self._provision: None | tuple[bool, str] = None
self._provision: tuple[bool, str] | None = None

self._state.conf.core.add_config("labels", Dict[str, EnvList], {}, "core labels")
tox_env_filter_regex = getattr(state.conf.options, "skip_env", "").strip()
@@ -205,7 +205,7 @@ def _ensure_envs_valid(self) -> None:
invalid_envs[env] = (
None
if any(i is None for i in factors.values())
else "-".join(cast(Iterable[str], factors.values()))
else "-".join(cast("Iterable[str]", factors.values()))
)
if invalid_envs:
msg = "provided environments not found in configuration file:\n"
@@ -316,7 +316,7 @@ def _build_run_env(self, name: str) -> RunToxEnv | None:
env_conf = self._state.conf.get_env(name, package=False)
desc = "the tox execute used to evaluate this environment"
env_conf.add_config(keys="runner", desc=desc, of_type=str, default=self._state.conf.options.default_runner)
runner = REGISTER.runner(cast(str, env_conf["runner"]))
runner = REGISTER.runner(cast("str", env_conf["runner"]))
journal = self._journal.get_env_journal(name)
args = ToxEnvCreateArgs(env_conf, self._state.conf.core, self._state.conf.options, journal, self._log_handler)
run_env = runner(args)
439 changes: 439 additions & 0 deletions src/tox/tox.schema.json

Large diffs are not rendered by default.

31 changes: 19 additions & 12 deletions src/tox/tox_env/api.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@
import sys
from abc import ABC, abstractmethod
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Sequence, Set, cast

@@ -20,6 +19,8 @@
from tox.util.path import ensure_empty_dir

if TYPE_CHECKING:
from io import BytesIO

from tox.config.cli.parser import Parsed
from tox.config.main import Config
from tox.config.set_env import SetEnv
@@ -111,19 +112,19 @@ def register_config(self) -> None:
self.conf.add_config(
keys=["env_dir", "envdir"],
of_type=Path,
default=lambda conf, name: cast(Path, conf.core["work_dir"]) / self.name, # noqa: ARG005
default=lambda conf, name: cast("Path", conf.core["work_dir"]) / self.name, # noqa: ARG005
desc="directory assigned to the tox environment",
)
self.conf.add_config(
keys=["env_tmp_dir", "envtmpdir"],
of_type=Path,
default=lambda conf, name: cast(Path, conf.core["work_dir"]) / self.name / "tmp", # noqa: ARG005
default=lambda conf, name: cast("Path", conf.core["work_dir"]) / self.name / "tmp", # noqa: ARG005
desc="a folder that is always reset at the start of the run",
)
self.conf.add_config(
keys=["env_log_dir", "envlogdir"],
of_type=Path,
default=lambda conf, name: cast(Path, conf.core["work_dir"]) / self.name / "log", # noqa: ARG005
default=lambda conf, name: cast("Path", conf.core["work_dir"]) / self.name / "log", # noqa: ARG005
desc="a folder for logging where tox will put logs of tool invocation",
)
self.executor.register_conf(self)
@@ -177,26 +178,26 @@ def pass_env_post_process(values: list[str]) -> list[str]:
assert self.installer is not None # noqa: S101 # trigger installer creation to allow config registration

def _recreate_default(self, conf: Config, value: str | None) -> bool: # noqa: ARG002
return cast(bool, self.options.recreate)
return cast("bool", self.options.recreate)

@property
def env_dir(self) -> Path:
""":return: the tox environments environment folder"""
return cast(Path, self.conf["env_dir"])
return cast("Path", self.conf["env_dir"])

@property
def env_tmp_dir(self) -> Path:
""":return: the tox environments temp folder"""
return cast(Path, self.conf["env_tmp_dir"])
return cast("Path", self.conf["env_tmp_dir"])

@property
def env_log_dir(self) -> Path:
""":return: the tox environments log folder"""
return cast(Path, self.conf["env_log_dir"])
return cast("Path", self.conf["env_log_dir"])

@property
def name(self) -> str:
return cast(str, self.conf["env_name"])
return cast("str", self.conf["env_name"])

def _default_set_env(self) -> dict[str, str]: # noqa: PLR6301
return {}
@@ -236,14 +237,20 @@ def _default_pass_env(self) -> list[str]: # noqa: PLR6301
],
)
else: # pragma: win32 no cover
env.append("TMPDIR") # temporary file location
env.extend(
[
"TMPDIR", # temporary file location
"NIX_LD", # nix-ld loader
"NIX_LD_LIBRARY_PATH", # nix-ld library path
],
)
return env

def setup(self) -> None:
"""Setup the tox environment."""
if self._run_state["setup"] is False: # pragma: no branch
self._platform_check()
recreate = cast(bool, self.conf["recreate"])
recreate = cast("bool", self.conf["recreate"])
if recreate:
self._clean(transitive=True)
try:
@@ -498,7 +505,7 @@ def close_and_read_out_err(self) -> tuple[bytes, bytes] | None:
if self._suspended_out_err is None: # pragma: no branch
return None # pragma: no cover
(out, err), self._suspended_out_err = self._suspended_out_err, None
out_b, err_b = cast(BytesIO, out.buffer).getvalue(), cast(BytesIO, err.buffer).getvalue()
out_b, err_b = cast("BytesIO", out.buffer).getvalue(), cast("BytesIO", err.buffer).getvalue()
out.close()
err.close()
return out_b, err_b
6 changes: 3 additions & 3 deletions src/tox/tox_env/package.py
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ def _func(*args: Any, **kwargs: Any) -> Any:
return meth(*args, **kwargs)
finally:
if file_locks:
cast(FileLock, file_lock).release()
cast("FileLock", file_lock).release()

return _func

@@ -73,13 +73,13 @@ def register_config(self) -> None:
self.core.add_config(
keys=["package_root", "setupdir"],
of_type=Path,
default=cast(Path, self.core["tox_root"]),
default=cast("Path", self.core["tox_root"]),
desc="indicates where the packaging root file exists (historically setup.py file or pyproject.toml now)",
)
self.conf.add_config(
keys=["package_root", "setupdir"],
of_type=Path,
default=cast(Path, self.core["package_root"]),
default=cast("Path", self.core["package_root"]),
desc="indicates where the packaging root file exists (historically setup.py file or pyproject.toml now)",
)

6 changes: 3 additions & 3 deletions src/tox/tox_env/python/api.py
Original file line number Diff line number Diff line change
@@ -145,7 +145,7 @@ def extract_base_python(cls, env_name: str) -> str | None:
match = PY_FACTORS_RE_EXPLICIT_VERSION.match(env_name)
if match:
found = match.groupdict()
candidates.append(f'{"pypy" if found["impl"] == "pypy" else ""}{found["version"]}')
candidates.append(f"{'pypy' if found['impl'] == 'pypy' else ''}{found['version']}")
else:
for factor in env_name.split("-"):
match = PY_FACTORS_RE.match(factor)
@@ -251,7 +251,7 @@ def _diff_msg(conf: dict[str, Any], old: dict[str, Any]) -> str:
changed = [f"{k}={old[k]!r}->{v!r}" for k, v in conf.items() if k in old and v != old[k]]
if changed:
result.append(f"changed {' | '.join(changed)}")
return f'python {", ".join(result)}'
return f"python {', '.join(result)}"

@abstractmethod
def prepend_env_var_path(self) -> list[Path]:
@@ -290,7 +290,7 @@ def base_python(self) -> PythonInfo:
raise Skip(msg)
raise NoInterpreter(base_pythons)

return cast(PythonInfo, self._base_python)
return cast("PythonInfo", self._base_python)

def _get_env_journal_python(self) -> dict[str, Any]:
return {
4 changes: 2 additions & 2 deletions src/tox/tox_env/python/package.py
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ def default_wheel_tag(conf: Config, env_name: str | None) -> str: # noqa: ARG00
# c-extension codes are trickier, but as of today both poetry/setuptools uses pypa/wheels logic
# https://github.com/pypa/wheel/blob/master/src/wheel/bdist_wheel.py#L234-L280
try:
run_py = cast(Python, run_env).base_python
run_py = cast("Python", run_env).base_python
except NoInterpreter:
run_py = None

@@ -116,7 +116,7 @@ def default_wheel_tag(conf: Config, env_name: str | None) -> str: # noqa: ARG00
)
pkg_env = run_env.conf["wheel_build_env"]
result = yield pkg_env, run_env.conf["package_tox_env_type"]
self._wheel_build_envs[pkg_env] = cast(PythonPackageToxEnv, result)
self._wheel_build_envs[pkg_env] = cast("PythonPackageToxEnv", result)

def child_pkg_envs(self, run_conf: EnvConfigSet) -> Iterator[PackageToxEnv]:
if run_conf["package"] == "wheel":
2 changes: 1 addition & 1 deletion src/tox/tox_env/python/pip/req/file.py
Original file line number Diff line number Diff line change
@@ -168,7 +168,7 @@ def options(self) -> Namespace:
@property
def requirements(self) -> list[ParsedRequirement]:
self._ensure_requirements_parsed()
return cast(List[ParsedRequirement], self._requirements)
return cast("List[ParsedRequirement]", self._requirements)

@property
def _parser(self) -> ArgumentParser:
6 changes: 3 additions & 3 deletions src/tox/tox_env/python/virtual_env/api.py
Original file line number Diff line number Diff line change
@@ -154,13 +154,13 @@ def prepend_env_var_path(self) -> list[Path]:
return list(dict.fromkeys((self.creator.bin_dir, self.creator.script_dir)))

def env_site_package_dir(self) -> Path:
return cast(Path, self.creator.purelib)
return cast("Path", self.creator.purelib)

def env_python(self) -> Path:
return cast(Path, self.creator.exe)
return cast("Path", self.creator.exe)

def env_bin_dir(self) -> Path:
return cast(Path, self.creator.script_dir)
return cast("Path", self.creator.script_dir)

@property
def runs_on_platform(self) -> str:
4 changes: 2 additions & 2 deletions src/tox/tox_env/python/virtual_env/package/cmd_builder.py
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ def register_config(self) -> None:
)

def requires(self) -> PythonDeps:
return cast(PythonDeps, self.conf["deps"])
return cast("PythonDeps", self.conf["deps"])

def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
self.setup()
@@ -121,7 +121,7 @@ def register_run_env(self, run_env: RunToxEnv) -> Generator[tuple[str, str], Pac
yield from super().register_run_env(run_env)
# in case the outcome is a sdist we'll use this to find out its metadata
result = yield f"{self.conf.name}_sdist_meta", Pep517VirtualEnvPackager.id()
self._sdist_meta_tox_env = cast(Pep517VirtualEnvPackager, result)
self._sdist_meta_tox_env = cast("Pep517VirtualEnvPackager", result)

def child_pkg_envs(self, run_conf: EnvConfigSet) -> Iterator[PackageToxEnv]: # noqa: ARG002
if self._sdist_meta_tox_env is not None: # pragma: no branch
10 changes: 5 additions & 5 deletions src/tox/tox_env/python/virtual_env/package/pyproject.py
Original file line number Diff line number Diff line change
@@ -171,7 +171,7 @@ def _add_config_settings(self, build_type: str) -> None:

@property
def pkg_dir(self) -> Path:
return cast(Path, self.conf["pkg_dir"])
return cast("Path", self.conf["pkg_dir"])

@property
def meta_folder(self) -> Path:
@@ -238,7 +238,7 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:
"package config for %s is editable, however the build backend %s does not support PEP-660, falling "
"back to editable-legacy - change your configuration to it",
names,
cast(Pep517VirtualEnvFrontend, self._frontend_).backend,
cast("Pep517VirtualEnvFrontend", self._frontend_).backend,
)
for env in targets:
env._defined["package"].value = "editable-legacy" # type: ignore[attr-defined] # noqa: SLF001
@@ -285,7 +285,7 @@ def perform_packaging(self, for_env: EnvConfigSet) -> list[Package]:

@property
def _package_temp_path(self) -> Path:
return cast(Path, self.core["temp_dir"]) / "package"
return cast("Path", self.core["temp_dir"]) / "package"

def _load_deps(self, for_env: EnvConfigSet) -> list[Requirement]:
# first check if this is statically available via PEP-621
@@ -344,15 +344,15 @@ def get_package_dependencies(self, for_env: EnvConfigSet) -> list[Requirement]:
with self._pkg_lock:
if self._package_dependencies is None: # pragma: no branch
self._ensure_meta_present(for_env)
requires: list[str] = cast(PathDistribution, self._distribution_meta).requires or []
requires: list[str] = cast("PathDistribution", self._distribution_meta).requires or []
self._package_dependencies = [Requirement(i) for i in requires] # pragma: no branch
return self._package_dependencies

def get_package_name(self, for_env: EnvConfigSet) -> str:
with self._pkg_lock:
if self._package_name is None: # pragma: no branch
self._ensure_meta_present(for_env)
self._package_name = cast(PathDistribution, self._distribution_meta).metadata["Name"]
self._package_name = cast("PathDistribution", self._distribution_meta).metadata["Name"]
return self._package_name

def _ensure_meta_present(self, for_env: EnvConfigSet) -> None:
8 changes: 4 additions & 4 deletions src/tox/tox_env/python/virtual_env/package/util.py
Original file line number Diff line number Diff line change
@@ -3,9 +3,9 @@
from copy import deepcopy
from typing import TYPE_CHECKING, Optional, Set, cast

from packaging.markers import Marker, Op, Variable # type: ignore[attr-defined]

if TYPE_CHECKING:
from packaging._parser import Op, Variable
from packaging.markers import Marker
from packaging.requirements import Requirement


@@ -67,10 +67,10 @@ def _extract_extra_markers(req: Requirement) -> tuple[Requirement, set[str | Non
new_markers.append(marker)
marker = markers.pop(0) if markers else None
if new_markers:
cast(Marker, req.marker)._markers = new_markers # noqa: SLF001
cast("Marker", req.marker)._markers = new_markers # noqa: SLF001
else:
req.marker = None
return req, cast(Set[Optional[str]], extra_markers) or {None}
return req, cast("Set[Optional[str]]", extra_markers) or {None}


def _get_extra(_marker: str | tuple[Variable, Op, Variable]) -> str | None:
2 changes: 1 addition & 1 deletion src/tox/tox_env/runner.py
Original file line number Diff line number Diff line change
@@ -100,7 +100,7 @@ def get_package_env_types(self) -> tuple[str, str] | None:
self.conf.add_config(
keys=["package_env"],
of_type=str,
default=f'{self.core["package_env"]}{"_external" if has_external_pkg else ""}',
default=f"{self.core['package_env']}{'_external' if has_external_pkg else ''}",
desc="tox environment used to package",
)
is_external = self.conf["package"] == "external"
2 changes: 1 addition & 1 deletion src/tox/tox_env/util.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ def _post_process_change_dir(value: Path) -> Path:
config.add_config(
keys=["change_dir", "changedir"],
of_type=Path,
default=lambda conf, name: cast(Path, conf.core["tox_root"]), # noqa: ARG005
default=lambda conf, name: cast("Path", conf.core["tox_root"]), # noqa: ARG005
desc="change to this working directory when executing the test command",
post_process=_post_process_change_dir,
)
2 changes: 2 additions & 0 deletions tests/config/cli/conftest.py
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
from tox.session.cmd.quickstart import quickstart
from tox.session.cmd.run.parallel import run_parallel
from tox.session.cmd.run.sequential import run_sequential
from tox.session.cmd.schema import gen_schema
from tox.session.cmd.show_config import show_config

if TYPE_CHECKING:
@@ -23,6 +24,7 @@ def core_handlers() -> dict[str, Callable[[State], int]]:
return {
"config": show_config,
"c": show_config,
"schema": gen_schema,
"list": list_env,
"l": list_env,
"run": run_sequential,
2 changes: 1 addition & 1 deletion tests/config/loader/ini/replace/test_replace_tox_env.py
Original file line number Diff line number Diff line change
@@ -189,7 +189,7 @@ def test_replace_from_tox_section_missing_value(tox_ini_conf: ToxIniCreator) ->
def test_replace_from_section_bad_type(tox_ini_conf: ToxIniCreator) -> None:
conf_a = tox_ini_conf("[testenv:e]\nx = {[m]a}\n[m]\na=w\n").get_env("e")
conf_a.add_config(keys="x", of_type=int, default=1, desc="d")
with pytest.raises(ValueError, match="invalid literal.*w.*"):
with pytest.raises(ValueError, match=r"invalid literal.*w.*"):
assert conf_a["x"]


4 changes: 2 additions & 2 deletions tests/config/loader/test_toml_loader.py
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ def test_toml_loader_list_ok() -> None:


def test_toml_loader_list_nok() -> None:
with pytest.raises(TypeError, match="{} is not list"):
with pytest.raises(TypeError, match=r"{} is not list"):
perform_load({}, List[str])


@@ -77,7 +77,7 @@ def test_toml_loader_dict_ok() -> None:


def test_toml_loader_dict_nok() -> None:
with pytest.raises(TypeError, match="{'a'} is not dictionary"):
with pytest.raises(TypeError, match=r"{'a'} is not dictionary"):
perform_load({"a"}, Dict[str, str])


2 changes: 1 addition & 1 deletion tests/config/source/test_toml_pyproject.py
Original file line number Diff line number Diff line change
@@ -387,7 +387,7 @@ def test_config_set_env_ref(tox_project: ToxProjectCreator) -> None:
]
"""
})
outcome = project.run("c", "-e" "t", "-k", "set_env", "--hashseed", "1")
outcome = project.run("c", "-et", "-k", "set_env", "--hashseed", "1")
outcome.assert_success()
out = (
"[testenv:t]\n"
27 changes: 16 additions & 11 deletions tests/execute/local_subprocess/test_local_subprocess.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
import locale
import logging
import os
import re
import shutil
import stat
import subprocess
@@ -39,8 +40,8 @@ def __init__(self) -> None:
)

def read_out_err(self) -> tuple[str, str]:
out_got = self.out_err[0].buffer.getvalue().decode(self.out_err[0].encoding) # type: ignore[attr-defined]
err_got = self.out_err[1].buffer.getvalue().decode(self.out_err[1].encoding) # type: ignore[attr-defined]
out_got = self.out_err[0].buffer.getvalue().decode(self.out_err[0].encoding)
err_got = self.out_err[1].buffer.getvalue().decode(self.out_err[1].encoding)
return out_got, err_got


@@ -233,14 +234,14 @@ def test_local_execute_basic_fail(capsys: CaptureFixture, caplog: LogCaptureFixt
assert record.levelno == logging.CRITICAL
assert record.msg == "exit %s (%.2f seconds) %s> %s%s"
assert record.args is not None
_code, _duration, _cwd, _cmd, _metadata = record.args
assert _code == 3
assert _cwd == cwd
assert _cmd == request.shell_cmd
assert isinstance(_duration, float)
assert _duration > 0
assert isinstance(_metadata, str)
assert _metadata.startswith(" pid=")
code, duration, cwd_, cmd_, metadata = record.args
assert code == 3
assert cwd_ == cwd
assert cmd_ == request.shell_cmd
assert isinstance(duration, float)
assert duration > 0
assert isinstance(metadata, str)
assert metadata.startswith(" pid=")


def test_command_does_not_exist(caplog: LogCaptureFixture, os_env: dict[str, str]) -> None:
@@ -264,7 +265,11 @@ def test_command_does_not_exist(caplog: LogCaptureFixture, os_env: dict[str, str
assert outcome.exit_code != Outcome.OK
assert not outcome.out
assert not outcome.err
assert not caplog.records
assert len(caplog.records) == 1
assert caplog.records[0].levelname == "ERROR"
assert re.match(
r".*(No such file or directory|The system cannot find the file specified).*", caplog.records[0].message
)


@pytest.mark.skipif(sys.platform == "win32", reason="You need a conhost shell for keyboard interrupt")
19 changes: 19 additions & 0 deletions tests/session/cmd/test_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

import json
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pathlib import Path

from tox.pytest import MonkeyPatch, ToxProjectCreator


def test_show_schema_empty_dir(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.chdir(tmp_path)

project = tox_project({})
result = project.run("-qq", "schema")
schema = json.loads(result.out)
assert "properties" in schema
assert "tox_root" in schema["properties"]
2 changes: 1 addition & 1 deletion tests/session/cmd/test_sequential.py
Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@ def test_result_json_sequential(
expected_pkg = {"pip", "setuptools", "wheel", "a"}
assert {i[: i.find("==")] if "@" not in i else "a" for i in packaging_installed} == expected_pkg
install_package = log_report["testenvs"]["py"].pop("installpkg")
assert re.match("^[a-fA-F0-9]{64}$", install_package.pop("sha256"))
assert re.match(r"^[a-fA-F0-9]{64}$", install_package.pop("sha256"))
assert install_package == {"basename": "a-1.0-py3-none-any.whl", "type": "file"}

expected = {
4 changes: 3 additions & 1 deletion tests/session/cmd/test_show_config.py
Original file line number Diff line number Diff line change
@@ -126,7 +126,9 @@ def test_pass_env_config_default(tox_project: ToxProjectCreator, stdout_is_atty:
+ ["CPPFLAGS", "CURL_CA_BUNDLE", "CXX", "FORCE_COLOR", "HOME", "LANG"]
+ ["LANGUAGE", "LDFLAGS", "LD_LIBRARY_PATH"]
+ (["MSYSTEM"] if is_win else [])
+ ["NETRC", "NO_COLOR"]
+ ["NETRC"]
+ (["NIX_LD", "NIX_LD_LIBRARY_PATH"] if not is_win else [])
+ ["NO_COLOR"]
+ (["NUMBER_OF_PROCESSORS", "PATHEXT"] if is_win else [])
+ ["PIP_*", "PKG_CONFIG", "PKG_CONFIG_PATH", "PKG_CONFIG_SYSROOT_DIR"]
+ (["PROCESSOR_ARCHITECTURE"] if is_win else [])
8 changes: 4 additions & 4 deletions tests/test_provision.py
Original file line number Diff line number Diff line change
@@ -62,12 +62,12 @@ def _make_tox_wheel(
into = tmp_path_factory.mktemp("dist") # pragma: no cover
from tox.version import version_tuple # noqa: PLC0415

_patch_version = version_tuple[2]
if isinstance(_patch_version, str) and _patch_version[:3] == "dev":
patch_version = version_tuple[2]
if isinstance(patch_version, str) and patch_version[:3] == "dev":
# Version is in the form of 1.23.dev456, we need to increment the 456 part
version = f"{version_tuple[0]}.{version_tuple[1]}.dev{int(_patch_version[3:]) + 1}"
version = f"{version_tuple[0]}.{version_tuple[1]}.dev{int(patch_version[3:]) + 1}"
else:
version = f"{version_tuple[0]}.{version_tuple[1]}.{int(_patch_version) + 1}"
version = f"{version_tuple[0]}.{version_tuple[1]}.{int(patch_version) + 1}"

with mock.patch.dict(os.environ, {"SETUPTOOLS_SCM_PRETEND_VERSION": version}):
return pkg_builder(into, Path(__file__).parents[1], ["wheel"], False) # pragma: no cover
6 changes: 3 additions & 3 deletions tests/test_run.py
Original file line number Diff line number Diff line change
@@ -43,9 +43,9 @@ def test_custom_work_dir(tox_project: ToxProjectCreator, tmp_path: Path) -> None
assert outcome.state.conf.core["work_dir"], f"should set work_dir to {expected_work_dir}"

assert outcome.state.conf.core["tox_root"] == expected_tox_root, "should not update the value of tox_root"
assert outcome.state.conf.core["work_dir"] != (
expected_tox_root / ".tox"
), "should explicitly demonstrate that tox_root and work_dir are decoupled"
assert outcome.state.conf.core["work_dir"] != (expected_tox_root / ".tox"), (
"should explicitly demonstrate that tox_root and work_dir are decoupled"
)

# should update config values that depend on work_dir
assert outcome.state.conf.core["temp_dir"] == expected_work_dir / ".tmp"
4 changes: 2 additions & 2 deletions tests/tox_env/python/pip/req/test_file.py
Original file line number Diff line number Diff line change
@@ -401,7 +401,7 @@ def test_requirements_file_missing(tmp_path: Path) -> None:
requirements_file = tmp_path / "req.txt"
requirements_file.write_text("-r one.txt")
req_file = RequirementsFile(requirements_file, constraint=False)
with pytest.raises(ValueError, match="No such file or directory: .*one.txt"):
with pytest.raises(ValueError, match=r"No such file or directory: .*one.txt"):
assert req_file.options


@@ -423,7 +423,7 @@ def test_req_path_with_space_escape(tmp_path: Path) -> None:
dep_requirements_file = tmp_path / "a b"
dep_requirements_file.write_text("c")
path = f"-r {dep_requirements_file!s}"
path = f'{path[: -len("a b")]}a\\ b'
path = f"{path[: -len('a b')]}a\\ b"

requirements_file = tmp_path / "req.txt"
requirements_file.write_text(path)
Original file line number Diff line number Diff line change
@@ -39,13 +39,14 @@ def test_load_dependency_many_extra(pkg_with_extras: PathDistribution) -> None:
requires = pkg_with_extras.requires
assert requires is not None
result = dependencies_with_extras([Requirement(i) for i in requires], {"docs", "testing"}, "")
sphinx = [Requirement("sphinx>=3"), Requirement("sphinx-rtd-theme<1,>=0.4.3")]
exp = [
Requirement("platformdirs>=2.1"),
Requirement("colorama>=0.4.3"),
Requirement("sphinx>=3"),
Requirement("sphinx-rtd-theme<1,>=0.4.3"),
*(sphinx if sys.version_info[0:2] <= (3, 8) else []),
Requirement(f'covdefaults>=1.2; python_version == "2.7" or python_version == "{py_ver}"'),
Requirement(f'pytest>=5.4.1; python_version == "{py_ver}"'),
*(sphinx if sys.version_info[0:2] > (3, 8) else []),
]
for left, right in zip_longest(result, exp):
assert isinstance(right, Requirement)
4 changes: 2 additions & 2 deletions tests/tox_env/python/virtual_env/test_setuptools.py
Original file line number Diff line number Diff line change
@@ -8,12 +8,12 @@

from tox.tox_env.python.package import WheelPackage
from tox.tox_env.python.virtual_env.package.pyproject import Pep517VirtualEnvPackager
from tox.tox_env.runner import RunToxEnv

if TYPE_CHECKING:
from pathlib import Path

from tox.pytest import ToxProjectCreator
from tox.tox_env.runner import RunToxEnv


@pytest.mark.integration
@@ -35,7 +35,7 @@ def test_setuptools_package(

outcome.assert_success()
assert f"\ngreetings from demo_pkg_setuptools{os.linesep}" in outcome.out
tox_env = cast(RunToxEnv, outcome.state.envs["py"])
tox_env = cast("RunToxEnv", outcome.state.envs["py"])

(package_env,) = list(tox_env.package_envs)
assert isinstance(package_env, Pep517VirtualEnvPackager)
9 changes: 3 additions & 6 deletions tox.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
requires = ["tox>=4.21"]
requires = ["tox>=4.23.2"]
env_list = ["fix", "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "cov", "type", "docs", "pkg_meta"]
skip_missing_interpreters = true

@@ -51,7 +51,7 @@ commands = [
description = "format the code base to adhere to our styles, and complain about what we cannot do automatically"
skip_install = true
dependency_groups = ["fix"]
pass_env = [{ replace = "ref", of = ["env_run_base", "pass_env"], extend = true }, "PROGRAMDATA"]
pass_env = [{ replace = "ref", of = ["env_run_base", "pass_env"], extend = true }, "PROGRAMDATA", "DISABLE_PRE_COMMIT_UV_PATCH"]
commands = [["pre-commit", "run", "--all-files", "--show-diff-on-failure", { replace = "posargs", extend = true }]]

[env.type]
@@ -72,10 +72,7 @@ commands = [
"--color",
"-b",
"html",
{ replace = "posargs", default = [
"-b",
"linkcheck",
], extend = true },
{ replace = "posargs", default = [], extend = true },
"-W",
],
[