Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."
)
Comment on lines +42 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ymyzk this can be removed if the default won't be changed, and this option probably needs to be added to the documentation.


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