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.24.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.25.0
Choose a head ref
  • 6 commits
  • 11 files changed
  • 5 contributors

Commits on Mar 10, 2025

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

    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.31.2 → 0.31.3](python-jsonschema/check-jsonschema@0.31.2...0.31.3)
    - [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.9.10](astral-sh/ruff-pre-commit@v0.9.9...v0.9.10)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Mar 10, 2025
    Copy the full SHA
    794e6be View commit details

Commits on Mar 18, 2025

  1. Tests: Adjust expected exception message for Python 3.14.0a6 (#3500)

    E       AssertionError: Regex pattern did not match.
        E        Regex: '3 cannot cast to typing.Union\\[str, int\\]'
        E        Input: '3 cannot cast to str | int'
    
    Change caused likely by python/cpython@dc6d66f
    hroncok authored Mar 18, 2025
    Copy the full SHA
    beba4be View commit details
  2. [pre-commit.ci] pre-commit autoupdate (#3499)

    updates:
    - [github.com/abravalheri/validate-pyproject: v0.23 → v0.24](abravalheri/validate-pyproject@v0.23...v0.24)
    - [github.com/astral-sh/ruff-pre-commit: v0.9.10 → v0.11.0](astral-sh/ruff-pre-commit@v0.9.10...v0.11.0)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Mar 18, 2025
    Copy the full SHA
    f5f5cb1 View commit details

Commits on Mar 24, 2025

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

    updates:
    - [github.com/abravalheri/validate-pyproject: v0.24 → v0.24.1](abravalheri/validate-pyproject@v0.24...v0.24.1)
    - [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](astral-sh/ruff-pre-commit@v0.11.0...v0.11.2)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Mar 24, 2025
    Copy the full SHA
    5a67ae1 View commit details

Commits on Mar 27, 2025

  1. feat(config): Allow ranges in envlist (#3503)

    * feat(config): Allow ranges in envlist
    
    Implements #3502. Now it is possible to use ranges within the {} of an
    env specifier such as py3{10-13}.
    I chose to implement it as a pre-processing string replacement that just
    replaces the range with a literal enumeration of the range members.
    This is mainly to avoid more in-depth handling of these ranges when it
    coto generative environment lists.
    
    Also moves CircularChainError from `of_type` to `types` to avoid a
    circular import error. (kinda ironic :D)
    
    * fixup! feat(config): Allow ranges in envlist
    
    * fixup! feat(config): Allow ranges in envlist
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    * fixup! feat(config): Allow ranges in envlist
    
    * fixup! feat(config): Allow ranges in envlist
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com>
    3 people authored Mar 27, 2025
    Copy the full SHA
    0e6b4ad View commit details
  2. release 4.25.0

    gaborbernat committed Mar 27, 2025
    Copy the full SHA
    3d35559 View commit details
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.31.2
rev: 0.31.3
hooks:
- id: check-github-workflows
args: ["--verbose"]
@@ -19,11 +19,11 @@ repos:
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: "v0.23"
rev: "v0.24.1"
hooks:
- id: validate-pyproject
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.9.9"
rev: "v0.11.2"
hooks:
- id: ruff-format
- id: ruff
12 changes: 12 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -4,6 +4,18 @@ Release History

.. towncrier release notes start
v4.25.0 (2025-03-27)
--------------------

Features - 4.25.0
~~~~~~~~~~~~~~~~~
- Add support for number ranges in generative environments, more details :ref:`here<generative-environment-list>`. - by :user:`mimre25` (:issue:`3502`)

Bugfixes - 4.25.0
~~~~~~~~~~~~~~~~~
- Make tox tests pass with Python 3.14.0a6
- by :user:`hroncok` (:issue:`3500`)

v4.24.2 (2025-03-07)
--------------------

45 changes: 36 additions & 9 deletions docs/config.rst
Original file line number Diff line number Diff line change
@@ -1554,6 +1554,8 @@ Conditional settings
Here pip will be always installed as the configuration value is not conditional. black is only used for the ``format``
environment, while ``pytest`` is only installed for the ``py310`` and ``py39`` environments.

.. _generative-environment-list:

Generative environment list
~~~~~~~~~~~~~~~~~~~~~~~~~~~

@@ -1563,7 +1565,7 @@ If you have a large matrix of dependencies, python versions and/or environments
.. code-block:: ini
[tox]
env_list = py{311,310,39}-django{41,40}-{sqlite,mysql}
env_list = py3{9-11}-django{41,40}-{sqlite,mysql}
[testenv]
deps =
@@ -1582,24 +1584,49 @@ This will generate the following tox environments:
> tox l
default environments:
py311-django41-sqlite -> [no description]
py311-django41-mysql -> [no description]
py311-django40-sqlite -> [no description]
py311-django40-mysql -> [no description]
py310-django41-sqlite -> [no description]
py310-django41-mysql -> [no description]
py310-django40-sqlite -> [no description]
py310-django40-mysql -> [no description]
py39-django41-sqlite -> [no description]
py39-django41-mysql -> [no description]
py39-django40-sqlite -> [no description]
py39-django40-mysql -> [no description]
py310-django41-sqlite -> [no description]
py310-django41-mysql -> [no description]
py310-django40-sqlite -> [no description]
py310-django40-mysql -> [no description]
py311-django41-sqlite -> [no description]
py311-django41-mysql -> [no description]
py311-django40-sqlite -> [no description]
py311-django40-mysql -> [no description]
Both enumerations (``{1,2,3}``) and numerical ranges (``{1-3}``) are supported, and can be mixed together:

.. code-block:: ini
[tox]
env_list = py3{8-10, 11, 13-14}
will create the following envs:

.. code-block:: shell
> tox l
default environments:
py38 -> [no description]
py39 -> [no description]
py310 -> [no description]
py311 -> [no description]
py313 -> [no description]
py314 -> [no description]
Negative ranges will also be expanded (``{3-1}`` -> ``{3,2,1}``), however, open ranges such as ``{1-}``, ``{-2}``, ``{a-}``, and ``{-b}`` will not be expanded.



Generative section names
~~~~~~~~~~~~~~~~~~~~~~~~

Suppose you have some binary packages, and need to run tests both in 32 and 64 bits. You also want an environment to
create your virtual env for the developers.
This also supports ranges in the same way as generative environment lists.

.. code-block:: ini
16 changes: 15 additions & 1 deletion src/tox/config/loader/ini/factor.py
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ def find_factor_groups(value: str) -> Iterator[list[tuple[str, bool]]]:
yield result


_FACTOR_RE = re.compile(r"!?[\w._][\w._-]*")
_FACTOR_RE = re.compile(r"(?:!?[\w._][\w._-]*|^$)")


def expand_env_with_negation(value: str) -> Iterator[str]:
@@ -93,8 +93,22 @@ def is_negated(factor: str) -> bool:
return factor.startswith("!")


def expand_ranges(value: str) -> str:
"""Expand ranges in env expressions, eg py3{10-13} -> "py3{10,11,12,13}"""
matches = re.findall(r"((\d+)-(\d+)|\d+)(?:,|})", value)
for src, start_, end_ in matches:
if src and start_ and end_:
start = int(start_)
end = int(end_)
direction = 1 if start < end else -1
expansion = ",".join(str(x) for x in range(start, end + direction, direction))
value = value.replace(src, expansion, 1)
return value


__all__ = (
"expand_factors",
"expand_ranges",
"extend_factors",
"filter_for_env",
"find_envs",
2 changes: 1 addition & 1 deletion src/tox/config/loader/replacer.py
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Final, Sequence, Union

from tox.config.of_type import CircularChainError
from tox.config.types import CircularChainError
from tox.execute.request import shell_cmd

if TYPE_CHECKING:
2 changes: 2 additions & 0 deletions src/tox/config/loader/str_convert.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
from typing import TYPE_CHECKING, Any, Iterator

from tox.config.loader.convert import Convert
from tox.config.loader.ini.factor import expand_ranges
from tox.config.types import Command, EnvList

if TYPE_CHECKING:
@@ -113,6 +114,7 @@ def to_command(value: str) -> Command | None:
def to_env_list(value: str) -> EnvList:
from tox.config.loader.ini.factor import extend_factors # noqa: PLC0415

value = expand_ranges(value)
elements = list(chain.from_iterable(extend_factors(expr) for expr in value.split("\n")))
return EnvList(elements)

5 changes: 1 addition & 4 deletions src/tox/config/of_type.py
Original file line number Diff line number Diff line change
@@ -7,16 +7,13 @@
from typing import TYPE_CHECKING, Callable, Generic, Iterable, TypeVar, cast

from tox.config.loader.api import ConfigLoadArgs, Loader
from tox.config.types import CircularChainError

if TYPE_CHECKING:
from tox.config.loader.convert import Factory
from tox.config.main import Config # pragma: no cover


class CircularChainError(ValueError):
"""circular chain in config"""


T = TypeVar("T")
V = TypeVar("V")

4 changes: 2 additions & 2 deletions src/tox/config/source/ini_section.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from tox.config.loader.ini.factor import extend_factors
from tox.config.loader.ini.factor import expand_ranges, extend_factors
from tox.config.loader.section import Section


@@ -15,7 +15,7 @@ def is_test_env(self) -> bool:

@property
def names(self) -> list[str]:
return list(extend_factors(self.name))
return list(extend_factors(expand_ranges(self.name)))


TEST_ENV_PREFIX = "testenv"
4 changes: 4 additions & 0 deletions src/tox/config/types.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@
from tox.execute.request import shell_cmd


class CircularChainError(ValueError):
"""circular chain in config"""


class Command: # noqa: PLW1641
"""A command to execute."""

82 changes: 82 additions & 0 deletions tests/config/loader/ini/test_factor.py
Original file line number Diff line number Diff line change
@@ -178,6 +178,76 @@ def test_factor_config_no_env_list_creates_env(tox_ini_conf: ToxIniCreator) -> N
assert list(config) == ["py37-django15", "py37-django16", "py36"]


@pytest.mark.parametrize(
("env_list", "expected_envs"),
[
pytest.param("py3{10-13}", ["py310", "py311", "py312", "py313"], id="Expand positive range"),
pytest.param("py3{10-11},a", ["py310", "py311", "a"], id="Expand range and add additional env"),
pytest.param("py3{10-11},a{1-2}", ["py310", "py311", "a1", "a2"], id="Expand multiple env with ranges"),
pytest.param(
"py3{10-12,14}",
["py310", "py311", "py312", "py314"],
id="Expand ranges, and allow extra parameter in generator",
),
pytest.param(
"py3{8-10,12,14-16}",
["py38", "py39", "py310", "py312", "py314", "py315", "py316"],
id="Expand multiple ranges for one generator",
),
pytest.param(
"py3{10-11}-django1.{3-5}",
[
"py310-django1.3",
"py310-django1.4",
"py310-django1.5",
"py311-django1.3",
"py311-django1.4",
"py311-django1.5",
],
id="Expand ranges and factor multiple environment parts",
),
pytest.param(
"py3{10-11, 13}-django1.{3-4, 6}",
[
"py310-django1.3",
"py310-django1.4",
"py310-django1.6",
"py311-django1.3",
"py311-django1.4",
"py311-django1.6",
"py313-django1.3",
"py313-django1.4",
"py313-django1.6",
],
id="Expand ranges and parameters and factor multiple environment parts",
),
pytest.param(
"py3{10-11},a{1-2}-b{3-4}",
["py310", "py311", "a1-b3", "a1-b4", "a2-b3", "a2-b4"],
id="Expand ranges and parameters & factor multiple environment parts for multiple generative environments",
),
pytest.param("py3{13-11}", ["py313", "py312", "py311"], id="Expand negative ranges"),
pytest.param("3.{10-13}", ["3.10", "3.11", "3.12", "3.13"], id="Expand new-style python envs"),
pytest.param("py3{-11}", ["py3-11"], id="Don't expand left-open numerical range"),
pytest.param("foo{11-}", ["foo11-"], id="Don't expand right-open numerical range"),
pytest.param("foo{a-}", ["fooa-"], id="Don't expand right-open range"),
pytest.param("foo{-a}", ["foo-a"], id="Don't expand left-open range"),
pytest.param("foo{a-11}", ["fooa-11"], id="Don't expand alpha-umerical range"),
pytest.param("foo{13-a}", ["foo13-a"], id="Don't expand numerical-alpha range"),
pytest.param("foo{a-b}", ["fooa-b"], id="Don't expand non-numerical range"),
],
)
def test_env_list_expands_ranges(env_list: str, expected_envs: list[str], tox_ini_conf: ToxIniCreator) -> None:
config = tox_ini_conf(
f"""
[tox]
env_list = {env_list}
"""
)

assert list(config) == expected_envs


@pytest.mark.parametrize(
("env", "result"),
[
@@ -202,6 +272,18 @@ def test_ini_loader_raw_with_factors(
assert outcome == result


def test_generative_section_name_with_ranges(tox_ini_conf: ToxIniCreator) -> None:
config = tox_ini_conf(
"""
[testenv:py3{11-13}-{black,lint}]
deps-x =
black: black
lint: flake8
""",
)
assert list(config) == ["py311-black", "py311-lint", "py312-black", "py312-lint", "py313-black", "py313-lint"]


def test_generative_section_name(tox_ini_conf: ToxIniCreator) -> None:
config = tox_ini_conf(
"""
2 changes: 1 addition & 1 deletion tests/config/loader/test_str_convert.py
Original file line number Diff line number Diff line change
@@ -77,7 +77,7 @@ def test_str_convert_ok_py39(raw: str, value: Any, of_type: type[Any]) -> None:
[
("a", TypeVar, TypeError, r"a cannot cast to .*typing.TypeVar.*"),
("3", Literal["1", "2"], ValueError, r"3 must be one of \('1', '2'\)"),
("3", Union[str, int], TypeError, r"3 cannot cast to typing.Union\[str, int\]"),
("3", Union[str, int], TypeError, r"3 cannot cast to (typing.Union\[str, int\]|str \| int)"),
("", Command, ValueError, r"attempting to parse '' into a command failed"),
],
)