From 310255e0b44b7bb01f41a67a41ad450ad227849d Mon Sep 17 00:00:00 2001 From: Martin Imre Date: Thu, 20 Mar 2025 17:15:32 +0100 Subject: [PATCH 1/6] 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) --- docs/changelog/3502.feature.rst | 1 + docs/config.rst | 39 +++++++++++++---- src/tox/config/loader/replacer.py | 17 +++++++- src/tox/config/loader/str_convert.py | 2 + src/tox/config/of_type.py | 5 +-- src/tox/config/source/ini_section.py | 3 +- src/tox/config/types.py | 4 ++ tests/config/loader/ini/test_factor.py | 60 ++++++++++++++++++++++++++ 8 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 docs/changelog/3502.feature.rst diff --git a/docs/changelog/3502.feature.rst b/docs/changelog/3502.feature.rst new file mode 100644 index 0000000000..85b6373bb3 --- /dev/null +++ b/docs/changelog/3502.feature.rst @@ -0,0 +1 @@ +Add ranges to generative environments such as py3{10-13}. - by :user:`mimre25` diff --git a/docs/config.rst b/docs/config.rst index fb099c3d9d..eb367a3a25 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -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 = @@ -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 diff --git a/src/tox/config/loader/replacer.py b/src/tox/config/loader/replacer.py index 05e3c060b5..148e8e8384 100644 --- a/src/tox/config/loader/replacer.py +++ b/src/tox/config/loader/replacer.py @@ -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: @@ -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", diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py index 65dd46dad7..e7878e5fe5 100644 --- a/src/tox/config/loader/str_convert.py +++ b/src/tox/config/loader/str_convert.py @@ -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: @@ -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) diff --git a/src/tox/config/of_type.py b/src/tox/config/of_type.py index d31922f6c2..4d49d16ef3 100644 --- a/src/tox/config/of_type.py +++ b/src/tox/config/of_type.py @@ -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") diff --git a/src/tox/config/source/ini_section.py b/src/tox/config/source/ini_section.py index d7d26720fc..548a6bab73 100644 --- a/src/tox/config/source/ini_section.py +++ b/src/tox/config/source/ini_section.py @@ -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 @@ -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" diff --git a/src/tox/config/types.py b/src/tox/config/types.py index 8d1300aa42..42c2c48149 100644 --- a/src/tox/config/types.py +++ b/src/tox/config/types.py @@ -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.""" diff --git a/tests/config/loader/ini/test_factor.py b/tests/config/loader/ini/test_factor.py index b73abd03be..ebc861dfd8 100644 --- a/tests/config/loader/ini/test_factor.py +++ b/tests/config/loader/ini/test_factor.py @@ -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"]), + ("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"), [ @@ -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( """ From d535b707e4246ce52c2a354711ec9b701eb24136 Mon Sep 17 00:00:00 2001 From: Martin Imre Date: Sun, 23 Mar 2025 14:10:53 +0100 Subject: [PATCH 2/6] fixup! feat(config): Allow ranges in envlist --- src/tox/config/loader/ini/factor.py | 2 +- tests/config/loader/ini/test_factor.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/tox/config/loader/ini/factor.py b/src/tox/config/loader/ini/factor.py index 1bb639bbf3..45e63e5b73 100644 --- a/src/tox/config/loader/ini/factor.py +++ b/src/tox/config/loader/ini/factor.py @@ -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]: diff --git a/tests/config/loader/ini/test_factor.py b/tests/config/loader/ini/test_factor.py index ebc861dfd8..b86c4a129d 100644 --- a/tests/config/loader/ini/test_factor.py +++ b/tests/config/loader/ini/test_factor.py @@ -213,6 +213,13 @@ def test_factor_config_no_env_list_creates_env(tox_ini_conf: ToxIniCreator) -> N ), ("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"]), + ("py3{-11}", ["py3-11"]), + ("foo{11-}", ["foo11-"]), + ("foo{a-}", ["fooa-"]), + ("foo{-a}", ["foo-a"]), + ("foo{a-11}", ["fooa-11"]), + ("foo{13-a}", ["foo13-a"]), + ("foo{a-b}", ["fooa-b"]), ], ) def test_env_list_expands_ranges(env_list: str, expected_envs: list[str], tox_ini_conf: ToxIniCreator) -> None: From 8f086c81ff0457b49f5c04cd01466821b05bb2b8 Mon Sep 17 00:00:00 2001 From: Martin Imre Date: Sun, 23 Mar 2025 16:23:23 +0100 Subject: [PATCH 3/6] fixup! feat(config): Allow ranges in envlist --- src/tox/config/loader/ini/factor.py | 12 ++++++++++++ src/tox/config/loader/replacer.py | 15 --------------- src/tox/config/loader/str_convert.py | 2 +- src/tox/config/source/ini_section.py | 3 +-- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/tox/config/loader/ini/factor.py b/src/tox/config/loader/ini/factor.py index 45e63e5b73..d10dd28d8d 100644 --- a/src/tox/config/loader/ini/factor.py +++ b/src/tox/config/loader/ini/factor.py @@ -92,10 +92,22 @@ def name_with_negate(factor: str) -> tuple[str, bool]: 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", "extend_factors", "filter_for_env", "find_envs", + "expand_ranges", ) diff --git a/src/tox/config/loader/replacer.py b/src/tox/config/loader/replacer.py index 148e8e8384..d1ab6a9e38 100644 --- a/src/tox/config/loader/replacer.py +++ b/src/tox/config/loader/replacer.py @@ -287,24 +287,9 @@ def replace_env(conf: Config | None, args: list[str], conf_args: ConfigLoadArgs) 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", diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py index e7878e5fe5..248afe2685 100644 --- a/src/tox/config/loader/str_convert.py +++ b/src/tox/config/loader/str_convert.py @@ -10,7 +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.loader.ini.factor import expand_ranges from tox.config.types import Command, EnvList if TYPE_CHECKING: diff --git a/src/tox/config/source/ini_section.py b/src/tox/config/source/ini_section.py index 548a6bab73..175444693a 100644 --- a/src/tox/config/source/ini_section.py +++ b/src/tox/config/source/ini_section.py @@ -1,7 +1,6 @@ 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.ini.factor import extend_factors, expand_ranges from tox.config.loader.section import Section From 3c4a8df9cb1b8c16bb43e3755d926fd10e11a693 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 15:23:47 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/tox/config/loader/ini/factor.py | 4 +++- src/tox/config/loader/replacer.py | 2 +- src/tox/config/source/ini_section.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tox/config/loader/ini/factor.py b/src/tox/config/loader/ini/factor.py index d10dd28d8d..11b4fa66d5 100644 --- a/src/tox/config/loader/ini/factor.py +++ b/src/tox/config/loader/ini/factor.py @@ -92,6 +92,7 @@ def name_with_negate(factor: str) -> tuple[str, bool]: 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) @@ -104,10 +105,11 @@ def expand_ranges(value: str) -> str: value = value.replace(src, expansion, 1) return value + __all__ = ( "expand_factors", + "expand_ranges", "extend_factors", "filter_for_env", "find_envs", - "expand_ranges", ) diff --git a/src/tox/config/loader/replacer.py b/src/tox/config/loader/replacer.py index d1ab6a9e38..5d9fd70ebd 100644 --- a/src/tox/config/loader/replacer.py +++ b/src/tox/config/loader/replacer.py @@ -4,7 +4,6 @@ import logging import os -import re import sys from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Final, Sequence, Union @@ -287,6 +286,7 @@ def replace_env(conf: Config | None, args: list[str], conf_args: ConfigLoadArgs) 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 "" + __all__ = [ "MatchExpression", "MatchRecursionError", diff --git a/src/tox/config/source/ini_section.py b/src/tox/config/source/ini_section.py index 175444693a..7ed3267f74 100644 --- a/src/tox/config/source/ini_section.py +++ b/src/tox/config/source/ini_section.py @@ -1,6 +1,6 @@ from __future__ import annotations -from tox.config.loader.ini.factor import extend_factors, expand_ranges +from tox.config.loader.ini.factor import expand_ranges, extend_factors from tox.config.loader.section import Section From f8dc100ab05a396a15ad33ccbd490f248ca14788 Mon Sep 17 00:00:00 2001 From: Martin Imre Date: Thu, 27 Mar 2025 09:15:04 +0100 Subject: [PATCH 5/6] fixup! feat(config): Allow ranges in envlist --- docs/changelog/3502.feature.rst | 2 +- docs/config.rst | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/changelog/3502.feature.rst b/docs/changelog/3502.feature.rst index 85b6373bb3..47772b8de4 100644 --- a/docs/changelog/3502.feature.rst +++ b/docs/changelog/3502.feature.rst @@ -1 +1 @@ -Add ranges to generative environments such as py3{10-13}. - by :user:`mimre25` +Add support for number ranges in generative environments, more details :ref:`here`. - by :user:`mimre25` diff --git a/docs/config.rst b/docs/config.rst index eb367a3a25..86e2826880 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1595,13 +1597,15 @@ This will generate the following tox environments: 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: +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 @@ -1613,6 +1617,8 @@ will create the following envs: 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 From 143270feaddac423c8db5a61c9190df7324f4a66 Mon Sep 17 00:00:00 2001 From: Martin Imre Date: Thu, 27 Mar 2025 09:15:18 +0100 Subject: [PATCH 6/6] fixup! feat(config): Allow ranges in envlist --- tests/config/loader/ini/test_factor.py | 47 +++++++++++++++++--------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/config/loader/ini/test_factor.py b/tests/config/loader/ini/test_factor.py index b86c4a129d..5dbc50b576 100644 --- a/tests/config/loader/ini/test_factor.py +++ b/tests/config/loader/ini/test_factor.py @@ -181,12 +181,20 @@ def test_factor_config_no_env_list_creates_env(tox_ini_conf: ToxIniCreator) -> N @pytest.mark.parametrize( ("env_list", "expected_envs"), [ - ("py3{10-13}", ["py310", "py311", "py312", "py313"]), - ("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"]), - ( + 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", @@ -196,8 +204,9 @@ def test_factor_config_no_env_list_creates_env(tox_ini_conf: ToxIniCreator) -> N "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", @@ -210,16 +219,22 @@ def test_factor_config_no_env_list_creates_env(tox_ini_conf: ToxIniCreator) -> N "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", ), - ("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"]), - ("py3{-11}", ["py3-11"]), - ("foo{11-}", ["foo11-"]), - ("foo{a-}", ["fooa-"]), - ("foo{-a}", ["foo-a"]), - ("foo{a-11}", ["fooa-11"]), - ("foo{13-a}", ["foo13-a"]), - ("foo{a-b}", ["fooa-b"]), + 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: