Skip to content

Commit 2cea5ac

Browse files
committed
Allow more lax env selection from computed factors
The previous env selection was rather strict when there were more than 2 factors supplied and this change allows users to relax it with the section value "envs_are_optional" which will become the default in a future release. Given the following factors: [{"py38", "lint"}, {"reqV1", "reqV2"}, {"opReqV1", "opReqV2"}] The existing env selection would only match: - py38-reqV1-opReqV1 - py38-reqV2-opReqV1 - py38-reqV1-opReqV2 - py38-reqV2-opReqV2 ... - lint-reqV1-opReqV1 ... It would fail to match the single factor "lint". Although, this may be correct for required factors, but for something like "lint" it may not need the additional factors that are required with the previous env selection. This change allows selecting the following envs: - py38 - py38-reqV1 - py38-reqV2 - py38-opReqV1 - py38-opReqV2 - py38-reqV1-opReqV1 - py38-reqV2-opReqV1 - py38-reqV2-opReqV2 - lint - ... In addition, this change makes the order of the factors no longer important: - py38-opReqV1-reqV1 - reqV1-opReqV1-py38 All of these permutations are bound by the envlist that the user defines in their tox configuration so it is up to the user to keep their configuration organized and not go crazy with their factor ordering.
1 parent 1c2ca56 commit 2cea5ac

File tree

3 files changed

+170
-61
lines changed

3 files changed

+170
-61
lines changed

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ python =
8989
3.9: py39
9090
pypy-2: pypy2
9191
pypy-3: pypy3
92+
envs_are_optional = true
9293

9394
[testenv]
9495
description = run test suite under {basepython}
@@ -100,7 +101,7 @@ commands = pytest --cov=tox_gh_actions --cov-branch --cov-report=term --cov-repo
100101

101102
[testenv:black]
102103
description = run black with check-only under {basepython}
103-
commands = black --check src/ tests/ setup.py
104+
commands = black --check --diff src/ tests/ setup.py
104105
extras = testing
105106

106107
[testenv:flake8]

src/tox_gh_actions/plugin.py

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
from itertools import product
1+
from itertools import combinations, product
22
import os
33
import sys
44
from typing import Any, Dict, Iterable, List
55

66
import pluggy
77
from tox.config import Config, TestenvConfig, _split_env as split_env
8-
from tox.reporter import verbosity1, verbosity2
8+
from tox.reporter import verbosity1, verbosity2, warning
99
from tox.venv import VirtualEnv
1010

1111

@@ -39,10 +39,21 @@ def tox_configure(config):
3939
gh_actions_config = parse_config(config._cfg.sections)
4040
verbosity2("tox-gh-actions config: {}".format(gh_actions_config))
4141

42+
if gh_actions_config["envs_are_optional"] is None:
43+
warning(
44+
"Config 'gh-actions.envs_are_optional' will become the default in a "
45+
"future release. Set explicitly to 'true' or 'false' to disable this "
46+
"warning."
47+
)
48+
4249
factors = get_factors(gh_actions_config, versions)
4350
verbosity2("using the following factors to decide envlist: {}".format(factors))
4451

45-
envlist = get_envlist_from_factors(config.envlist, factors)
52+
envlist = get_envlist_from_factors(
53+
config.envlist,
54+
factors,
55+
envs_are_optional=gh_actions_config["envs_are_optional"],
56+
)
4657
config.envlist_default = config.envlist = envlist
4758
verbosity1("overriding envlist with: {}".format(envlist))
4859

@@ -65,24 +76,32 @@ def tox_runtest_post(venv):
6576
print("::endgroup::")
6677

6778

79+
def parse_env_config(value):
80+
# type: (str) -> Dict[str, Dict[str, List[str]]]
81+
return {k: split_env(v) for k, v in parse_dict(value).items()}
82+
83+
6884
def parse_config(config):
69-
# type: (Dict[str, Dict[str, str]]) -> Dict[str, Dict[str, Any]]
85+
# type: (Dict[str, Dict[str, str]]) -> Dict[str, Any]
7086
"""Parse gh-actions section in tox.ini"""
71-
config_python = parse_dict(config.get("gh-actions", {}).get("python", ""))
72-
config_env = {
73-
name: {k: split_env(v) for k, v in parse_dict(conf).items()}
74-
for name, conf in config.get("gh-actions:env", {}).items()
75-
}
87+
action_config = config.get("gh-actions", {})
88+
envs_are_optional = action_config.get("envs_are_optional")
7689
# Example of split_env:
7790
# "py{27,38}" => ["py27", "py38"]
7891
return {
79-
"python": {k: split_env(v) for k, v in config_python.items()},
80-
"env": config_env,
92+
"python": parse_env_config(action_config.get("python", "")),
93+
"envs_are_optional": (
94+
None if envs_are_optional is None else envs_are_optional.lower() == "true"
95+
),
96+
"env": {
97+
name: parse_env_config(conf)
98+
for name, conf in config.get("gh-actions:env", {}).items()
99+
},
81100
}
82101

83102

84103
def get_factors(gh_actions_config, versions):
85-
# type: (Dict[str, Dict[str, Any]], Iterable[str]) -> List[str]
104+
# type: (Dict[str, Any], Iterable[str]) -> List[List[str]]
86105
"""Get a list of factors"""
87106
factors = [] # type: List[List[str]]
88107
for version in versions:
@@ -95,20 +114,48 @@ def get_factors(gh_actions_config, versions):
95114
env_value = os.environ[env]
96115
if env_value in env_config:
97116
factors.append(env_config[env_value])
98-
return [x for x in map(lambda f: "-".join(f), product(*factors)) if x]
117+
return factors
99118

100119

101-
def get_envlist_from_factors(envlist, factors):
102-
# type: (Iterable[str], Iterable[str]) -> List[str]
120+
def get_envlist_from_factors(envlist, grouped_factors, envs_are_optional=False):
121+
# type: (Iterable[str], Iterable[List[List[str]]], bool) -> List[str]
103122
"""Filter envlist using factors"""
104-
result = []
105-
for env in envlist:
106-
for factor in factors:
107-
env_facts = env.split("-")
108-
if all(f in env_facts for f in factor.split("-")):
109-
result.append(env)
110-
break
111-
return result
123+
if not grouped_factors:
124+
return []
125+
126+
result = set()
127+
all_env_factors = [(set(e.split("-")), e) for e in envlist]
128+
129+
if not envs_are_optional:
130+
for env_factors, env in all_env_factors:
131+
for factors in product(*grouped_factors):
132+
if env_factors.issuperset(factors):
133+
result.add(env)
134+
else:
135+
# The first factors come from the python config and are required
136+
for required_factor in grouped_factors[0]:
137+
env_factors = [(f, e) for f, e in all_env_factors if required_factor in f]
138+
139+
# The remaining factors come from the env and will be tried exactly at
140+
# first, and then will be tried again after a single factor is removed
141+
# until there is only 1 factor left. All matches after removing N factors
142+
# are added to the result set.
143+
matches = set()
144+
for optional_factors in product(*grouped_factors[1:]):
145+
for count in range(len(optional_factors), 0, -1):
146+
for factors in combinations(optional_factors, count):
147+
factors = set(factors)
148+
matches.update(e for f, e in env_factors if f >= factors)
149+
150+
if matches:
151+
result |= matches
152+
break
153+
154+
# if none of the optional factors matched add all required matches
155+
if not matches:
156+
result.update(e for f, e in env_factors)
157+
158+
return [i for i in envlist if i in result]
112159

113160

114161
def get_python_version_keys():

0 commit comments

Comments
 (0)