diff --git a/README.md b/README.md index 9df477c..b6f7827 100644 --- a/README.md +++ b/README.md @@ -140,3 +140,71 @@ python = [testenv] ... ``` + +You can also use environment variable to decide which environment to run. +The following is an example to install different dependency based on platform. +It will create 12 jobs when running the workflow on GitHub Actions. +- On Python 2.7/ubuntu-latest job, tox runs `py27-linux` environment +- On Python 3.5/ubuntu-latest job, tox runs `py35-linux` environment +- and so on. + +`.github/workflows/.yml`: +```yaml +name: Python package + +on: [push] + +jobs: + build: + runs-on: ${{ matrix.platform }} + strategy: + max-parallel: 4 + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + python-version: [2.7, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + env: + PLATFORM: ${{ matrix.platform }} +``` + +`tox.ini`: +```ini +[tox] +envlist = py{27,36,37,38}-{linux,macos,windows} + +[gh-actions] +python = + 2.7: py27 + 3.8: py38, mypy + pypy2: pypy2 + pypy3: pypy3 + +[gh-actions:env] +PLATFORM = + ubuntu-latest: linux + macos-latest: macos + windows-latest: windows + +[testenv] +deps = + + linux: + macos: + windows: +... +``` + +See [tox's documentation about factor-conditional settings](https://tox.readthedocs.io/en/latest/config.html#factors-and-factor-conditional-settings) as well. + diff --git a/src/tox_gh_actions/plugin.py b/src/tox_gh_actions/plugin.py index 573f30d..ffe2711 100644 --- a/src/tox_gh_actions/plugin.py +++ b/src/tox_gh_actions/plugin.py @@ -1,6 +1,7 @@ +from itertools import product import os import sys -from typing import Dict, Iterable, List +from typing import Any, Dict, Iterable, List import pluggy from tox.config import Config, _split_env as split_env @@ -20,7 +21,7 @@ def tox_configure(config): version = get_python_version() verbosity2("Python version: {}".format(version)) - gh_actions_config = parse_config(config._cfg.sections.get("gh-actions", {})) + gh_actions_config = parse_config(config._cfg.sections) verbosity2("tox-gh-actions config: {}".format(gh_actions_config)) factors = get_factors(gh_actions_config, version) @@ -35,20 +36,33 @@ def tox_configure(config): def parse_config(config): - # type: (Dict[str, str]) -> Dict[str, Dict[str, List[str]]] + # type: (Dict[str, Dict[str, str]]) -> Dict[str, Dict[str, Any]] """Parse gh-actions section in tox.ini""" - config_python = parse_dict(config.get("python", "")) + 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"] return { - "python": {k: split_env(v) for k, v in config_python.items()} + "python": {k: split_env(v) for k, v in config_python.items()}, + "env": config_env, } def get_factors(gh_actions_config, version): - # type: (Dict[str, Dict[str, List[str]]], str) -> List[str] + # type: (Dict[str, Dict[str, Any]], str) -> List[str] """Get a list of factors""" - return gh_actions_config["python"].get(version, []) + factors = [] # type: List[List[str]] + if version in gh_actions_config["python"]: + factors.append(gh_actions_config["python"][version]) + for env, env_config in gh_actions_config.get("env", {}).items(): + if env in os.environ: + 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] def get_envlist_from_factors(envlist, factors): diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 18fd0e6..e5250c8 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -6,10 +6,12 @@ @pytest.mark.parametrize("config,expected", [ ( { - "python": """2.7: py27 + "gh-actions": { + "python": """2.7: py27 3.5: py35 3.6: py36 3.7: py37, flake8""" + } }, { "python": { @@ -18,18 +20,55 @@ "3.6": ["py36"], "3.7": ["py37", "flake8"], }, + "env": {}, + }, + ), + ( + { + "gh-actions": { + "python": """2.7: py27 +3.8: py38""" + }, + "gh-actions:env": { + "PLATFORM": """ubuntu-latest: linux +macos-latest: macos +windows-latest: windows""" + } + }, + { + "python": { + "2.7": ["py27"], + "3.8": ["py38"], + }, + "env": { + "PLATFORM": { + "ubuntu-latest": ["linux"], + "macos-latest": ["macos"], + "windows-latest": ["windows"], + }, + }, + }, + ), + ( + {"gh-actions": {}}, + { + "python": {}, + "env": {}, }, ), ( {}, - {"python": {}}, + { + "python": {}, + "env": {}, + }, ), ]) def test_parse_config(config, expected): assert plugin.parse_config(config) == expected -@pytest.mark.parametrize("config,factors,expected", [ +@pytest.mark.parametrize("config,version,environ,expected", [ ( { "python": { @@ -39,15 +78,128 @@ def test_parse_config(config, expected): "unknown": {}, }, "2.7", + {}, ["py27", "flake8"], ), ( { "python": { + "2.7": ["py27", "flake8"], "3.8": ["py38", "flake8"], }, + "env": { + "SAMPLE": { + "VALUE1": ["fact1", "fact2"], + "VALUE2": ["fact3", "fact4"], + }, + }, }, "2.7", + { + "SAMPLE": "VALUE1", + "HOGE": "VALUE3", + }, + ["py27-fact1", "py27-fact2", "flake8-fact1", "flake8-fact2"], + ), + ( + { + "python": { + "2.7": ["py27", "flake8"], + "3.8": ["py38", "flake8"], + }, + "env": { + "SAMPLE": { + "VALUE1": ["fact1", "fact2"], + "VALUE2": ["fact3", "fact4"], + }, + "HOGE": { + "VALUE3": ["fact5", "fact6"], + "VALUE4": ["fact7", "fact8"], + }, + }, + }, + "2.7", + { + "SAMPLE": "VALUE1", + "HOGE": "VALUE3", + }, + [ + "py27-fact1-fact5", "py27-fact1-fact6", + "py27-fact2-fact5", "py27-fact2-fact6", + "flake8-fact1-fact5", "flake8-fact1-fact6", + "flake8-fact2-fact5", "flake8-fact2-fact6", + ], + ), + ( + { + "python": { + "2.7": ["py27", "flake8"], + "3.8": ["py38", "flake8"], + }, + "env": { + "SAMPLE": { + "VALUE1": ["django18", "flake8"], + "VALUE2": ["django18"], + }, + }, + }, + "2.7", + { + "SAMPLE": "VALUE1", + "HOGE": "VALUE3", + }, + [ + "py27-django18", "py27-flake8", + "flake8-django18", "flake8-flake8", + ], + ), + ( + { + "python": { + "2.7": ["py27", "flake8"], + "3.8": ["py38", "flake8"], + }, + "env": { + "SAMPLE": { + "VALUE1": ["fact1", "fact2"], + "VALUE2": ["fact3", "fact4"], + }, + }, + "unknown": {}, + }, + "2.7", + { + "SAMPLE": "VALUE3", + }, + ["py27", "flake8"], + ), + ( + { + "python": { + "2.7": ["py27", "flake8"], + "3.8": ["py38", "flake8"], + }, + "env": { + "SAMPLE": { + "VALUE1": [], + }, + }, + "unknown": {}, + }, + "3.8", + { + "SAMPLE": "VALUE2", + }, + ["py38", "flake8"], + ), + ( + { + "python": { + "3.8": ["py38", "flake8"], + }, + }, + "2.7", + {}, [], ), ( @@ -55,11 +207,22 @@ def test_parse_config(config, expected): "python": {}, }, "3.8", + {}, [], ), ]) -def test_get_factors(config, factors, expected): - assert plugin.get_factors(config, factors) == expected +def test_get_factors(mocker, config, version, environ, expected): + mocker.patch("tox_gh_actions.plugin.os.environ", environ) + result = normalize_factors_list(plugin.get_factors(config, version)) + expected = normalize_factors_list(expected) + assert result == expected + + +def normalize_factors_list(factors): + """Utility to make it compare equality of a list of factors""" + result = [tuple(sorted(f.split("-"))) for f in factors] + result.sort() + return result @pytest.mark.parametrize("envlist,factors,expected", [ @@ -83,6 +246,14 @@ def test_get_factors(config, factors, expected): ['py37', 'flake8'], ['py37-dj111', 'py37-dj20', 'flake8'], ), + ( + ['py27-django18', 'py37-django18', 'flake8'], + [ + 'py27-django18', 'py27-flake8', + 'flake8-django18', 'flake8-flake8', + ], + ['py27-django18', 'flake8'], + ) ]) def test_get_envlist_from_factors(envlist, factors, expected): assert plugin.get_envlist_from_factors(envlist, factors) == expected