diff --git a/docs/changelog/2766.feature.rst b/docs/changelog/2766.feature.rst new file mode 100644 index 000000000..3e4d67104 --- /dev/null +++ b/docs/changelog/2766.feature.rst @@ -0,0 +1 @@ +``-f`` can be used multiple times and on hyphenated factors (e.g. ``-f py311-django -f py39``) - by :user:`sirosen`. diff --git a/src/tox/session/env_select.py b/src/tox/session/env_select.py index c746c25b5..f926beda3 100644 --- a/src/tox/session/env_select.py +++ b/src/tox/session/env_select.py @@ -91,8 +91,19 @@ def register_env_select_flags( if multiple: help_msg = "labels to evaluate" add_to.add_argument("-m", dest="labels", metavar="label", help=help_msg, default=[], type=str, nargs="+") - help_msg = "factors to evaluate" - add_to.add_argument("-f", dest="factors", metavar="factor", help=help_msg, default=[], type=str, nargs="+") + help_msg = ( + "factors to evaluate (passing multiple factors means 'AND', passing this option multiple times means 'OR')" + ) + add_to.add_argument( + "-f", + dest="factors", + metavar="factor", + help=help_msg, + default=[], + type=str, + nargs="+", + action="append", + ) help_msg = "exclude all environments selected that match this regular expression" add_to.add_argument("--skip-env", dest="skip_env", metavar="re", help=help_msg, default="", type=str) return add_to @@ -288,9 +299,17 @@ def _get_package_env(self, packager: str, name: str, is_active: bool) -> Package self._manager.tox_add_env_config(pkg_conf, self._state) return pkg_env + def _parse_factors(self) -> tuple[set[str], ...]: + # factors is a list of lists, from the combination of nargs="+" and action="append" + # also parse hyphenated factors into lists of factors + # so that `-f foo-bar` and `-f foo bar` are treated equivalently + raw_factors = getattr(self._state.conf.options, "factors", []) + return tuple({f for factor in factor_list for f in factor.split("-")} for factor_list in raw_factors) + def _mark_active(self) -> None: labels = set(getattr(self._state.conf.options, "labels", [])) - factors = set(getattr(self._state.conf.options, "factors", [])) + factors = self._parse_factors() + assert self._defined_envs_ is not None if labels or factors: for env_info in self._defined_envs_.values(): @@ -302,10 +321,12 @@ def _mark_active(self) -> None: for env_info in self._defined_envs_.values(): if labels.intersection(env_info.env.conf["labels"]): env_info.is_active = True - if self._state.conf.options.factors: # if matches mark it active + if factors: # if matches mark it active for name, env_info in self._defined_envs_.items(): - if factors.issubset(set(name.split("-"))): - env_info.is_active = True + for factor_set in factors: + if factor_set.issubset(set(name.split("-"))): + env_info.is_active = True + break def __getitem__(self, item: str) -> RunToxEnv | PackageToxEnv: """ diff --git a/tests/session/test_env_select.py b/tests/session/test_env_select.py index b6683541f..496107a37 100644 --- a/tests/session/test_env_select.py +++ b/tests/session/test_env_select.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + from tox.pytest import MonkeyPatch, ToxProjectCreator @@ -61,15 +63,36 @@ def test_label_core_and_trait(tox_project: ToxProjectCreator) -> None: outcome.assert_out_err("py310\npy39\nflake8\ntype\n", "") -def test_factor_select(tox_project: ToxProjectCreator) -> None: +@pytest.mark.parametrize( + ("selection_arguments", "expect_envs"), + [ + ( + ("-f", "cov", "django20"), + ("py310-django20-cov", "py39-django20-cov"), + ), + ( + ("-f", "cov-django20"), + ("py310-django20-cov", "py39-django20-cov"), + ), + ( + ("-f", "py39", "django20", "-f", "py310", "django21"), + ("py310-django21-cov", "py310-django21", "py39-django20-cov", "py39-django20"), + ), + ], +) +def test_factor_select( + tox_project: ToxProjectCreator, + selection_arguments: tuple[str, ...], + expect_envs: tuple[str, ...], +) -> None: ini = """ [tox] env_list = py3{10,9}-{django20,django21}{-cov,} """ project = tox_project({"tox.ini": ini}) - outcome = project.run("l", "--no-desc", "-f", "cov", "django20") + outcome = project.run("l", "--no-desc", *selection_arguments) outcome.assert_success() - outcome.assert_out_err("py310-django20-cov\npy39-django20-cov\n", "") + outcome.assert_out_err("{}\n".format("\n".join(expect_envs)), "") def test_tox_skip_env(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch) -> None: