From 2bc8d549ab33fa83f7e87d3a4c8a7e220e4d04a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Thu, 16 Sep 2021 09:38:01 +0100 Subject: [PATCH] Allow plugins to change pass_env and set_env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor --- .pre-commit-config.yaml | 2 +- docs/changelog/2215.feature.rst | 1 + src/tox/config/set_env.py | 6 +++-- src/tox/config/sets.py | 2 +- src/tox/pytest.py | 18 +++++++++---- src/tox/tox_env/api.py | 10 ++++--- tests/config/test_set_env.py | 2 +- tests/plugin/test_plugin.py | 46 +++++++++++++++++++++++++++++++++ 8 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 docs/changelog/2215.feature.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 137ee8b34..cb501bb8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 21.8b0 + rev: 21.9b0 hooks: - id: black args: diff --git a/docs/changelog/2215.feature.rst b/docs/changelog/2215.feature.rst new file mode 100644 index 000000000..93d6e9742 --- /dev/null +++ b/docs/changelog/2215.feature.rst @@ -0,0 +1 @@ +Allow plugins to update the :ref:`set_env` and change the :ref:`pass_env` configurations -- by :user:`gaborbernat`. diff --git a/src/tox/config/set_env.py b/src/tox/config/set_env.py index 39faad233..33cfae454 100644 --- a/src/tox/config/set_env.py +++ b/src/tox/config/set_env.py @@ -28,6 +28,7 @@ def __init__(self, raw: str, name: str, env_name: Optional[str]) -> None: else: self._raw[key] = value self._materialized: Dict[str, str] = {} + self.changed = False @staticmethod def _extract_key_value(line: str) -> Tuple[str, str]: @@ -63,11 +64,12 @@ def __iter__(self) -> Iterator[str]: self._raw.update(sub_raw) yield from sub_raw.keys() - def update_if_not_present(self, param: Mapping[str, str]) -> None: + def update(self, param: Mapping[str, str], *, override: bool = True) -> None: for key, value in param.items(): # do not override something already set explicitly - if key not in self._raw and key not in self._materialized: + if override or (key not in self._raw and key not in self._materialized): self._materialized[key] = value + self.changed = True __all__ = ("SetEnv",) diff --git a/src/tox/config/sets.py b/src/tox/config/sets.py index 25f9e8dfb..d4aeedc5f 100644 --- a/src/tox/config/sets.py +++ b/src/tox/config/sets.py @@ -225,7 +225,7 @@ def __init__(self, conf: "Config", section: Section, env_name: str) -> None: def register_config(self) -> None: def set_env_post_process(values: SetEnv) -> SetEnv: - values.update_if_not_present(self.default_set_env_loader()) + values.update(self.default_set_env_loader(), override=False) return values def set_env_factory(raw: object) -> SetEnv: diff --git a/src/tox/pytest.py b/src/tox/pytest.py index 34ffe24b3..e16677a12 100644 --- a/src/tox/pytest.py +++ b/src/tox/pytest.py @@ -37,6 +37,7 @@ from tox.execute.api import Execute, ExecuteInstance, ExecuteOptions, ExecuteStatus, Outcome from tox.execute.request import ExecuteRequest, shell_cmd from tox.execute.stream import SyncWrite +from tox.plugin import manager from tox.report import LOGGER, OutErr from tox.run import run as tox_run from tox.run import setup_state as previous_setup_state @@ -74,12 +75,19 @@ def ensure_logging_framework_not_altered() -> Iterator[None]: # noqa: PT004 def _disable_root_tox_py(request: SubRequest, mocker: MockerFixture) -> Iterator[None]: """unless this is a plugin test do not allow loading toxfile.py""" if request.node.get_closest_marker("plugin_test"): # unregister inline plugin - from tox.plugin import manager + module, load_inline = None, manager._load_inline - inline_plugin = mocker.spy(manager, "_load_inline") + def _load_inline(path: Path) -> Optional[ModuleType]: # register only on first run, and unregister at end + nonlocal module + if module is None: + module = load_inline(path) + return module + return None + + mocker.patch.object(manager, "_load_inline", _load_inline) yield - if inline_plugin.spy_return is not None: # pragma: no branch - manager.MANAGER.manager.unregister(inline_plugin.spy_return) + if module is not None: # pragma: no branch + manager.MANAGER.manager.unregister(module) else: # do not allow loading inline plugins mocker.patch("tox.plugin.inline._load_plugin", return_value=None) yield @@ -605,7 +613,7 @@ def enable_pip_pypi_access_fixture( return previous_url -def register_inline_plugin(mocker: MockerFixture, *args: Callable[..., Any]) -> None: # +def register_inline_plugin(mocker: MockerFixture, *args: Callable[..., Any]) -> None: frame_info = inspect.stack()[1] caller_module = inspect.getmodule(frame_info[0]) assert caller_module is not None diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py index e06e16d90..aed536e5d 100644 --- a/src/tox/tox_env/api.py +++ b/src/tox/tox_env/api.py @@ -59,6 +59,7 @@ def __init__(self, create_args: ToxEnvCreateArgs) -> None: self._paths_private: List[Path] = [] #: a property holding the PATH environment variables self._hidden_outcomes: Optional[List[Outcome]] = [] self._env_vars: Optional[Dict[str, str]] = None + self._env_vars_pass_env: List[str] = [] self._suspended_out_err: Optional[OutErr] = None self._execute_statuses: Dict[int, ExecuteStatus] = {} self._interrupted = False @@ -284,13 +285,14 @@ def _clean(self, transitive: bool = False) -> None: # noqa: U100 @property def _environment_variables(self) -> Dict[str, str]: - if self._env_vars is not None: - return self._env_vars pass_env: List[str] = self.conf["pass_env"] - result = self._load_pass_env(pass_env) set_env: SetEnv = self.conf["set_env"] + if self._env_vars_pass_env == pass_env and not set_env.changed and self._env_vars is not None: + return self._env_vars + + result = self._load_pass_env(pass_env) # load/paths_env might trigger a load of the environment variables, set result here, returns current state - self._env_vars = result + self._env_vars, self._env_vars_pass_env, set_env.changed = result, pass_env, False # set PATH here in case setting and environment variable requires access to the environment variable PATH result["PATH"] = self._make_path() for key in set_env: diff --git a/tests/config/test_set_env.py b/tests/config/test_set_env.py index ad9d36705..0694a71af 100644 --- a/tests/config/test_set_env.py +++ b/tests/config/test_set_env.py @@ -9,7 +9,7 @@ def test_set_env_explicit() -> None: set_env = SetEnv("\nA=1\nB = 2\nC= 3\nD= 4", "py", "py") - set_env.update_if_not_present({"E": "5 ", "F": "6"}) + set_env.update({"E": "5 ", "F": "6"}, override=False) keys = list(set_env) assert keys == ["E", "F", "A", "B", "C", "D"] diff --git a/tests/plugin/test_plugin.py b/tests/plugin/test_plugin.py index 90de3c239..bca241708 100644 --- a/tests/plugin/test_plugin.py +++ b/tests/plugin/test_plugin.py @@ -1,6 +1,8 @@ import logging +import os import sys from typing import List +from unittest.mock import patch import pytest from pytest_mock import MockerFixture @@ -134,3 +136,47 @@ def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: result = project.run() result.assert_failed() assert "raise TypeError(raw)" in result.out + + +def test_plugin_extend_pass_env(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: + @impl + def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: + env_conf["pass_env"].append("MAGIC_*") + + register_inline_plugin(mocker, tox_add_env_config) + ini = """ + [testenv] + package=skip + commands=python -c 'import os; print(os.environ["MAGIC_1"]); print(os.environ["MAGIC_2"])' + """ + project = tox_project({"tox.ini": ini}) + with patch.dict(os.environ, {"MAGIC_1": "magic_1", "MAGIC_2": "magic_2"}): + result = project.run("r") + result.assert_success() + assert "magic_1" in result.out + assert "magic_2" in result.out + + result_conf = project.run("c", "-e", "py", "-k", "pass_env") + result_conf.assert_success() + assert "MAGIC_*" in result_conf.out + + +def test_plugin_extend_set_env(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None: + @impl + def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None: + env_conf["set_env"].update({"MAGI_CAL": "magi_cal"}) + + register_inline_plugin(mocker, tox_add_env_config) + ini = """ + [testenv] + package=skip + commands=python -c 'import os; print(os.environ["MAGI_CAL"])' + """ + project = tox_project({"tox.ini": ini}) + result = project.run("r") + result.assert_success() + assert "magi_cal" in result.out + + result_conf = project.run("c", "-e", "py", "-k", "set_env") + result_conf.assert_success() + assert "MAGI_CAL=magi_cal" in result_conf.out