Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support of tox v4 #81

Merged
merged 1 commit into from Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 4 additions & 7 deletions setup.cfg
Expand Up @@ -40,7 +40,7 @@ package_dir =
zip_safe = True
python_requires = >=3.6
install_requires =
tox >=3.12, <4
tox >= 4.0.0a8, <5
setup_requires =
setuptools_scm[toml] >=6, <7

Expand Down Expand Up @@ -76,7 +76,7 @@ envlist =
black
flake8
mypy
{py36,py37,py38,py39,pypy2,pypy3}-tox{312,315,latest}
{py36,py37,py38,py39,pypy2,pypy3}-toxlatest

[gh-actions]
python =
Expand All @@ -89,11 +89,8 @@ python =

[testenv]
description = run test suite under {basepython}
deps =
tox312: tox>=3.12,<3.13
tox315: tox>=3.15,<3.16
extras = testing
commands = pytest --cov=tox_gh_actions --cov-branch --cov-report=term --cov-report=xml {posargs}
commands = pytest --cov=tox_gh_actions --cov-branch --cov-report=term --cov-report=xml tests/ {posargs}

[testenv:black]
description = run black with check-only under {basepython}
Expand All @@ -107,7 +104,7 @@ extras = testing

[testenv:mypy]
description = run mypy under {basepython}
commands = flake8 src/ tests/ setup.py
commands = mypy src/ tests/
extras = testing

[flake8]
Expand Down
125 changes: 70 additions & 55 deletions src/tox_gh_actions/plugin.py
@@ -1,79 +1,79 @@
from itertools import product

from logging import getLogger
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.venv import VirtualEnv

from tox.config.loader.memory import MemoryLoader
from tox.config.loader.str_convert import StrConvert
from tox.config.main import Config
from tox.config.of_type import _PLACE_HOLDER
from tox.config.sets import ConfigSet
from tox.config.types import EnvList
from tox.plugin import impl

hookimpl = pluggy.HookimplMarker("tox")
logger = getLogger(__name__)


@hookimpl
@impl
def tox_configure(config: Config) -> None:
verbosity1("running tox-gh-actions")
logger.info("running tox-gh-actions")
if not is_running_on_actions():
verbosity1(
"tox-gh-actions won't override envlist "
"because tox is not running in GitHub Actions"
logger.warning(
"tox-gh-actions won't override envlist because tox is not running "
"in GitHub Actions"
)
return
elif is_env_specified(config):
verbosity1(
logger.warning(
"tox-gh-actions won't override envlist because "
"envlist is explicitly given via TOXENV or -e option"
)
return

verbosity2("original envconfigs: {}".format(list(config.envconfigs.keys())))
verbosity2("original envlist_default: {}".format(config.envlist_default))
verbosity2("original envlist: {}".format(config.envlist))
original_envlist: EnvList = config.core["envlist"]
# TODO We need to expire cache explicitly otherwise
# the overridden envlist won't be read at all
config.core._defined["envlist"]._cache = _PLACE_HOLDER # type: ignore
logger.debug("original envlist: %s", original_envlist.envs)

versions = get_python_version_keys()
verbosity2("Python versions: {}".format(versions))
logger.debug("Python versions: {}".format(versions))

gh_actions_config = parse_config(config._cfg.sections)
verbosity2("tox-gh-actions config: {}".format(gh_actions_config))
gh_actions_config = load_config(config)
logger.debug("tox-gh-actions config: %s", gh_actions_config)

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)
config.envlist_default = config.envlist = envlist
verbosity1("overriding envlist with: {}".format(envlist))


@hookimpl
def tox_runtest_pre(venv: VirtualEnv) -> None:
if is_running_on_actions():
envconfig: TestenvConfig = venv.envconfig
message = envconfig.envname
if envconfig.description:
message += " - " + envconfig.description
print("::group::tox: " + message)


@hookimpl
def tox_runtest_post(venv: VirtualEnv) -> None:
if is_running_on_actions():
print("::endgroup::")


def parse_config(config: Dict[str, Dict[str, str]]) -> Dict[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()
}
# Example of split_env:
# "py{27,38}" => ["py27", "py38"]
logger.debug("using the following factors to decide envlist: %s", factors)

envlist = get_envlist_from_factors(original_envlist.envs, factors)
config.core.loaders.insert(0, MemoryLoader(env_list=EnvList(envlist)))
logger.info("overriding envlist with: %s", envlist)


def load_config(config: Config) -> Dict[str, Dict[str, Any]]:
# It's better to utilize ConfigSet to parse gh-actions configuration but
# we use our custom configuration parser at this point for compatibility with
# the existing config files and limitations in ConfigSet API.
python_config = {}
for loader in config.get_section_config("gh-actions", ConfigSet).loaders:
if "python" not in loader.found_keys():
continue
python_config = parse_factors_dict(loader.load_raw("python", None, None))

env = {}
for loader in config.get_section_config("gh-actions:env", ConfigSet).loaders:
for env_variable in loader.found_keys():
if env_variable.upper() in env:
continue
env[env_variable.upper()] = parse_factors_dict(
loader.load_raw(env_variable, None, None)
)

# TODO Use more precise type
return {
"python": {k: split_env(v) for k, v in config_python.items()},
"env": config_env,
"python": python_config,
"env": env,
}


Expand All @@ -84,7 +84,7 @@ def get_factors(
factors: List[List[str]] = []
for version in versions:
if version in gh_actions_config["python"]:
verbosity2("got factors for Python version: {}".format(version))
logger.debug("got factors for Python version: %s", version)
factors.append(gh_actions_config["python"][version])
break # Shouldn't check remaining versions
for env, env_config in gh_actions_config.get("env", {}).items():
Expand Down Expand Up @@ -146,12 +146,27 @@ def is_env_specified(config: Config) -> bool:
if os.environ.get("TOXENV"):
# When TOXENV is a non-empty string
return True
elif config.option.env is not None:
elif hasattr(config.options, "env") and not config.options.env.use_default_list:
# When command line argument (-e) is given
return True
return False


def parse_factors_dict(value: str) -> Dict[str, List[str]]:
"""Parse a dict value from key to factors.

For example, this function converts an input
3.8: py38, docs
3.9: py39-django{2,3}
to a dict
{
"3.8": ["py38", "docs"],
"3.9": ["py39-django2", "py39-django3"],
}
"""
return {k: StrConvert.to_env_list(v).envs for k, v in parse_dict(value).items()}


# The following function was copied from
# https://github.com/tox-dev/tox-travis/blob/0.12/src/tox_travis/utils.py#L11-L32
# which is licensed under MIT LICENSE
Expand Down
16 changes: 0 additions & 16 deletions tests/integration/tox.ini

This file was deleted.

72 changes: 0 additions & 72 deletions tests/test_integration.py

This file was deleted.

85 changes: 0 additions & 85 deletions tests/test_plugin.py
@@ -1,75 +1,8 @@
import pytest
from tox.config import Config

from tox_gh_actions import plugin


@pytest.mark.parametrize(
"config,expected",
[
(
{
"gh-actions": {
"python": """3.7: py37
3.8: py38
3.9: py39, flake8"""
}
},
{
"python": {
"3.7": ["py37"],
"3.8": ["py38"],
"3.9": ["py39", "flake8"],
},
"env": {},
},
),
(
{
"gh-actions": {
"python": """3.7: py37
3.8: py38"""
},
"gh-actions:env": {
"PLATFORM": """ubuntu-latest: linux
macos-latest: macos
windows-latest: windows"""
},
},
{
"python": {
"3.7": ["py37"],
"3.8": ["py38"],
},
"env": {
"PLATFORM": {
"ubuntu-latest": ["linux"],
"macos-latest": ["macos"],
"windows-latest": ["windows"],
},
},
},
),
(
{"gh-actions": {}},
{
"python": {},
"env": {},
},
),
(
{},
{
"python": {},
"env": {},
},
),
],
)
def test_parse_config(config, expected):
assert plugin.parse_config(config) == expected


@pytest.mark.parametrize(
"config,version,environ,expected",
[
Expand Down Expand Up @@ -353,21 +286,3 @@ def test_get_version_keys_on_pyston(mocker):
def test_is_running_on_actions(mocker, environ, expected):
mocker.patch("tox_gh_actions.plugin.os.environ", environ)
assert plugin.is_running_on_actions() == expected


@pytest.mark.parametrize(
"option_env,environ,expected",
[
(None, {"TOXENV": "flake8"}, True),
(["py37,py38"], {}, True),
(["py37", "py38"], {}, True),
(["py37"], {"TOXENV": "flake8"}, True),
(None, {}, False),
],
)
def test_is_env_specified(mocker, option_env, environ, expected):
mocker.patch("tox_gh_actions.plugin.os.environ", environ)
option = mocker.MagicMock()
option.env = option_env
config = Config(None, option, None, mocker.MagicMock(), [])
assert plugin.is_env_specified(config) == expected