Skip to content

Commit 721c2b2

Browse files
committed
Allow disabling plugins on a one-off
Signed-off-by: Bernát Gábor <[email protected]>
1 parent e2ff114 commit 721c2b2

File tree

14 files changed

+114
-47
lines changed

14 files changed

+114
-47
lines changed

docs/changelog/3468.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow disabling tox plugins via the ``TOX_DISABLED_EXTERNAL_PLUGINS`` environment variable - by :user:`gaborbernat`.

docs/plugins.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ installed. For example:
2727
2828
For more information, refer to :ref:`the user guide <auto-provisioning>`.
2929

30+
Plugins can be disabled via the ``TOX_DISABLED_EXTERNAL_PLUGINS`` environment variable. This variable can be set to a
31+
comma separated list of plugin names, e.g.:
32+
33+
```bash
34+
env TOX_DISABLED_EXTERNAL_PLUGINS=tox-uv,tox-extra tox --version
35+
```
36+
3037
Developing your own plugin
3138
--------------------------
3239

pyproject.toml

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[build-system]
22
build-backend = "hatchling.build"
33
requires = [
4-
"hatch-vcs>=0.4",
4+
"hatch-vcs>=0.5",
55
"hatchling>=1.27",
66
]
77

@@ -50,22 +50,17 @@ dynamic = [
5050
"version",
5151
]
5252
dependencies = [
53-
"cachetools>=5.5.1",
53+
"cachetools>=6.1",
5454
"chardet>=5.2",
5555
"colorama>=0.4.6",
56-
"filelock>=3.16.1",
57-
"packaging>=24.2",
58-
"platformdirs>=4.3.6",
59-
"pluggy>=1.5",
60-
"pyproject-api>=1.8",
56+
"filelock>=3.18",
57+
"packaging>=25",
58+
"platformdirs>=4.3.8",
59+
"pluggy>=1.6",
60+
"pyproject-api>=1.9.1",
6161
"tomli>=2.2.1; python_version<'3.11'",
62-
"typing-extensions>=4.12.2; python_version<'3.11'",
63-
"virtualenv>=20.31",
64-
]
65-
optional-dependencies.test = [
66-
"devpi-process>=1.0.2",
67-
"pytest>=8.3.4",
68-
"pytest-mock>=3.14",
62+
"typing-extensions>=4.14; python_version<'3.11'",
63+
"virtualenv>=20.31.2",
6964
]
7065
urls.Documentation = "https://tox.wiki"
7166
urls.Homepage = "http://tox.readthedocs.org"
@@ -83,35 +78,35 @@ dev = [
8378
test = [
8479
"build[virtualenv]>=1.2.2.post1",
8580
"covdefaults>=2.3",
86-
"coverage>=7.9.1",
81+
"coverage>=7.9.2",
8782
"detect-test-pollution>=1.2",
8883
"devpi-process>=1.0.2",
89-
"diff-cover>=9.2",
84+
"diff-cover>=9.4.1",
9085
"distlib>=0.3.9",
9186
"flaky>=3.8.1",
92-
"hatch-vcs>=0.4",
87+
"hatch-vcs>=0.5",
9388
"hatchling>=1.27",
94-
"psutil>=6.1.1",
95-
"pytest>=8.3.4",
96-
"pytest-cov>=5",
97-
"pytest-mock>=3.14",
98-
"pytest-xdist>=3.6.1",
89+
"psutil>=7",
90+
"pytest>=8.4.1",
91+
"pytest-cov>=6.2.1",
92+
"pytest-mock>=3.14.1",
93+
"pytest-xdist>=3.8",
9994
"re-assert>=1.1",
100-
"setuptools>=75.8",
101-
"time-machine>=2.15; implementation_name!='pypy'",
95+
"setuptools>=80.9",
96+
"time-machine>=2.16; implementation_name!='pypy'",
10297
"wheel>=0.45.1",
10398
]
10499
type = [
105-
"mypy==1.15",
106-
"types-cachetools>=5.5.0.20240820",
100+
"mypy==1.16.1",
101+
"types-cachetools>=6.0.0.20250525",
107102
"types-chardet>=5.0.4.6",
108103
{ include-group = "test" },
109104
]
110105
docs = [
111106
"furo>=2024.8.6",
112-
"sphinx>=8.1.3",
107+
"sphinx>=8.2.3",
113108
"sphinx-argparse-cli>=1.19",
114-
"sphinx-autodoc-typehints>=3.0.1",
109+
"sphinx-autodoc-typehints>=3.2",
115110
"sphinx-copybutton>=0.5.2",
116111
"sphinx-inline-tabs>=2023.4.21",
117112
"sphinxcontrib-towncrier>=0.2.1a0",
@@ -121,13 +116,13 @@ fix = [
121116
"pre-commit-uv>=4.1.4",
122117
]
123118
pkg-meta = [
124-
"check-wheel-contents>=0.6.1",
119+
"check-wheel-contents>=0.6.2",
125120
"twine>=6.1",
126-
"uv>=0.5.29",
121+
"uv>=0.7.19",
127122
]
128123
release = [
129124
"gitpython>=3.1.44",
130-
"packaging>=24.2",
125+
"packaging>=25",
131126
"towncrier>=24.8",
132127
]
133128

src/tox/config/cli/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def add_argument_group(self, *args: Any, **kwargs: Any) -> Any:
263263

264264
def add_mutually_exclusive_group(**e_kwargs: Any) -> Any:
265265
def add_argument(*a_args: str, of_type: type[Any] | None = None, **a_kwargs: Any) -> Action:
266-
res_args: Action = prev_add_arg(*a_args, **a_kwargs) # type: ignore[has-type]
266+
res_args: Action = prev_add_arg(*a_args, **a_kwargs)
267267
arguments.append((a_args, of_type, a_kwargs))
268268
return res_args
269269

src/tox/plugin/manager.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import os
56
from typing import TYPE_CHECKING, Any
67

78
import pluggy
@@ -49,7 +50,7 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
4950

5051
if inline is not None:
5152
self.manager.register(inline)
52-
self.manager.load_setuptools_entrypoints(NAME)
53+
self._load_external_plugins()
5354
internal_plugins = (
5455
loader_api,
5556
provision,
@@ -74,6 +75,11 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
7475
self.manager.register(state)
7576
self.manager.check_pending()
7677

78+
def _load_external_plugins(self) -> None:
79+
for name in os.environ.get("TOX_DISABLED_EXTERNAL_PLUGINS", "").split(","):
80+
self.manager.set_blocked(name)
81+
self.manager.load_setuptools_entrypoints(NAME)
82+
7783
def tox_add_option(self, parser: ToxParser) -> None:
7884
self.manager.hook.tox_add_option(parser=parser)
7985

src/tox/pytest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ def _load_inline(path: Path) -> ModuleType | None: # register only on first run
8282
@contextmanager
8383
def check_os_environ() -> Iterator[None]:
8484
old = os.environ.copy()
85-
to_clean = {k: os.environ.pop(k, None) for k in (ENV_VAR_KEY, "TOX_WORK_DIR", "PYTHONPATH", "COV_CORE_CONTEXT")}
85+
to_clean = {
86+
k: os.environ.pop(k, None)
87+
for k in (ENV_VAR_KEY, "TOX_WORK_DIR", "PYTHONPATH", "COV_CORE_CONTEXT", "TOX_DISABLED_EXTERNAL_PLUGINS")
88+
}
8689

8790
yield
8891

@@ -93,6 +96,7 @@ def check_os_environ() -> Iterator[None]:
9396
new = os.environ
9497
extra = {k: new[k] for k in set(new) - set(old)}
9598
extra.pop("PLAT", None)
99+
extra.pop("TOX_DISABLED_EXTERNAL_PLUGINS", None)
96100
miss = {k: old[k] for k in set(old) - set(new)}
97101
diff = {
98102
f"{k} = {old[k]} vs {new[k]}" for k in set(old) & set(new) if old[k] != new[k] and not k.startswith("PYTEST_")

tests/plugin/test_plugin.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from tox.config.sets import ConfigSet, CoreConfigSet, EnvConfigSet
1414
from tox.execute import Outcome
1515
from tox.plugin import impl
16+
from tox.plugin.manager import Plugin
1617
from tox.pytest import ToxProjectCreator, register_inline_plugin
1718
from tox.session.state import State
1819
from tox.tox_env.api import ToxEnv
@@ -21,6 +22,7 @@
2122
if TYPE_CHECKING:
2223
from pathlib import Path
2324

25+
from pluggy import PluginManager
2426
from pytest_mock import MockerFixture
2527

2628

@@ -268,3 +270,45 @@ def tox_after_run_commands(tox_env: ToxEnv, exit_code: int, outcomes: list[Outco
268270
project = tox_project({"tox.ini": "[testenv]\npackage=skip"})
269271
result = project.run("r")
270272
result.assert_success()
273+
274+
275+
@pytest.mark.parametrize(
276+
("env_val", "expect_a", "expect_b"),
277+
[
278+
pytest.param("", True, True, id="none_disabled"),
279+
pytest.param("dummy_plugin_a,dummy_plugin_b", False, False, id="both_disabled"),
280+
pytest.param("dummy_plugin_a", False, True, id="only_a_disabled"),
281+
pytest.param("dummy_plugin_b", True, False, id="only_b_disabled"),
282+
],
283+
)
284+
def test_disable_external_plugins(
285+
tox_project: ToxProjectCreator,
286+
mocker: MockerFixture,
287+
env_val: str,
288+
expect_a: bool,
289+
expect_b: bool,
290+
) -> None:
291+
class DummyPluginA:
292+
@staticmethod
293+
@impl
294+
def tox_add_option(parser: ToxParser) -> None: # noqa: ARG004
295+
logging.warning("dummy plugin A called")
296+
297+
class DummyPluginB:
298+
@staticmethod
299+
@impl
300+
def tox_add_option(parser: ToxParser) -> None: # noqa: ARG004
301+
logging.warning("dummy plugin B called")
302+
303+
def fake_load_entrypoints(self: PluginManager, name: str) -> None: # noqa: ARG001
304+
self.register(DummyPluginA(), name="dummy_plugin_a")
305+
self.register(DummyPluginB(), name="dummy_plugin_b")
306+
307+
mocker.patch("pluggy.PluginManager.load_setuptools_entrypoints", fake_load_entrypoints)
308+
mocker.patch.dict(os.environ, {"TOX_DISABLED_EXTERNAL_PLUGINS": env_val})
309+
mocker.patch("tox.plugin.manager.MANAGER", Plugin())
310+
project = tox_project({"tox.ini": ""})
311+
result = project.run("--version")
312+
result.assert_success()
313+
assert ("dummy plugin A called" in result.out) == expect_a
314+
assert ("dummy plugin B called" in result.out) == expect_b

tests/session/cmd/test_sequential.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_result_json_sequential(
7171
"setup.py": "from setuptools import setup\nsetup(name='a', version='1.0', py_modules=['run'],"
7272
"install_requires=['setuptools>44'])",
7373
"run.py": "print('run')",
74-
"pyproject.toml": '[build-system]\nrequires=["setuptools","wheel"]\nbuild-backend="setuptools.build_meta"',
74+
"pyproject.toml": '[build-system]\nrequires=["setuptools"]\nbuild-backend="setuptools.build_meta"',
7575
},
7676
)
7777
log = project.path / "log.json"
@@ -104,7 +104,7 @@ def test_result_json_sequential(
104104
packaging_test = get_cmd_exit_run_id(log_report, ".pkg", "test")
105105
assert packaging_test == [(None, "build_wheel")]
106106
packaging_installed = log_report["testenvs"][".pkg"].pop("installed_packages")
107-
assert {i[: i.find("==")] for i in packaging_installed} == {"pip", "setuptools", "wheel"}
107+
assert {i[: i.find("==")] for i in packaging_installed} == {"pip", "setuptools"}
108108

109109
result_py = log_report["testenvs"]["py"].pop("result")
110110
assert result_py.pop("duration") > 0

tests/test_provision.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,13 @@ def tox_wheels(tox_wheel: Path, tmp_path_factory: TempPathFactory) -> list[Path]
9191
assert distribution.requires is not None
9292
for req in distribution.requires:
9393
requirement = Requirement(req)
94-
if not requirement.extras: # pragma: no branch # we don't need to install any extras (tests/docs/etc)
94+
if not requirement.extras: # pragma: no branch # we don't need to install any extras (tests/docs/etc)
9595
cmd.append(req)
9696
check_call(cmd)
9797
result.extend(wheel_cache.iterdir())
98+
res = "\n".join(str(i) for i in result)
99+
with elapsed(f"acquired dependencies for current tox: {res}"):
100+
pass
98101
return result
99102

100103

tests/tox_env/python/virtual_env/package/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ def pkg_with_extras_project(tmp_path_factory: pytest.TempPathFactory) -> Path:
2020
[options]
2121
packages = find:
2222
install_requires =
23-
platformdirs>=2.1
24-
colorama>=0.4.3
23+
platformdirs>=4.3.8
24+
colorama>=0.4.6
2525
2626
[options.extras_require]
2727
testing =

0 commit comments

Comments
 (0)