Skip to content

Allow disabling plugins on a one-off #3560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions docs/changelog/3468.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow disabling tox plugins via the ``TOX_DISABLED_EXTERNAL_PLUGINS`` environment variable - by :user:`gaborbernat`.
7 changes: 7 additions & 0 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ installed. For example:

For more information, refer to :ref:`the user guide <auto-provisioning>`.

Plugins can be disabled via the ``TOX_DISABLED_EXTERNAL_PLUGINS`` environment variable. This variable can be set to a
comma separated list of plugin names, e.g.:

```bash
env TOX_DISABLED_EXTERNAL_PLUGINS=tox-uv,tox-extra tox --version
```

Developing your own plugin
--------------------------

Expand Down
57 changes: 26 additions & 31 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"hatch-vcs>=0.4",
"hatch-vcs>=0.5",
"hatchling>=1.27",
]

Expand Down Expand Up @@ -50,22 +50,17 @@ dynamic = [
"version",
]
dependencies = [
"cachetools>=5.5.1",
"cachetools>=6.1",
"chardet>=5.2",
"colorama>=0.4.6",
"filelock>=3.16.1",
"packaging>=24.2",
"platformdirs>=4.3.6",
"pluggy>=1.5",
"pyproject-api>=1.8",
"filelock>=3.18",
"packaging>=25",
"platformdirs>=4.3.8",
"pluggy>=1.6",
"pyproject-api>=1.9.1",
"tomli>=2.2.1; python_version<'3.11'",
"typing-extensions>=4.12.2; python_version<'3.11'",
"virtualenv>=20.31",
]
optional-dependencies.test = [
"devpi-process>=1.0.2",
"pytest>=8.3.4",
"pytest-mock>=3.14",
"typing-extensions>=4.14; python_version<'3.11'",
"virtualenv>=20.31.2",
]
urls.Documentation = "https://tox.wiki"
urls.Homepage = "http://tox.readthedocs.org"
Expand All @@ -83,35 +78,35 @@ dev = [
test = [
"build[virtualenv]>=1.2.2.post1",
"covdefaults>=2.3",
"coverage>=7.9.1",
"coverage>=7.9.2",
"detect-test-pollution>=1.2",
"devpi-process>=1.0.2",
"diff-cover>=9.2",
"diff-cover>=9.4.1",
"distlib>=0.3.9",
"flaky>=3.8.1",
"hatch-vcs>=0.4",
"hatch-vcs>=0.5",
"hatchling>=1.27",
"psutil>=6.1.1",
"pytest>=8.3.4",
"pytest-cov>=5",
"pytest-mock>=3.14",
"pytest-xdist>=3.6.1",
"psutil>=7",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
"pytest-mock>=3.14.1",
"pytest-xdist>=3.8",
"re-assert>=1.1",
"setuptools>=75.8",
"time-machine>=2.15; implementation_name!='pypy'",
"setuptools>=80.9",
"time-machine>=2.16; implementation_name!='pypy'",
"wheel>=0.45.1",
]
type = [
"mypy==1.15",
"types-cachetools>=5.5.0.20240820",
"mypy==1.16.1",
"types-cachetools>=6.0.0.20250525",
"types-chardet>=5.0.4.6",
{ include-group = "test" },
]
docs = [
"furo>=2024.8.6",
"sphinx>=8.1.3",
"sphinx>=8.2.3",
"sphinx-argparse-cli>=1.19",
"sphinx-autodoc-typehints>=3.0.1",
"sphinx-autodoc-typehints>=3.2",
"sphinx-copybutton>=0.5.2",
"sphinx-inline-tabs>=2023.4.21",
"sphinxcontrib-towncrier>=0.2.1a0",
Expand All @@ -121,13 +116,13 @@ fix = [
"pre-commit-uv>=4.1.4",
]
pkg-meta = [
"check-wheel-contents>=0.6.1",
"check-wheel-contents>=0.6.2",
"twine>=6.1",
"uv>=0.5.29",
"uv>=0.7.19",
]
release = [
"gitpython>=3.1.44",
"packaging>=24.2",
"packaging>=25",
"towncrier>=24.8",
]

Expand Down
2 changes: 1 addition & 1 deletion src/tox/config/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def add_argument_group(self, *args: Any, **kwargs: Any) -> Any:

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

Expand Down
8 changes: 7 additions & 1 deletion src/tox/plugin/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
from typing import TYPE_CHECKING, Any

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

if inline is not None:
self.manager.register(inline)
self.manager.load_setuptools_entrypoints(NAME)
self._load_external_plugins()
internal_plugins = (
loader_api,
provision,
Expand All @@ -74,6 +75,11 @@ def _register_plugins(self, inline: ModuleType | None) -> None:
self.manager.register(state)
self.manager.check_pending()

def _load_external_plugins(self) -> None:
for name in os.environ.get("TOX_DISABLED_EXTERNAL_PLUGINS", "").split(","):
self.manager.set_blocked(name)
self.manager.load_setuptools_entrypoints(NAME)

def tox_add_option(self, parser: ToxParser) -> None:
self.manager.hook.tox_add_option(parser=parser)

Expand Down
6 changes: 5 additions & 1 deletion src/tox/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ def _load_inline(path: Path) -> ModuleType | None: # register only on first run
@contextmanager
def check_os_environ() -> Iterator[None]:
old = os.environ.copy()
to_clean = {k: os.environ.pop(k, None) for k in (ENV_VAR_KEY, "TOX_WORK_DIR", "PYTHONPATH", "COV_CORE_CONTEXT")}
to_clean = {
k: os.environ.pop(k, None)
for k in (ENV_VAR_KEY, "TOX_WORK_DIR", "PYTHONPATH", "COV_CORE_CONTEXT", "TOX_DISABLED_EXTERNAL_PLUGINS")
}

yield

Expand All @@ -93,6 +96,7 @@ def check_os_environ() -> Iterator[None]:
new = os.environ
extra = {k: new[k] for k in set(new) - set(old)}
extra.pop("PLAT", None)
extra.pop("TOX_DISABLED_EXTERNAL_PLUGINS", None)
miss = {k: old[k] for k in set(old) - set(new)}
diff = {
f"{k} = {old[k]} vs {new[k]}" for k in set(old) & set(new) if old[k] != new[k] and not k.startswith("PYTEST_")
Expand Down
44 changes: 44 additions & 0 deletions tests/plugin/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from tox.config.sets import ConfigSet, CoreConfigSet, EnvConfigSet
from tox.execute import Outcome
from tox.plugin import impl
from tox.plugin.manager import Plugin
from tox.pytest import ToxProjectCreator, register_inline_plugin
from tox.session.state import State
from tox.tox_env.api import ToxEnv
Expand All @@ -21,6 +22,7 @@
if TYPE_CHECKING:
from pathlib import Path

from pluggy import PluginManager
from pytest_mock import MockerFixture


Expand Down Expand Up @@ -268,3 +270,45 @@ def tox_after_run_commands(tox_env: ToxEnv, exit_code: int, outcomes: list[Outco
project = tox_project({"tox.ini": "[testenv]\npackage=skip"})
result = project.run("r")
result.assert_success()


@pytest.mark.parametrize(
("env_val", "expect_a", "expect_b"),
[
pytest.param("", True, True, id="none_disabled"),
pytest.param("dummy_plugin_a,dummy_plugin_b", False, False, id="both_disabled"),
pytest.param("dummy_plugin_a", False, True, id="only_a_disabled"),
pytest.param("dummy_plugin_b", True, False, id="only_b_disabled"),
],
)
def test_disable_external_plugins(
tox_project: ToxProjectCreator,
mocker: MockerFixture,
env_val: str,
expect_a: bool,
expect_b: bool,
) -> None:
class DummyPluginA:
@staticmethod
@impl
def tox_add_option(parser: ToxParser) -> None: # noqa: ARG004
logging.warning("dummy plugin A called")

class DummyPluginB:
@staticmethod
@impl
def tox_add_option(parser: ToxParser) -> None: # noqa: ARG004
logging.warning("dummy plugin B called")

def fake_load_entrypoints(self: PluginManager, name: str) -> None: # noqa: ARG001
self.register(DummyPluginA(), name="dummy_plugin_a")
self.register(DummyPluginB(), name="dummy_plugin_b")

mocker.patch("pluggy.PluginManager.load_setuptools_entrypoints", fake_load_entrypoints)
mocker.patch.dict(os.environ, {"TOX_DISABLED_EXTERNAL_PLUGINS": env_val})
mocker.patch("tox.plugin.manager.MANAGER", Plugin())
project = tox_project({"tox.ini": ""})
result = project.run("--version")
result.assert_success()
assert ("dummy plugin A called" in result.out) == expect_a
assert ("dummy plugin B called" in result.out) == expect_b
4 changes: 2 additions & 2 deletions tests/session/cmd/test_sequential.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_result_json_sequential(
"setup.py": "from setuptools import setup\nsetup(name='a', version='1.0', py_modules=['run'],"
"install_requires=['setuptools>44'])",
"run.py": "print('run')",
"pyproject.toml": '[build-system]\nrequires=["setuptools","wheel"]\nbuild-backend="setuptools.build_meta"',
"pyproject.toml": '[build-system]\nrequires=["setuptools"]\nbuild-backend="setuptools.build_meta"',
},
)
log = project.path / "log.json"
Expand Down Expand Up @@ -104,7 +104,7 @@ def test_result_json_sequential(
packaging_test = get_cmd_exit_run_id(log_report, ".pkg", "test")
assert packaging_test == [(None, "build_wheel")]
packaging_installed = log_report["testenvs"][".pkg"].pop("installed_packages")
assert {i[: i.find("==")] for i in packaging_installed} == {"pip", "setuptools", "wheel"}
assert {i[: i.find("==")] for i in packaging_installed} == {"pip", "setuptools"}

result_py = log_report["testenvs"]["py"].pop("result")
assert result_py.pop("duration") > 0
Expand Down
5 changes: 4 additions & 1 deletion tests/test_provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ def tox_wheels(tox_wheel: Path, tmp_path_factory: TempPathFactory) -> list[Path]
assert distribution.requires is not None
for req in distribution.requires:
requirement = Requirement(req)
if not requirement.extras: # pragma: no branch # we don't need to install any extras (tests/docs/etc)
if not requirement.extras: # pragma: no branch # we don't need to install any extras (tests/docs/etc)
cmd.append(req)
check_call(cmd)
result.extend(wheel_cache.iterdir())
res = "\n".join(str(i) for i in result)
with elapsed(f"acquired dependencies for current tox: {res}"):
pass
return result


Expand Down
4 changes: 2 additions & 2 deletions tests/tox_env/python/virtual_env/package/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def pkg_with_extras_project(tmp_path_factory: pytest.TempPathFactory) -> Path:
[options]
packages = find:
install_requires =
platformdirs>=2.1
colorama>=0.4.3
platformdirs>=4.3.8
colorama>=0.4.6

[options.extras_require]
testing =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_tox_install_pkg_wheel(tox_project: ToxProjectCreator, pkg_with_extras_p
result = proj.run("r", "-e", "py", "--installpkg", str(pkg_with_extras_project_wheel))
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd[5:]) for i in execute_calls.call_args_list]
deps = ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3"]
deps = ["black>=3", "colorama>=0.4.6", "flake8", "platformdirs>=4.3.8", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3"]
expected = [
("py", "install_package_deps", deps),
("py", "install_package", ["--force-reinstall", "--no-deps", str(pkg_with_extras_project_wheel)]),
Expand Down Expand Up @@ -61,7 +61,7 @@ def test_tox_install_pkg_sdist(tox_project: ToxProjectCreator, pkg_with_extras_p
result = proj.run("r", "-e", "py", "--installpkg", str(pkg_with_extras_project_sdist))
result.assert_success()
calls = [(i[0][0].conf.name, i[0][3].run_id, i[0][3].cmd[5:]) for i in execute_calls.call_args_list]
deps = ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3"]
deps = ["black>=3", "colorama>=0.4.6", "flake8", "platformdirs>=4.3.8", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3"]
assert calls == [
(".pkg_external_sdist_meta", "install_requires", ["setuptools", "wheel"]),
(".pkg_external_sdist_meta", "_optional_hooks", []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,15 @@ def test_get_package_deps_different_extras(pkg_with_extras_project: Path, tox_pr
if i[0][3].run_id.startswith("install_package_deps")
}
assert installs == {
"a": ["colorama>=0.4.3", "platformdirs>=2.1", "setuptools", "sphinx-rtd-theme<1,>=0.4.3", "sphinx>=3", "wheel"],
"b": ["black>=3", "colorama>=0.4.3", "flake8", "platformdirs>=2.1"],
"a": [
"colorama>=0.4.6",
"platformdirs>=4.3.8",
"setuptools",
"sphinx-rtd-theme<1,>=0.4.3",
"sphinx>=3",
"wheel",
],
"b": ["black>=3", "colorama>=0.4.6", "flake8", "platformdirs>=4.3.8"],
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_load_dependency_no_extra(pkg_with_extras: PathDistribution) -> None:
requires = pkg_with_extras.requires
assert requires is not None
result = dependencies_with_extras([Requirement(i) for i in requires], set(), "")
for left, right in zip_longest(result, (Requirement("platformdirs>=2.1"), Requirement("colorama>=0.4.3"))):
for left, right in zip_longest(result, (Requirement("platformdirs>=4.3.8"), Requirement("colorama>=0.4.6"))):
assert isinstance(right, Requirement)
assert str(left) == str(right)

Expand All @@ -41,8 +41,8 @@ def test_load_dependency_many_extra(pkg_with_extras: PathDistribution) -> None:
result = dependencies_with_extras([Requirement(i) for i in requires], {"docs", "testing"}, "")
sphinx = [Requirement("sphinx>=3"), Requirement("sphinx-rtd-theme<1,>=0.4.3")]
exp = [
Requirement("platformdirs>=2.1"),
Requirement("colorama>=0.4.3"),
Requirement("platformdirs>=4.3.8"),
Requirement("colorama>=0.4.6"),
*(sphinx if sys.version_info[0:2] <= (3, 8) else []),
Requirement(f'covdefaults>=1.2; python_version == "2.7" or python_version == "{py_ver}"'),
Requirement(f'pytest>=5.4.1; python_version == "{py_ver}"'),
Expand Down
2 changes: 1 addition & 1 deletion tox.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
requires = ["tox>=4.24.1"]
requires = ["tox>=4.27"]
env_list = ["fix", "3.14t", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "cov", "type", "docs", "pkg_meta"]
skip_missing_interpreters = true

Expand Down
Loading