Skip to content

Commit

Permalink
Improved factor selection to allow multiple uses of -f for "OR" and…
Browse files Browse the repository at this point in the history
… to allow hyphenated factors (#2786)

* Enable multiple uses of '-f' meaning 'OR'

Previously, when `-f` was passed, it overwrote the last value. The
result was that `-f foo -f bar` was equivalent to only passing
`-f bar`. Under the new behavior, `-f foo -f bar` combines `foo` and
`bar` as selection criteria, using OR-semantics. Envs matching
`foo OR bar` will be selected.

The existing multi-value argument behavior for `-f` is retained, in
which `-f foo bar` means `foo AND bar`. The behaviors can be combined
to express a variety of environment selections which were not
previously possible in a single invocation. e.g. `-f foo bar -f baz`
meaning `(foo AND bar) OR baz`.

No existing tests fail, and the new behavior is checked by a new test.
The help message for `-f` is updated.

* Allow factors to be passed hyphenated

The existing parsing of factors allows multiple factors to be selected
by passing them as multiple arguments to the `-f` flag. For example,
`-f foo bar` to pass both `foo` and `bar` as factors. This can now be
passed equivalently using `-f foo-bar`. The meaning of this
usage is identical to `-f foo bar`.

A new test checks the behavior, and very closely mirrors the existing
`-f` selection test so that their outputs are exactly equivalent.

* Make factor tests parametrized & apply pre-commit

These three tests are nearly identical in structure, and rely upon the
same project configuration. Convert from three distinct test cases to
a single parametrized test.

Also apply pre-commit, which does some mild reformatting.

* Add changelog entry for #2766

* Fix missing annotation in tests

* Fix changelog entry for #2766

* Improve env selection with factors: perf and types

- use tuple instead of list for immutable data
- use `continue` and `break` to skip unnecessary loop iterations

* Cleanup factor selection tests

- convert args from list[str] to tuple[str, ...]
- reformat str concat into a `.format()` usage

* Remove unreachable factor selection check

This check cannot be reached because it relies on an impossible
combination of factors and labels.
  • Loading branch information
sirosen committed Dec 29, 2022
1 parent 6f056ca commit 6cdd99c
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 9 deletions.
1 change: 1 addition & 0 deletions 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`.
33 changes: 27 additions & 6 deletions src/tox/session/env_select.py
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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:
"""
Expand Down
29 changes: 26 additions & 3 deletions tests/session/test_env_select.py
@@ -1,5 +1,7 @@
from __future__ import annotations

import pytest

from tox.pytest import MonkeyPatch, ToxProjectCreator


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

0 comments on commit 6cdd99c

Please sign in to comment.