Skip to content

Commit

Permalink
Add label filtering
Browse files Browse the repository at this point in the history
Signed-off-by: Bernát Gábor <[email protected]>
  • Loading branch information
gaborbernat committed Jan 3, 2022
1 parent b86729e commit d6966d4
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 42 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v4.1.0
hooks:
- id: check-builtin-literals
- id: check-docstring-first
Expand All @@ -17,7 +17,7 @@ repos:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v2.29.1
rev: v2.30.1
hooks:
- id: pyupgrade
args: ["--py37-plus"]
Expand All @@ -43,7 +43,7 @@ repos:
- id: prettier
additional_dependencies:
- [email protected]
- "@prettier/plugin-xml@1.1.0"
- "@prettier/plugin-xml@1.2.0"
args: ["--print-width=120", "--prose-wrap=always"]
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.0
Expand All @@ -55,7 +55,7 @@ repos:
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: "0.5.1"
rev: "0.5.2"
hooks:
- id: tox-ini-fmt
args: ["-p", "fix"]
Expand Down
29 changes: 14 additions & 15 deletions src/tox/session/cmd/list_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""
from __future__ import annotations

from itertools import chain

from tox.config.cli.parser import ToxParser
from tox.plugin import impl
from tox.session.env_select import register_env_select_flags
Expand All @@ -12,25 +14,22 @@
@impl
def tox_add_option(parser: ToxParser) -> None:
our = parser.add_command("list", ["l"], "list environments", list_env)
our.add_argument("-d", action="store_true", help="list just default envs", dest="list_default_only")
our.add_argument("--no-desc", action="store_true", help="do not show description", dest="list_no_description")
register_env_select_flags(our, default=None, group_only=True)
d = register_env_select_flags(our, default=None, group_only=True)
d.add_argument("-d", action="store_true", help="list just default envs", dest="list_default_only")


def list_env(state: State) -> int:
option = state.conf.options
default = list(state.envs.iter())
has_group_select = bool(option.factors or option.labels)
active_only = has_group_select or option.list_default_only

extra = []
if not option.list_default_only:
default_entries = set(default)
for env in state.envs.iter(only_active=False):
if env not in default_entries:
extra.append(env)
active = dict.fromkeys(state.envs.iter())
inactive = {} if active_only else {env: None for env in state.envs.iter(only_active=False) if env not in active}

if not option.list_no_description and default:
if not has_group_select and not option.list_no_description and active:
print("default environments:")
max_length = max((len(env) for env in (default + extra)), default=0)
max_length = max((len(env) for env in chain(active, inactive)), default=0)

def report_env(name: str) -> None:
if not option.list_no_description:
Expand All @@ -44,14 +43,14 @@ def report_env(name: str) -> None:
msg = env
print(msg)

for env in default:
for env in active:
report_env(env)

if not option.list_default_only and extra:
if not has_group_select and not option.list_default_only and inactive:
if not option.list_no_description:
if default: # pragma: no branch
if active: # pragma: no branch
print("")
print("additional environments:")
for env in extra:
for env in inactive:
report_env(env)
return 0
70 changes: 54 additions & 16 deletions src/tox/session/env_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
from collections import Counter
from dataclasses import dataclass
from itertools import chain
from typing import TYPE_CHECKING, Any, Iterable, Iterator, List, cast
from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, cast

from tox.config.loader.str_convert import StrConvert
from tox.tox_env.api import ToxEnvCreateArgs
from tox.tox_env.register import REGISTER
from tox.tox_env.runner import RunToxEnv

from ..config.loader.memory import MemoryLoader
from ..config.types import EnvList
from ..report import HandledError
from ..tox_env.errors import Skip
from ..tox_env.package import PackageToxEnv
Expand Down Expand Up @@ -58,18 +59,33 @@ def register_env_select_flags(
default: CliEnv | None,
multiple: bool = True,
group_only: bool = False,
) -> None:
if not multiple:
parser.add_argument("-e", dest="env", help="environment to run", default=default, type=CliEnv)
return

if not group_only:
) -> ArgumentParser:
"""
Register environment selection flags.
:param parser: the parser to register to
:param default: the default value for env selection
:param multiple: allow selecting multiple environments
:param group_only:
:return:
"""
if multiple:
group = parser.add_argument_group("select target environment(s)")
excl = group.add_mutually_exclusive_group(required=False)
help_msg = "enumerate (ALL -> all environments, not set -> use <env_list> from config)"
excl.add_argument("-e", dest="env", help=help_msg, default=default, type=CliEnv)
# excl.add_argument("-m", dest="labels", metavar="label", help="labels to run", default=[], type=str, nargs="+")
# excl.add_argument("-f", dest="factors", help="factors to run", default=[], type=str, nargs="+")
add_to: ArgumentParser = group.add_mutually_exclusive_group(required=False) # type: ignore
else:
add_to = parser
if not group_only:
if multiple:
help_msg = "enumerate (ALL -> all environments, not set -> use <env_list> from config)"
else:
help_msg = "environment to run"
add_to.add_argument("-e", dest="env", help=help_msg, default=default, type=CliEnv)
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="+")
return add_to


@dataclass
Expand Down Expand Up @@ -98,6 +114,8 @@ def __init__(self, state: State) -> None:
self._journal = self._state._journal
self._provision: None | tuple[bool, str, MemoryLoader] = None

self._state.conf.core.add_config("labels", Dict[str, EnvList], {}, "core labels")

def _collect_names(self) -> Iterator[tuple[Iterable[str], bool]]:
""":return: sources of tox environments defined with name and if is marked as target to run"""
if self._provision is not None: # pragma: no branch
Expand All @@ -110,16 +128,21 @@ def _collect_names(self) -> Iterator[tuple[Iterable[str], bool]]:
else:
yield self._cli_envs, True
yield self._state.conf, everything_active
label_envs = dict.fromkeys(chain.from_iterable(self._state.conf.core["labels"].values()))
if label_envs:
yield label_envs.keys(), False

def _env_name_to_active(self) -> dict[str, bool]:
env_name_to_active_map = {}
for a_collection, is_active in self._collect_names():
for name in a_collection:
if name not in env_name_to_active_map:
env_name_to_active_map[name] = is_active
# if no active environment is defined fallback to py
if self.on_empty_fallback_py and not any(env_name_to_active_map.values()):
env_name_to_active_map["py"] = True
# for factor/label selection update the active flag
if not (self._state.conf.options.labels or self._state.conf.options.factors):
# if no active environment is defined fallback to py
if self.on_empty_fallback_py and not any(env_name_to_active_map.values()):
env_name_to_active_map["py"] = True
return env_name_to_active_map

@property
Expand Down Expand Up @@ -183,9 +206,24 @@ def _defined_envs(self) -> dict[str, _ToxEnvInfo]:
# reorder to as defined rather as found
order = chain(env_name_to_active, (i for i in self._defined_envs_ if i not in env_name_to_active))
self._defined_envs_ = {name: self._defined_envs_[name] for name in order if name in self._defined_envs_}

self._mark_active()
return self._defined_envs_

def _mark_active(self):
labels, factors = set(self._state.conf.options.labels), set(self._state.conf.options.factors)
if labels or factors:
for env_info in self._defined_envs_.values():
env_info.is_active = False # if any was selected reset
if labels:
for label in labels:
for env_name in self._state.conf.core["labels"].get(label, []):
self._defined_envs_[env_name].is_active = True
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
raise NotImplementedError

def _build_run_env(self, name: str) -> RunToxEnv | None:
if self._provision is not None and self._provision[0] is False and name == self._provision[1]:
return None
Expand Down
8 changes: 7 additions & 1 deletion src/tox/tox_env/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Sequence, cast
from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Sequence, Set, cast

from tox.config.main import Config
from tox.config.set_env import SetEnv
Expand Down Expand Up @@ -97,6 +97,12 @@ def register_config(self) -> None:
desc="the name of the tox environment",
value=self.conf.name,
)
self.conf.add_config(
keys=["labels"],
of_type=Set[str],
default=set(),
desc="labels attached to the tox environment",
)
self.conf.add_config(
keys=["env_dir", "envdir"],
of_type=Path,
Expand Down
6 changes: 0 additions & 6 deletions src/tox/tox_env/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ def ensure_one_line(value: str) -> str:
desc="description attached to the tox environment",
post_process=ensure_one_line,
)
self.conf.add_config(
keys=["labels"],
of_type=List[str],
default=[],
desc="labels attached to the tox environment",
)
self.conf.add_config(
"depends",
of_type=EnvList,
Expand Down
61 changes: 61 additions & 0 deletions tests/session/test_env_select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

from tox.pytest import ToxProjectCreator


def test_label_core_can_define(tox_project: ToxProjectCreator) -> None:
ini = """
[tox]
labels =
test = py3{10,9}
static = flake8, type
"""
project = tox_project({"tox.ini": ini})
outcome = project.run("l", "--no-desc")
outcome.assert_success()
outcome.assert_out_err("py\npy310\npy39\nflake8\ntype\n", "")


def test_label_core_select(tox_project: ToxProjectCreator) -> None:
ini = """
[tox]
labels =
test = py3{10,9}
static = flake8, type
"""
project = tox_project({"tox.ini": ini})
outcome = project.run("l", "--no-desc", "-m", "test")
outcome.assert_success()
outcome.assert_out_err("py310\npy39\n", "")


def test_label_select_trait(tox_project: ToxProjectCreator) -> None:
ini = """
[tox]
env_list = py310, py39, flake8, type
[testenv]
labels = test
[testenv:flake8]
labels = static
[testenv:type]
labels = static
"""
project = tox_project({"tox.ini": ini})
outcome = project.run("l", "--no-desc", "-m", "test")
outcome.assert_success()
outcome.assert_out_err("py310\npy39\n", "")


def test_label_core_and_trait(tox_project: ToxProjectCreator) -> None:
ini = """
[tox]
env_list = py310, py39, flake8, type
labels =
static = flake8, type
[testenv]
labels = test
"""
project = tox_project({"tox.ini": ini})
outcome = project.run("l", "--no-desc", "-m", "test", "static")
outcome.assert_success()
outcome.assert_out_err("py310\npy39\nflake8\ntype\n", "")

0 comments on commit d6966d4

Please sign in to comment.