Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/3502.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ranges to generative environments such as py3{10-13}. - by :user:`mimre25`
39 changes: 30 additions & 9 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,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 =
Expand All @@ -1582,24 +1582,45 @@ 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 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]



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

Expand Down
17 changes: 16 additions & 1 deletion src/tox/config/loader/replacer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import logging
import os
import re
import sys
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:
Expand Down Expand Up @@ -287,9 +288,23 @@ def replace_tty(args: list[str]) -> str:
return (args[0] if len(args) > 0 else "") if sys.stdout.isatty() else args[1] if len(args) > 1 else ""


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__ = [
"MatchExpression",
"MatchRecursionError",
"expand_ranges",
"find_replace_expr",
"load_posargs",
"replace",
Expand Down
2 changes: 2 additions & 0 deletions src/tox/config/loader/str_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from typing import TYPE_CHECKING, Any, Iterator

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

if TYPE_CHECKING:
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 1 addition & 4 deletions src/tox/config/of_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

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

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


Expand All @@ -15,7 +16,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"
Expand Down
4 changes: 4 additions & 0 deletions src/tox/config/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
60 changes: 60 additions & 0 deletions tests/config/loader/ini/test_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,54 @@ 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"),
[
("py3{10-13}", ["py310", "py311", "py312", "py313"]),
Copy link
Member

@gaborbernat gaborbernat Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to see tests for the case when:

  • a-b
  • 8-b
  • a-9
  • a-
  • -b

Also, what about:

  • 9-7 ? Should we expand it or accept it? My initial thought is to expand it.

These cases should still be accepted, but not expanded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that the sponorlink (tidelift.com/funding/github/pypi/tox) doesn't seem to link to a funding page, but rather a general sales page of tidelift - is this indented?

Yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both 11- and a- run into a ValueError in the factor computations (I used foo{11-} and foo{a-}).
I'll take a look at fixing that.

On that note: This feature introduces syntax in the ini file that is not backwards compatible - should we be concerned about this?

9-7 ? Should we expand it or accept it? My initial thought is to expand it.

The current implementation expands it to 9,8,7.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm happy with expansion.

("py3{10-11},a", ["py310", "py311", "a"]),
("py3{10-11},a{1-2}", ["py310", "py311", "a1", "a2"]),
("py3{10-12,14}", ["py310", "py311", "py312", "py314"]),
("py3{8-10,12,14-16}", ["py38", "py39", "py310", "py312", "py314", "py315", "py316"]),
(
"py3{10-11}-django1.{3-5}",
[
"py310-django1.3",
"py310-django1.4",
"py310-django1.5",
"py311-django1.3",
"py311-django1.4",
"py311-django1.5",
],
),
(
"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",
],
),
("py3{10-11},a{1-2}-b{3-4}", ["py310", "py311", "a1-b3", "a1-b4", "a2-b3", "a2-b4"]),
("py3{13-11}", ["py313", "py312", "py311"]),
],
)
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"),
[
Expand All @@ -202,6 +250,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(
"""
Expand Down