diff --git a/docs/changelog/1545.feature.rst b/docs/changelog/1545.feature.rst new file mode 100644 index 0000000000..755145e9ee --- /dev/null +++ b/docs/changelog/1545.feature.rst @@ -0,0 +1 @@ +Allow generative section name expansion. - by :user:`bruchar1` diff --git a/docs/config.rst b/docs/config.rst index 2f20732111..c159a50868 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -830,6 +830,27 @@ are stripped, so the following line defines the same environment names:: flake +.. _generative-sections: + +Generative section names +++++++++++++++++++++++++ + +.. versionadded:: 3.15 + +Using similar syntax, it is possible to generate sections:: + + [testenv:py{27,36}-flake] + +This is equivalent to defining distinct sections:: + + $ tox -a + py27-flake + py36-flake + +It is useful when you need an environment different from the default one, +but still want to take advantage of factor-conditional settings. + + .. _factors: Factors and factor-conditional settings diff --git a/docs/example/basic.rst b/docs/example/basic.rst index 6030bc1747..6e6edd0ec5 100644 --- a/docs/example/basic.rst +++ b/docs/example/basic.rst @@ -372,6 +372,29 @@ use :ref:`generative-envlist` and :ref:`conditional settings ` to expre # mocking sqlite on 2.7 and 3.6 if factor "sqlite" is present py{27,36}-sqlite: mock + +Using 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. + +.. code-block:: ini + + [testenv] + basepython = + py38-x86: python3.8-32 + py38-x64: python3.8-64 + commands = pytest + + [testenv:py38-{x86,x64}-venv] + usedevelop = true + envdir = + x86: .venv-x86 + x64: .venv-x64 + commands = + + Prevent symbolic links in virtualenv ------------------------------------ By default virtualenv will use symlinks to point to the system's python files, modules, etc. diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py index 922e3bc518..56400419a9 100644 --- a/src/tox/config/__init__.py +++ b/src/tox/config/__init__.py @@ -1010,6 +1010,8 @@ def __init__(self, config, ini_path, ini_data): # noqa self._cfg = py.iniconfig.IniConfig(config.toxinipath, ini_data) previous_line_of = self._cfg.lineof + self.expand_section_names(self._cfg) + def line_of_default_to_zero(section, name=None): at = previous_line_of(section, name=name) if at is None: @@ -1348,6 +1350,28 @@ def _getenvdata(self, reader, config): raise tox.exception.ConfigError(msg) return env_list, all_envs, _split_env(from_config), envlist_explicit + @staticmethod + def expand_section_names(config): + """Generative section names. + + Allow writing section as [testenv:py{36,37}-cov] + The parser will see it as two different sections: [testenv:py36-cov], [testenv:py37-cov] + + """ + factor_re = re.compile(r"\{\s*([\w\s,]+)\s*\}") + split_re = re.compile(r"\s*,\s*") + to_remove = set() + for section in list(config.sections): + split_section = factor_re.split(section) + for parts in itertools.product(*map(split_re.split, split_section)): + section_name = "".join(parts) + if section_name not in config.sections: + config.sections[section_name] = config.sections[section] + to_remove.add(section) + + for section in to_remove: + del config.sections[section] + def _split_env(env): """if handed a list, action="append" was used for -e """ diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 56e849e679..ba21a25b32 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -859,6 +859,15 @@ def test_getbool(self, newconfig): (msg,) = excinfo.value.args assert msg == "key5: boolean value 'yes' needs to be 'True' or 'False'" + def test_expand_section_name(self, newconfig): + config = newconfig( + """ + [testenv:custom-{one,two,three}-{four,five}-six] + """ + ) + assert "testenv:custom-one-five-six" in config._cfg.sections + assert "testenv:custom-{one,two,three}-{four,five}-six" not in config._cfg.sections + class TestIniParserPrefix: def test_basic_section_access(self, newconfig):