Skip to content

Commit 95ffbb3

Browse files
authored
Allow plugins to change pass_env and set_env (#2218)
1 parent 27733ab commit 95ffbb3

File tree

8 files changed

+73
-14
lines changed

8 files changed

+73
-14
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ repos:
2424
hooks:
2525
- id: isort
2626
- repo: https://github.com/psf/black
27-
rev: 21.8b0
27+
rev: 21.9b0
2828
hooks:
2929
- id: black
3030
args:

docs/changelog/2215.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow plugins to update the :ref:`set_env` and change the :ref:`pass_env` configurations -- by :user:`gaborbernat`.

src/tox/config/set_env.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, raw: str, name: str, env_name: Optional[str]) -> None:
2828
else:
2929
self._raw[key] = value
3030
self._materialized: Dict[str, str] = {}
31+
self.changed = False
3132

3233
@staticmethod
3334
def _extract_key_value(line: str) -> Tuple[str, str]:
@@ -63,11 +64,12 @@ def __iter__(self) -> Iterator[str]:
6364
self._raw.update(sub_raw)
6465
yield from sub_raw.keys()
6566

66-
def update_if_not_present(self, param: Mapping[str, str]) -> None:
67+
def update(self, param: Mapping[str, str], *, override: bool = True) -> None:
6768
for key, value in param.items():
6869
# do not override something already set explicitly
69-
if key not in self._raw and key not in self._materialized:
70+
if override or (key not in self._raw and key not in self._materialized):
7071
self._materialized[key] = value
72+
self.changed = True
7173

7274

7375
__all__ = ("SetEnv",)

src/tox/config/sets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def __init__(self, conf: "Config", section: Section, env_name: str) -> None:
225225

226226
def register_config(self) -> None:
227227
def set_env_post_process(values: SetEnv) -> SetEnv:
228-
values.update_if_not_present(self.default_set_env_loader())
228+
values.update(self.default_set_env_loader(), override=False)
229229
return values
230230

231231
def set_env_factory(raw: object) -> SetEnv:

src/tox/pytest.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from tox.execute.api import Execute, ExecuteInstance, ExecuteOptions, ExecuteStatus, Outcome
3838
from tox.execute.request import ExecuteRequest, shell_cmd
3939
from tox.execute.stream import SyncWrite
40+
from tox.plugin import manager
4041
from tox.report import LOGGER, OutErr
4142
from tox.run import run as tox_run
4243
from tox.run import setup_state as previous_setup_state
@@ -74,12 +75,19 @@ def ensure_logging_framework_not_altered() -> Iterator[None]: # noqa: PT004
7475
def _disable_root_tox_py(request: SubRequest, mocker: MockerFixture) -> Iterator[None]:
7576
"""unless this is a plugin test do not allow loading toxfile.py"""
7677
if request.node.get_closest_marker("plugin_test"): # unregister inline plugin
77-
from tox.plugin import manager
78+
module, load_inline = None, manager._load_inline
7879

79-
inline_plugin = mocker.spy(manager, "_load_inline")
80+
def _load_inline(path: Path) -> Optional[ModuleType]: # register only on first run, and unregister at end
81+
nonlocal module
82+
if module is None:
83+
module = load_inline(path)
84+
return module
85+
return None
86+
87+
mocker.patch.object(manager, "_load_inline", _load_inline)
8088
yield
81-
if inline_plugin.spy_return is not None: # pragma: no branch
82-
manager.MANAGER.manager.unregister(inline_plugin.spy_return)
89+
if module is not None: # pragma: no branch
90+
manager.MANAGER.manager.unregister(module)
8391
else: # do not allow loading inline plugins
8492
mocker.patch("tox.plugin.inline._load_plugin", return_value=None)
8593
yield
@@ -605,7 +613,7 @@ def enable_pip_pypi_access_fixture(
605613
return previous_url
606614

607615

608-
def register_inline_plugin(mocker: MockerFixture, *args: Callable[..., Any]) -> None: #
616+
def register_inline_plugin(mocker: MockerFixture, *args: Callable[..., Any]) -> None:
609617
frame_info = inspect.stack()[1]
610618
caller_module = inspect.getmodule(frame_info[0])
611619
assert caller_module is not None

src/tox/tox_env/api.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def __init__(self, create_args: ToxEnvCreateArgs) -> None:
5959
self._paths_private: List[Path] = [] #: a property holding the PATH environment variables
6060
self._hidden_outcomes: Optional[List[Outcome]] = []
6161
self._env_vars: Optional[Dict[str, str]] = None
62+
self._env_vars_pass_env: List[str] = []
6263
self._suspended_out_err: Optional[OutErr] = None
6364
self._execute_statuses: Dict[int, ExecuteStatus] = {}
6465
self._interrupted = False
@@ -284,13 +285,14 @@ def _clean(self, transitive: bool = False) -> None: # noqa: U100
284285

285286
@property
286287
def _environment_variables(self) -> Dict[str, str]:
287-
if self._env_vars is not None:
288-
return self._env_vars
289288
pass_env: List[str] = self.conf["pass_env"]
290-
result = self._load_pass_env(pass_env)
291289
set_env: SetEnv = self.conf["set_env"]
290+
if self._env_vars_pass_env == pass_env and not set_env.changed and self._env_vars is not None:
291+
return self._env_vars
292+
293+
result = self._load_pass_env(pass_env)
292294
# load/paths_env might trigger a load of the environment variables, set result here, returns current state
293-
self._env_vars = result
295+
self._env_vars, self._env_vars_pass_env, set_env.changed = result, pass_env, False
294296
# set PATH here in case setting and environment variable requires access to the environment variable PATH
295297
result["PATH"] = self._make_path()
296298
for key in set_env:

tests/config/test_set_env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
def test_set_env_explicit() -> None:
1111
set_env = SetEnv("\nA=1\nB = 2\nC= 3\nD= 4", "py", "py")
12-
set_env.update_if_not_present({"E": "5 ", "F": "6"})
12+
set_env.update({"E": "5 ", "F": "6"}, override=False)
1313

1414
keys = list(set_env)
1515
assert keys == ["E", "F", "A", "B", "C", "D"]

tests/plugin/test_plugin.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logging
2+
import os
23
import sys
34
from typing import List
5+
from unittest.mock import patch
46

57
import pytest
68
from pytest_mock import MockerFixture
@@ -134,3 +136,47 @@ def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None:
134136
result = project.run()
135137
result.assert_failed()
136138
assert "raise TypeError(raw)" in result.out
139+
140+
141+
def test_plugin_extend_pass_env(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None:
142+
@impl
143+
def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None:
144+
env_conf["pass_env"].append("MAGIC_*")
145+
146+
register_inline_plugin(mocker, tox_add_env_config)
147+
ini = """
148+
[testenv]
149+
package=skip
150+
commands=python -c 'import os; print(os.environ["MAGIC_1"]); print(os.environ["MAGIC_2"])'
151+
"""
152+
project = tox_project({"tox.ini": ini})
153+
with patch.dict(os.environ, {"MAGIC_1": "magic_1", "MAGIC_2": "magic_2"}):
154+
result = project.run("r")
155+
result.assert_success()
156+
assert "magic_1" in result.out
157+
assert "magic_2" in result.out
158+
159+
result_conf = project.run("c", "-e", "py", "-k", "pass_env")
160+
result_conf.assert_success()
161+
assert "MAGIC_*" in result_conf.out
162+
163+
164+
def test_plugin_extend_set_env(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None:
165+
@impl
166+
def tox_add_env_config(env_conf: EnvConfigSet, config: Config) -> None:
167+
env_conf["set_env"].update({"MAGI_CAL": "magi_cal"})
168+
169+
register_inline_plugin(mocker, tox_add_env_config)
170+
ini = """
171+
[testenv]
172+
package=skip
173+
commands=python -c 'import os; print(os.environ["MAGI_CAL"])'
174+
"""
175+
project = tox_project({"tox.ini": ini})
176+
result = project.run("r")
177+
result.assert_success()
178+
assert "magi_cal" in result.out
179+
180+
result_conf = project.run("c", "-e", "py", "-k", "set_env")
181+
result_conf.assert_success()
182+
assert "MAGI_CAL=magi_cal" in result_conf.out

0 commit comments

Comments
 (0)