Skip to content

Commit 0e6b4ad

Browse files
mimre25pre-commit-ci[bot]gaborbernat
authored
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 <[email protected]>
1 parent 5a67ae1 commit 0e6b4ad

File tree

9 files changed

+144
-17
lines changed

9 files changed

+144
-17
lines changed

docs/changelog/3502.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for number ranges in generative environments, more details :ref:`here<generative-environment-list>`. - by :user:`mimre25`

docs/config.rst

+36-9
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,8 @@ Conditional settings
15541554
Here pip will be always installed as the configuration value is not conditional. black is only used for the ``format``
15551555
environment, while ``pytest`` is only installed for the ``py310`` and ``py39`` environments.
15561556

1557+
.. _generative-environment-list:
1558+
15571559
Generative environment list
15581560
~~~~~~~~~~~~~~~~~~~~~~~~~~~
15591561

@@ -1563,7 +1565,7 @@ If you have a large matrix of dependencies, python versions and/or environments
15631565
.. code-block:: ini
15641566
15651567
[tox]
1566-
env_list = py{311,310,39}-django{41,40}-{sqlite,mysql}
1568+
env_list = py3{9-11}-django{41,40}-{sqlite,mysql}
15671569
15681570
[testenv]
15691571
deps =
@@ -1582,24 +1584,49 @@ This will generate the following tox environments:
15821584
15831585
> tox l
15841586
default environments:
1585-
py311-django41-sqlite -> [no description]
1586-
py311-django41-mysql -> [no description]
1587-
py311-django40-sqlite -> [no description]
1588-
py311-django40-mysql -> [no description]
1589-
py310-django41-sqlite -> [no description]
1590-
py310-django41-mysql -> [no description]
1591-
py310-django40-sqlite -> [no description]
1592-
py310-django40-mysql -> [no description]
15931587
py39-django41-sqlite -> [no description]
15941588
py39-django41-mysql -> [no description]
15951589
py39-django40-sqlite -> [no description]
15961590
py39-django40-mysql -> [no description]
1591+
py310-django41-sqlite -> [no description]
1592+
py310-django41-mysql -> [no description]
1593+
py310-django40-sqlite -> [no description]
1594+
py310-django40-mysql -> [no description]
1595+
py311-django41-sqlite -> [no description]
1596+
py311-django41-mysql -> [no description]
1597+
py311-django40-sqlite -> [no description]
1598+
py311-django40-mysql -> [no description]
1599+
1600+
Both enumerations (``{1,2,3}``) and numerical ranges (``{1-3}``) are supported, and can be mixed together:
1601+
1602+
.. code-block:: ini
1603+
1604+
[tox]
1605+
env_list = py3{8-10, 11, 13-14}
1606+
1607+
will create the following envs:
1608+
1609+
.. code-block:: shell
1610+
1611+
> tox l
1612+
default environments:
1613+
py38 -> [no description]
1614+
py39 -> [no description]
1615+
py310 -> [no description]
1616+
py311 -> [no description]
1617+
py313 -> [no description]
1618+
py314 -> [no description]
1619+
1620+
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.
1621+
1622+
15971623

15981624
Generative section names
15991625
~~~~~~~~~~~~~~~~~~~~~~~~
16001626

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

16041631
.. code-block:: ini
16051632

src/tox/config/loader/ini/factor.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def find_factor_groups(value: str) -> Iterator[list[tuple[str, bool]]]:
6666
yield result
6767

6868

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

7171

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

9595

96+
def expand_ranges(value: str) -> str:
97+
"""Expand ranges in env expressions, eg py3{10-13} -> "py3{10,11,12,13}"""
98+
matches = re.findall(r"((\d+)-(\d+)|\d+)(?:,|})", value)
99+
for src, start_, end_ in matches:
100+
if src and start_ and end_:
101+
start = int(start_)
102+
end = int(end_)
103+
direction = 1 if start < end else -1
104+
expansion = ",".join(str(x) for x in range(start, end + direction, direction))
105+
value = value.replace(src, expansion, 1)
106+
return value
107+
108+
96109
__all__ = (
97110
"expand_factors",
111+
"expand_ranges",
98112
"extend_factors",
99113
"filter_for_env",
100114
"find_envs",

src/tox/config/loader/replacer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from abc import ABC, abstractmethod
99
from typing import TYPE_CHECKING, Any, Final, Sequence, Union
1010

11-
from tox.config.of_type import CircularChainError
11+
from tox.config.types import CircularChainError
1212
from tox.execute.request import shell_cmd
1313

1414
if TYPE_CHECKING:

src/tox/config/loader/str_convert.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import TYPE_CHECKING, Any, Iterator
1111

1212
from tox.config.loader.convert import Convert
13+
from tox.config.loader.ini.factor import expand_ranges
1314
from tox.config.types import Command, EnvList
1415

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

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

src/tox/config/of_type.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77
from typing import TYPE_CHECKING, Callable, Generic, Iterable, TypeVar, cast
88

99
from tox.config.loader.api import ConfigLoadArgs, Loader
10+
from tox.config.types import CircularChainError
1011

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

1516

16-
class CircularChainError(ValueError):
17-
"""circular chain in config"""
18-
19-
2017
T = TypeVar("T")
2118
V = TypeVar("V")
2219

src/tox/config/source/ini_section.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from tox.config.loader.ini.factor import extend_factors
3+
from tox.config.loader.ini.factor import expand_ranges, extend_factors
44
from tox.config.loader.section import Section
55

66

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

1616
@property
1717
def names(self) -> list[str]:
18-
return list(extend_factors(self.name))
18+
return list(extend_factors(expand_ranges(self.name)))
1919

2020

2121
TEST_ENV_PREFIX = "testenv"

src/tox/config/types.py

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from tox.execute.request import shell_cmd
77

88

9+
class CircularChainError(ValueError):
10+
"""circular chain in config"""
11+
12+
913
class Command: # noqa: PLW1641
1014
"""A command to execute."""
1115

tests/config/loader/ini/test_factor.py

+82
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,76 @@ def test_factor_config_no_env_list_creates_env(tox_ini_conf: ToxIniCreator) -> N
178178
assert list(config) == ["py37-django15", "py37-django16", "py36"]
179179

180180

181+
@pytest.mark.parametrize(
182+
("env_list", "expected_envs"),
183+
[
184+
pytest.param("py3{10-13}", ["py310", "py311", "py312", "py313"], id="Expand positive range"),
185+
pytest.param("py3{10-11},a", ["py310", "py311", "a"], id="Expand range and add additional env"),
186+
pytest.param("py3{10-11},a{1-2}", ["py310", "py311", "a1", "a2"], id="Expand multiple env with ranges"),
187+
pytest.param(
188+
"py3{10-12,14}",
189+
["py310", "py311", "py312", "py314"],
190+
id="Expand ranges, and allow extra parameter in generator",
191+
),
192+
pytest.param(
193+
"py3{8-10,12,14-16}",
194+
["py38", "py39", "py310", "py312", "py314", "py315", "py316"],
195+
id="Expand multiple ranges for one generator",
196+
),
197+
pytest.param(
198+
"py3{10-11}-django1.{3-5}",
199+
[
200+
"py310-django1.3",
201+
"py310-django1.4",
202+
"py310-django1.5",
203+
"py311-django1.3",
204+
"py311-django1.4",
205+
"py311-django1.5",
206+
],
207+
id="Expand ranges and factor multiple environment parts",
208+
),
209+
pytest.param(
210+
"py3{10-11, 13}-django1.{3-4, 6}",
211+
[
212+
"py310-django1.3",
213+
"py310-django1.4",
214+
"py310-django1.6",
215+
"py311-django1.3",
216+
"py311-django1.4",
217+
"py311-django1.6",
218+
"py313-django1.3",
219+
"py313-django1.4",
220+
"py313-django1.6",
221+
],
222+
id="Expand ranges and parameters and factor multiple environment parts",
223+
),
224+
pytest.param(
225+
"py3{10-11},a{1-2}-b{3-4}",
226+
["py310", "py311", "a1-b3", "a1-b4", "a2-b3", "a2-b4"],
227+
id="Expand ranges and parameters & factor multiple environment parts for multiple generative environments",
228+
),
229+
pytest.param("py3{13-11}", ["py313", "py312", "py311"], id="Expand negative ranges"),
230+
pytest.param("3.{10-13}", ["3.10", "3.11", "3.12", "3.13"], id="Expand new-style python envs"),
231+
pytest.param("py3{-11}", ["py3-11"], id="Don't expand left-open numerical range"),
232+
pytest.param("foo{11-}", ["foo11-"], id="Don't expand right-open numerical range"),
233+
pytest.param("foo{a-}", ["fooa-"], id="Don't expand right-open range"),
234+
pytest.param("foo{-a}", ["foo-a"], id="Don't expand left-open range"),
235+
pytest.param("foo{a-11}", ["fooa-11"], id="Don't expand alpha-umerical range"),
236+
pytest.param("foo{13-a}", ["foo13-a"], id="Don't expand numerical-alpha range"),
237+
pytest.param("foo{a-b}", ["fooa-b"], id="Don't expand non-numerical range"),
238+
],
239+
)
240+
def test_env_list_expands_ranges(env_list: str, expected_envs: list[str], tox_ini_conf: ToxIniCreator) -> None:
241+
config = tox_ini_conf(
242+
f"""
243+
[tox]
244+
env_list = {env_list}
245+
"""
246+
)
247+
248+
assert list(config) == expected_envs
249+
250+
181251
@pytest.mark.parametrize(
182252
("env", "result"),
183253
[
@@ -202,6 +272,18 @@ def test_ini_loader_raw_with_factors(
202272
assert outcome == result
203273

204274

275+
def test_generative_section_name_with_ranges(tox_ini_conf: ToxIniCreator) -> None:
276+
config = tox_ini_conf(
277+
"""
278+
[testenv:py3{11-13}-{black,lint}]
279+
deps-x =
280+
black: black
281+
lint: flake8
282+
""",
283+
)
284+
assert list(config) == ["py311-black", "py311-lint", "py312-black", "py312-lint", "py313-black", "py313-lint"]
285+
286+
205287
def test_generative_section_name(tox_ini_conf: ToxIniCreator) -> None:
206288
config = tox_ini_conf(
207289
"""

0 commit comments

Comments
 (0)