Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
23 changes: 22 additions & 1 deletion doc/en/how-to/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,25 @@ CI server), you can set ``PYTEST_ADDOPTS`` environment variable to

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

.. _`builtin plugins`:
.. _`disable_plugin_autoload`:

Disabling plugins from autoloading
----------------------------------

If you want to disable plugins from loading automatically, requiring you to
Comment thread
nicoddemus marked this conversation as resolved.
Outdated
manually specify each plugin with ``-p`` or :envvar:`PYTEST_PLUGINS`, you can use ``--disable-plugin-autoload`` or :envvar:`PYTEST_DISABLE_PLUGIN_AUTOLOAD`.

.. code-block:: bash

export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
export PYTEST_PLUGINS=NAME
Comment thread
jakkdl marked this conversation as resolved.
Outdated
pytest

.. code-block:: bash

pytest --disable-plugin-autoload -p NAME,NAME2

.. code-block:: ini

[pytest]
addopts = --disable-plugin-autoload -p NAME,NAME2
Comment thread
nicoddemus marked this conversation as resolved.
7 changes: 5 additions & 2 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1161,8 +1161,9 @@ as discussed in :ref:`temporary directory location and retention`.
.. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD

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

.. envvar:: PYTEST_PLUGINS

Expand All @@ -1172,6 +1173,8 @@ Contains comma-separated list of modules that should be loaded as plugins:

export PYTEST_PLUGINS=mymodule.plugin,xdist

See also ``-p``.

.. envvar:: PYTEST_THEME

Sets a `pygment style <https://pygments.org/docs/styles/>`_ to use for the code output.
Expand Down
26 changes: 19 additions & 7 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@


if TYPE_CHECKING:
from _pytest.assertions.rewrite import AssertionRewritingHook
from _pytest.cacheprovider import Cache
from _pytest.terminal import TerminalReporter

Expand Down Expand Up @@ -1271,6 +1272,10 @@ def _consider_importhook(self, args: Sequence[str]) -> None:
"""
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
mode = getattr(ns, "assertmode", "plain")

disable_autoload = getattr(ns, "disable_plugin_autoload", False) | bool(
Comment thread
nicoddemus marked this conversation as resolved.
Outdated
os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
)
if mode == "rewrite":
import _pytest.assertion

Expand All @@ -1279,16 +1284,18 @@ def _consider_importhook(self, args: Sequence[str]) -> None:
except SystemError:
mode = "plain"
else:
self._mark_plugins_for_rewrite(hook)
self._mark_plugins_for_rewrite(hook, disable_autoload)
self._warn_about_missing_assertion(mode)

def _mark_plugins_for_rewrite(self, hook) -> None:
def _mark_plugins_for_rewrite(
self, hook: AssertionRewritingHook, disable_autoload: bool
) -> None:
"""Given an importhook, mark for rewrite any top-level
modules or packages in the distribution package for
all pytest plugins."""
self.pluginmanager.rewrite_hook = hook

if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
if disable_autoload:
# We don't autoload from distribution package entry points,
# no need to continue.
return
Expand Down Expand Up @@ -1393,10 +1400,15 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None:
self._consider_importhook(args)
self._configure_python_path()
self.pluginmanager.consider_preparse(args, exclude_only=False)
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# Don't autoload from distribution package entry point. Only
# explicitly specified plugins are going to be loaded.
if (
not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
and not self.known_args_namespace.disable_plugin_autoload
):
# Autoloading from distribution package entry point has
# not been disabled.
self.pluginmanager.load_setuptools_entrypoints("pytest11")
# Otherwise only plugins explicitly specified in PYTEST_PLUGINS
# are going to be loaded.
self.pluginmanager.consider_env()

self.known_args_namespace = self._parser.parse_known_args(
Expand All @@ -1419,7 +1431,7 @@ def _preparse(self, args: list[str], addopts: bool = True) -> None:
except ConftestImportFailure as e:
if self.known_args_namespace.help or self.known_args_namespace.version:
# we don't want to prevent --help/--version to work
# so just let is pass and print a warning at the end
# so just let it pass and print a warning at the end
self.issue_config_time_warning(
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
stacklevel=2,
Expand Down
9 changes: 8 additions & 1 deletion src/_pytest/helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,14 @@ def pytest_addoption(parser: Parser) -> None:
metavar="name",
help="Early-load given plugin module name or entry point (multi-allowed). "
"To avoid loading of plugins, use the `no:` prefix, e.g. "
"`no:doctest`.",
"`no:doctest`. See also --disable-plugin-autoload",
Comment thread
jakkdl marked this conversation as resolved.
Outdated
)
group.addoption(
"--disable-plugin-autoload",
action="store_true",
default=False,
help="Disable plugin auto-loading through entry point packaging metadata. "
"Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.",
)
group.addoption(
"--traceconfig",
Expand Down
53 changes: 46 additions & 7 deletions testing/test_assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,40 @@ def test_foo(pytestconfig):
assert result.ret == 0

@pytest.mark.parametrize("mode", ["plain", "rewrite"])
@pytest.mark.parametrize("disable_plugin_autoload", ["env_var", "cli", ""])
@pytest.mark.parametrize("explicit_specify", ["env_var", "cli", ""])
def test_installed_plugin_rewrite(
self, pytester: Pytester, mode, monkeypatch
self,
pytester: Pytester,
mode: str,
monkeypatch: pytest.MonkeyPatch,
disable_plugin_autoload: str,
explicit_specify: str,
) -> None:
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
args = ["mainwrapper.py", "-s", f"--assert={mode}"]
if disable_plugin_autoload == "env_var":
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
elif disable_plugin_autoload == "cli":
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
args.append("--disable-plugin-autoload")
else:
assert disable_plugin_autoload == ""
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)

# FIXME: if it's already loaded then you get a ValueError: "Plugin already
# registered under a different name."
# vaguely related to https://github.com/pytest-dev/pytest/issues/5661
name = "spamplugin" if disable_plugin_autoload else "spam"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here is that when loading via entry point, the plugin can choose its own name (name = 'spam'), but when loading via other methods the name of the module is used (spamplugin).

I have simplified things here by always using spamplugin as the name of the plugin (b157cef).

# is there a single dotted name that can be used either way? idk

if explicit_specify == "env_var":
monkeypatch.setenv("PYTEST_PLUGINS", name)
elif explicit_specify == "cli":
args.append("-p")
args.append(name)
else:
assert explicit_specify == ""

# Make sure the hook is installed early enough so that plugins
# installed via distribution package are rewritten.
pytester.mkdir("hampkg")
Expand Down Expand Up @@ -275,20 +305,29 @@ def test(check_first):
check_first([10, 30], 30)

def test2(check_first2):
check_first([10, 30], 30)
check_first2([10, 30], 30)
""",
}
pytester.makepyfile(**contents)
result = pytester.run(
sys.executable, "mainwrapper.py", "-s", f"--assert={mode}"
)
result = pytester.run(sys.executable, *args)
if mode == "plain":
expected = "E AssertionError"
elif mode == "rewrite":
expected = "*assert 10 == 30*"
else:
assert 0
result.stdout.fnmatch_lines([expected])

if not disable_plugin_autoload or explicit_specify:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With all the additional logic in this test, I wonder if it would be better to have a fixture for the hampkg and split this into two distinct tests at least (autoload enabled/disabled)?

result.assert_outcomes(failed=2)
result.stdout.fnmatch_lines([expected, expected])
else:
result.assert_outcomes(errors=2)
result.stdout.fnmatch_lines(
[
"E fixture 'check_first' not found",
"E fixture 'check_first2' not found",
]
)

def test_rewrite_ast(self, pytester: Pytester) -> None:
pytester.mkdir("pkg")
Expand Down
52 changes: 40 additions & 12 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1314,14 +1314,13 @@ def distributions():
)


@pytest.mark.parametrize(
"parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)]
)
@pytest.mark.parametrize("disable_plugin_method", ["env_var", "flag", ""])
@pytest.mark.parametrize("enable_plugin_method", ["env_var", "flag", ""])
def test_disable_plugin_autoload(
pytester: Pytester,
monkeypatch: MonkeyPatch,
parse_args: tuple[str, str] | tuple[()],
should_load: bool,
enable_plugin_method: str,
disable_plugin_method: str,
) -> None:
class DummyEntryPoint:
project_name = name = "mytestplugin"
Expand All @@ -1342,23 +1341,52 @@ class PseudoPlugin:
attrs_used = []

def __getattr__(self, name):
assert name == "__loader__"
assert name in ("__loader__", "__spec__")
self.attrs_used.append(name)
return object()

def distributions():
return (Distribution(),)

monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
parse_args: list[str] = []

if disable_plugin_method == "env_var":
monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1")
elif disable_plugin_method == "flag":
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
parse_args.append("--disable-plugin-autoload")
else:
assert disable_plugin_method == ""
monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")

if enable_plugin_method == "env_var":
monkeypatch.setenv("PYTEST_PLUGINS", "mytestplugin")
elif enable_plugin_method == "flag":
parse_args.extend(["-p", "mytestplugin"])
else:
assert enable_plugin_method == ""

monkeypatch.setattr(importlib.metadata, "distributions", distributions)
monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin())
config = pytester.parseconfig(*parse_args)

has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None
assert has_loaded == should_load
if should_load:
assert PseudoPlugin.attrs_used == ["__loader__"]
else:
assert PseudoPlugin.attrs_used == []
# it should load if it's enabled, or we haven't disabled autoloading
assert has_loaded == bool(enable_plugin_method) or not disable_plugin_method
Comment thread
jakkdl marked this conversation as resolved.
Outdated

# __loader__ is accessed in mark_rewrite
# ...??
assert ("__loader__" in PseudoPlugin.attrs_used) == bool(
enable_plugin_method == "flag"
or (enable_plugin_method == "env_var" and disable_plugin_method)
)

# Config._preparse explicitly loads plugins in PYTEST_PLUGINS
# but if autoloading has been disabled it needs to inspect __spec__ when loading
assert ("__spec__" in PseudoPlugin.attrs_used) == bool(
enable_plugin_method == "env_var" and disable_plugin_method
)
# why doesn't that happen with -p? dunno


def test_plugin_loading_order(pytester: Pytester) -> None:
Expand Down