Skip to content

Commit 5bd393c

Browse files
jakkdlThe-Compilernicoddemuspre-commit-ci[bot]
authored
add --disable-plugin-autoload (#13253)
* add --disable-plugin-autoload * update comments in test, don't check __spec__ on pypy (????), add changelog * pemdas except not * Apply suggestions from code review Co-authored-by: Florian Bruhin <[email protected]> * add parens * Simplify plugin name in test_installed_plugin_rewrite * Apply suggestions from code review * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Florian Bruhin <[email protected]> Co-authored-by: Bruno Oliveira <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ca35145 commit 5bd393c

File tree

7 files changed

+151
-31
lines changed

7 files changed

+151
-31
lines changed

changelog/13253.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
New flag: :ref:`--disable-plugin-autoload <disable_plugin_autoload>` which works as an alternative to :envvar:`PYTEST_DISABLE_PLUGIN_AUTOLOAD` when setting environment variables is inconvenient; and allows setting it in config files with :confval:`addopts`.

doc/en/how-to/plugins.rst

+26-1
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,29 @@ CI server), you can set ``PYTEST_ADDOPTS`` environment variable to
133133

134134
See :ref:`findpluginname` for how to obtain the name of a plugin.
135135

136-
.. _`builtin plugins`:
136+
.. _`disable_plugin_autoload`:
137+
138+
Disabling plugins from autoloading
139+
----------------------------------
140+
141+
If you want to disable plugins from loading automatically, instead of requiring you to
142+
manually specify each plugin with ``-p`` or :envvar:`PYTEST_PLUGINS`, you can use ``--disable-plugin-autoload`` or :envvar:`PYTEST_DISABLE_PLUGIN_AUTOLOAD`.
143+
144+
.. code-block:: bash
145+
146+
export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
147+
export PYTEST_PLUGINS=NAME,NAME2
148+
pytest
149+
150+
.. code-block:: bash
151+
152+
pytest --disable-plugin-autoload -p NAME,NAME2
153+
154+
.. code-block:: ini
155+
156+
[pytest]
157+
addopts = --disable-plugin-autoload -p NAME,NAME2
158+
159+
.. versionadded:: 8.4
160+
161+
The ``--disable-plugin-autoload`` command-line flag.

doc/en/reference/reference.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -1181,8 +1181,9 @@ as discussed in :ref:`temporary directory location and retention`.
11811181
.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD
11821182

11831183
When set, disables plugin auto-loading through :std:doc:`entry point packaging
1184-
metadata <packaging:guides/creating-and-discovering-plugins>`. Only explicitly
1185-
specified plugins will be loaded.
1184+
metadata <packaging:guides/creating-and-discovering-plugins>`. Only plugins
1185+
explicitly specified in :envvar:`PYTEST_PLUGINS` or with ``-p`` will be loaded.
1186+
See also :ref:`--disable-plugin-autoload <disable_plugin_autoload>`.
11861187

11871188
.. envvar:: PYTEST_PLUGINS
11881189

@@ -1192,6 +1193,8 @@ Contains comma-separated list of modules that should be loaded as plugins:
11921193
11931194
export PYTEST_PLUGINS=mymodule.plugin,xdist
11941195
1196+
See also ``-p``.
1197+
11951198
.. envvar:: PYTEST_THEME
11961199

11971200
Sets a `pygment style <https://pygments.org/docs/styles/>`_ to use for the code output.

src/_pytest/config/__init__.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070

7171

7272
if TYPE_CHECKING:
73+
from _pytest.assertions.rewrite import AssertionRewritingHook
7374
from _pytest.cacheprovider import Cache
7475
from _pytest.terminal import TerminalReporter
7576

@@ -1271,6 +1272,10 @@ def _consider_importhook(self, args: Sequence[str]) -> None:
12711272
"""
12721273
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
12731274
mode = getattr(ns, "assertmode", "plain")
1275+
1276+
disable_autoload = getattr(ns, "disable_plugin_autoload", False) or bool(
1277+
os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1278+
)
12741279
if mode == "rewrite":
12751280
import _pytest.assertion
12761281

@@ -1279,16 +1284,18 @@ def _consider_importhook(self, args: Sequence[str]) -> None:
12791284
except SystemError:
12801285
mode = "plain"
12811286
else:
1282-
self._mark_plugins_for_rewrite(hook)
1287+
self._mark_plugins_for_rewrite(hook, disable_autoload)
12831288
self._warn_about_missing_assertion(mode)
12841289

1285-
def _mark_plugins_for_rewrite(self, hook) -> None:
1290+
def _mark_plugins_for_rewrite(
1291+
self, hook: AssertionRewritingHook, disable_autoload: bool
1292+
) -> None:
12861293
"""Given an importhook, mark for rewrite any top-level
12871294
modules or packages in the distribution package for
12881295
all pytest plugins."""
12891296
self.pluginmanager.rewrite_hook = hook
12901297

1291-
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
1298+
if disable_autoload:
12921299
# We don't autoload from distribution package entry points,
12931300
# no need to continue.
12941301
return
@@ -1393,10 +1400,15 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None:
13931400
self._consider_importhook(args)
13941401
self._configure_python_path()
13951402
self.pluginmanager.consider_preparse(args, exclude_only=False)
1396-
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
1397-
# Don't autoload from distribution package entry point. Only
1398-
# explicitly specified plugins are going to be loaded.
1403+
if (
1404+
not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1405+
and not self.known_args_namespace.disable_plugin_autoload
1406+
):
1407+
# Autoloading from distribution package entry point has
1408+
# not been disabled.
13991409
self.pluginmanager.load_setuptools_entrypoints("pytest11")
1410+
# Otherwise only plugins explicitly specified in PYTEST_PLUGINS
1411+
# are going to be loaded.
14001412
self.pluginmanager.consider_env()
14011413

14021414
self.known_args_namespace = self._parser.parse_known_args(
@@ -1419,7 +1431,7 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None:
14191431
except ConftestImportFailure as e:
14201432
if self.known_args_namespace.help or self.known_args_namespace.version:
14211433
# we don't want to prevent --help/--version to work
1422-
# so just let is pass and print a warning at the end
1434+
# so just let it pass and print a warning at the end
14231435
self.issue_config_time_warning(
14241436
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
14251437
stacklevel=2,

src/_pytest/helpconfig.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ def pytest_addoption(parser: Parser) -> None:
7070
metavar="name",
7171
help="Early-load given plugin module name or entry point (multi-allowed). "
7272
"To avoid loading of plugins, use the `no:` prefix, e.g. "
73-
"`no:doctest`.",
73+
"`no:doctest`. See also --disable-plugin-autoload.",
74+
)
75+
group.addoption(
76+
"--disable-plugin-autoload",
77+
action="store_true",
78+
default=False,
79+
help="Disable plugin auto-loading through entry point packaging metadata. "
80+
"Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.",
7481
)
7582
group.addoption(
7683
"--traceconfig",

testing/test_assertion.py

+43-8
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,36 @@ def test_foo(pytestconfig):
218218
assert result.ret == 0
219219

220220
@pytest.mark.parametrize("mode", ["plain", "rewrite"])
221+
@pytest.mark.parametrize("disable_plugin_autoload", ["env_var", "cli", ""])
222+
@pytest.mark.parametrize("explicit_specify", ["env_var", "cli", ""])
221223
def test_installed_plugin_rewrite(
222-
self, pytester: Pytester, mode, monkeypatch
224+
self,
225+
pytester: Pytester,
226+
mode: str,
227+
monkeypatch: pytest.MonkeyPatch,
228+
disable_plugin_autoload: str,
229+
explicit_specify: str,
223230
) -> None:
224-
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
231+
args = ["mainwrapper.py", "-s", f"--assert={mode}"]
232+
if disable_plugin_autoload == "env_var":
233+
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
234+
elif disable_plugin_autoload == "cli":
235+
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
236+
args.append("--disable-plugin-autoload")
237+
else:
238+
assert disable_plugin_autoload == ""
239+
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
240+
241+
name = "spamplugin"
242+
243+
if explicit_specify == "env_var":
244+
monkeypatch.setenv("PYTEST_PLUGINS", name)
245+
elif explicit_specify == "cli":
246+
args.append("-p")
247+
args.append(name)
248+
else:
249+
assert explicit_specify == ""
250+
225251
# Make sure the hook is installed early enough so that plugins
226252
# installed via distribution package are rewritten.
227253
pytester.mkdir("hampkg")
@@ -250,7 +276,7 @@ def check(values, value):
250276
import pytest
251277
252278
class DummyEntryPoint(object):
253-
name = 'spam'
279+
name = 'spamplugin'
254280
module_name = 'spam.py'
255281
group = 'pytest11'
256282
@@ -275,20 +301,29 @@ def test(check_first):
275301
check_first([10, 30], 30)
276302
277303
def test2(check_first2):
278-
check_first([10, 30], 30)
304+
check_first2([10, 30], 30)
279305
""",
280306
}
281307
pytester.makepyfile(**contents)
282-
result = pytester.run(
283-
sys.executable, "mainwrapper.py", "-s", f"--assert={mode}"
284-
)
308+
result = pytester.run(sys.executable, *args)
285309
if mode == "plain":
286310
expected = "E AssertionError"
287311
elif mode == "rewrite":
288312
expected = "*assert 10 == 30*"
289313
else:
290314
assert 0
291-
result.stdout.fnmatch_lines([expected])
315+
316+
if not disable_plugin_autoload or explicit_specify:
317+
result.assert_outcomes(failed=2)
318+
result.stdout.fnmatch_lines([expected, expected])
319+
else:
320+
result.assert_outcomes(errors=2)
321+
result.stdout.fnmatch_lines(
322+
[
323+
"E fixture 'check_first' not found",
324+
"E fixture 'check_first2' not found",
325+
]
326+
)
292327

293328
def test_rewrite_ast(self, pytester: Pytester) -> None:
294329
pytester.mkdir("pkg")

testing/test_config.py

+49-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import importlib.metadata
77
import os
88
from pathlib import Path
9+
import platform
910
import re
1011
import sys
1112
import textwrap
@@ -1314,14 +1315,13 @@ def distributions():
13141315
)
13151316

13161317

1317-
@pytest.mark.parametrize(
1318-
"parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)]
1319-
)
1318+
@pytest.mark.parametrize("disable_plugin_method", ["env_var", "flag", ""])
1319+
@pytest.mark.parametrize("enable_plugin_method", ["env_var", "flag", ""])
13201320
def test_disable_plugin_autoload(
13211321
pytester: Pytester,
13221322
monkeypatch: MonkeyPatch,
1323-
parse_args: tuple[str, str] | tuple[()],
1324-
should_load: bool,
1323+
enable_plugin_method: str,
1324+
disable_plugin_method: str,
13251325
) -> None:
13261326
class DummyEntryPoint:
13271327
project_name = name = "mytestplugin"
@@ -1342,23 +1342,60 @@ class PseudoPlugin:
13421342
attrs_used = []
13431343

13441344
def __getattr__(self, name):
1345-
assert name == "__loader__"
1345+
assert name in ("__loader__", "__spec__")
13461346
self.attrs_used.append(name)
13471347
return object()
13481348

13491349
def distributions():
13501350
return (Distribution(),)
13511351

1352-
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
1352+
parse_args: list[str] = []
1353+
1354+
if disable_plugin_method == "env_var":
1355+
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
1356+
elif disable_plugin_method == "flag":
1357+
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1358+
parse_args.append("--disable-plugin-autoload")
1359+
else:
1360+
assert disable_plugin_method == ""
1361+
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1362+
1363+
if enable_plugin_method == "env_var":
1364+
monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin")
1365+
elif enable_plugin_method == "flag":
1366+
parse_args.extend(["-p", "mytestplugin"])
1367+
else:
1368+
assert enable_plugin_method == ""
1369+
13531370
monkeypatch.setattr(importlib.metadata, "distributions", distributions)
13541371
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin())
13551372
config = pytester.parseconfig(*parse_args)
1373+
13561374
has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
1357-
assert has_loaded == should_load
1358-
if should_load:
1359-
assert PseudoPlugin.attrs_used == ["__loader__"]
1360-
else:
1361-
assert PseudoPlugin.attrs_used == []
1375+
# it should load if it's enabled, or we haven't disabled autoloading
1376+
assert has_loaded == (bool(enable_plugin_method) or not disable_plugin_method)
1377+
1378+
# The reason for the discrepancy between 'has_loaded' and __loader__ being accessed
1379+
# appears to be the monkeypatching of importlib.metadata.distributions; where
1380+
# files being empty means that _mark_plugins_for_rewrite doesn't find the plugin.
1381+
# But enable_method==flag ends up in mark_rewrite being called and __loader__
1382+
# being accessed.
1383+
assert ("__loader__" in PseudoPlugin.attrs_used) == (
1384+
has_loaded
1385+
and not (enable_plugin_method in ("env_var", "") and not disable_plugin_method)
1386+
)
1387+
1388+
# __spec__ is accessed in AssertionRewritingHook.exec_module, which would be
1389+
# eventually called if we did a full pytest run; but it's only accessed with
1390+
# enable_plugin_method=="env_var" because that will early-load it.
1391+
# Except when autoloads aren't disabled, in which case PytestPluginManager.import_plugin
1392+
# bails out before importing it.. because it knows it'll be loaded later?
1393+
# The above seems a bit weird, but I *think* it's true.
1394+
if platform.python_implementation() != "PyPy":
1395+
assert ("__spec__" in PseudoPlugin.attrs_used) == bool(
1396+
enable_plugin_method == "env_var" and disable_plugin_method
1397+
)
1398+
# __spec__ is present when testing locally on pypy, but not in CI ????
13621399

13631400

13641401
def test_plugin_loading_order(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)