Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 21.8b0
rev: 21.9b0
hooks:
- id: black
args:
Expand Down
1 change: 1 addition & 0 deletions docs/changelog/2215.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow plugins to update the :ref:`set_env` and change the :ref:`pass_env` configurations -- by :user:`gaborbernat`.
6 changes: 4 additions & 2 deletions src/tox/config/set_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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",)
2 changes: 1 addition & 1 deletion src/tox/config/sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 13 additions & 5 deletions src/tox/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions src/tox/tox_env/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/config/test_set_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
46 changes: 46 additions & 0 deletions tests/plugin/test_plugin.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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