Skip to content

Commit

Permalink
Allow more lax env selection from computed factors
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
terencehonles committed Apr 9, 2021
1 parent 1c2ca56 commit 2cea5ac
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 61 deletions.
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ python =
3.9: py39
pypy-2: pypy2
pypy-3: pypy3
envs_are_optional = true

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

[testenv:black]
description = run black with check-only under {basepython}
commands = black --check src/ tests/ setup.py
commands = black --check --diff src/ tests/ setup.py
extras = testing

[testenv:flake8]
Expand Down
93 changes: 70 additions & 23 deletions src/tox_gh_actions/plugin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from itertools import product
from itertools import combinations, product
import os
import sys
from typing import Any, Dict, Iterable, List

import pluggy
from tox.config import Config, TestenvConfig, _split_env as split_env
from tox.reporter import verbosity1, verbosity2
from tox.reporter import verbosity1, verbosity2, warning
from tox.venv import VirtualEnv


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

if gh_actions_config["envs_are_optional"] is None:
warning(
"Config 'gh-actions.envs_are_optional' will become the default in a "
"future release. Set explicitly to 'true' or 'false' to disable this "
"warning."
)

factors = get_factors(gh_actions_config, versions)
verbosity2("using the following factors to decide envlist: {}".format(factors))

envlist = get_envlist_from_factors(config.envlist, factors)
envlist = get_envlist_from_factors(
config.envlist,
factors,
envs_are_optional=gh_actions_config["envs_are_optional"],
)
config.envlist_default = config.envlist = envlist
verbosity1("overriding envlist with: {}".format(envlist))

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


def parse_env_config(value):
# type: (str) -> Dict[str, Dict[str, List[str]]]
return {k: split_env(v) for k, v in parse_dict(value).items()}


def parse_config(config):
# type: (Dict[str, Dict[str, str]]) -> Dict[str, Dict[str, Any]]
# type: (Dict[str, Dict[str, str]]) -> Dict[str, Any]
"""Parse gh-actions section in tox.ini"""
config_python = parse_dict(config.get("gh-actions", {}).get("python", ""))
config_env = {
name: {k: split_env(v) for k, v in parse_dict(conf).items()}
for name, conf in config.get("gh-actions:env", {}).items()
}
action_config = config.get("gh-actions", {})
envs_are_optional = action_config.get("envs_are_optional")
# Example of split_env:
# "py{27,38}" => ["py27", "py38"]
return {
"python": {k: split_env(v) for k, v in config_python.items()},
"env": config_env,
"python": parse_env_config(action_config.get("python", "")),
"envs_are_optional": (
None if envs_are_optional is None else envs_are_optional.lower() == "true"
),
"env": {
name: parse_env_config(conf)
for name, conf in config.get("gh-actions:env", {}).items()
},
}


def get_factors(gh_actions_config, versions):
# type: (Dict[str, Dict[str, Any]], Iterable[str]) -> List[str]
# type: (Dict[str, Any], Iterable[str]) -> List[List[str]]
"""Get a list of factors"""
factors = [] # type: List[List[str]]
for version in versions:
Expand All @@ -95,20 +114,48 @@ def get_factors(gh_actions_config, versions):
env_value = os.environ[env]
if env_value in env_config:
factors.append(env_config[env_value])
return [x for x in map(lambda f: "-".join(f), product(*factors)) if x]
return factors


def get_envlist_from_factors(envlist, factors):
# type: (Iterable[str], Iterable[str]) -> List[str]
def get_envlist_from_factors(envlist, grouped_factors, envs_are_optional=False):
# type: (Iterable[str], Iterable[List[List[str]]], bool) -> List[str]
"""Filter envlist using factors"""
result = []
for env in envlist:
for factor in factors:
env_facts = env.split("-")
if all(f in env_facts for f in factor.split("-")):
result.append(env)
break
return result
if not grouped_factors:
return []

result = set()
all_env_factors = [(set(e.split("-")), e) for e in envlist]

if not envs_are_optional:
for env_factors, env in all_env_factors:
for factors in product(*grouped_factors):
if env_factors.issuperset(factors):
result.add(env)
else:
# The first factors come from the python config and are required
for required_factor in grouped_factors[0]:
env_factors = [(f, e) for f, e in all_env_factors if required_factor in f]

# The remaining factors come from the env and will be tried exactly at
# first, and then will be tried again after a single factor is removed
# until there is only 1 factor left. All matches after removing N factors
# are added to the result set.
matches = set()
for optional_factors in product(*grouped_factors[1:]):
for count in range(len(optional_factors), 0, -1):
for factors in combinations(optional_factors, count):
factors = set(factors)
matches.update(e for f, e in env_factors if f >= factors)

if matches:
result |= matches
break

# if none of the optional factors matched add all required matches
if not matches:
result.update(e for f, e in env_factors)

return [i for i in envlist if i in result]


def get_python_version_keys():
Expand Down
Loading

0 comments on commit 2cea5ac

Please sign in to comment.