Skip to content
Closed
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
13 changes: 13 additions & 0 deletions doc/en/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,19 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
log_cli = True

.. confval:: load_entrypoint_plugins

.. versionadded:: 5.4

A list of pytest plugins that should be loaded via entrypoints.

By default all plugins are loaded. An empty list can be used to load none.

.. code-block:: ini

[pytest]
load_entrypoint_plugins = pytester xdist

.. confval:: log_cli_date_format


Expand Down
33 changes: 31 additions & 2 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def directory_arg(path, optname):
def get_config(args=None, plugins=None):
# subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager()
config = Config(
pluginmanager.config = config = Config(
pluginmanager,
invocation_params=Config.InvocationParams(
args=args or (), plugins=plugins, dir=Path().resolve()
Expand Down Expand Up @@ -291,6 +291,22 @@ def __init__(self):
# Used to know when we are importing conftests after the pytest_configure stage
self._configured = False

def is_blocked(self, name: str) -> bool:
ret = super(PytestPluginManager, self).is_blocked(name) # type: bool
if ret:
return ret

config = self.config
try:
load_entrypoint_plugins = config.getini("load_entrypoint_plugins")
except AttributeError: # 'Config' object has no attribute 'inicfg'
assert not hasattr(config, "inicfg")
return False
return (
load_entrypoint_plugins is not notset
and name not in load_entrypoint_plugins
)

def parse_hookimpl_opts(self, plugin, name):
# pytest hooks are always prefixed with pytest_
# so we avoid accessing possibly non-readable attributes
Expand Down Expand Up @@ -366,6 +382,14 @@ def hasplugin(self, name):
"""Return True if the plugin with the given name is registered."""
return bool(self.get_plugin(name))

def pytest_addoption(self, parser):
parser.addini(
"load_entrypoint_plugins",
help="only load specified plugins via entrypoint",
default=notset,
type="args",
)

def pytest_configure(self, config):
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
# we should remove tryfirst/trylast as markers
Expand Down Expand Up @@ -907,6 +931,10 @@ def _mark_plugins_for_rewrite(self, hook):
"""
self.pluginmanager.rewrite_hook = hook

load_entrypoint_plugins = self.getini("load_entrypoint_plugins")
if load_entrypoint_plugins is not notset and not len(load_entrypoint_plugins):
return

if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
# We don't autoload from setuptools entry points, no need to continue.
return
Expand All @@ -919,7 +947,8 @@ def _mark_plugins_for_rewrite(self, hook):
)

for name in _iter_rewritable_modules(package_files):
hook.mark_rewrite(name)
if load_entrypoint_plugins is notset or name in load_entrypoint_plugins:
hook.mark_rewrite(name)

def _validate_args(self, args, via):
"""Validate known args."""
Expand Down
9 changes: 9 additions & 0 deletions testing/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys

import pytest
from _pytest.pytester import Testdir

if sys.gettrace():

Expand Down Expand Up @@ -118,3 +119,11 @@ def runtest(self):
"""
)
testdir.makefile(".yaml", test1="")


@pytest.fixture
def testdir(testdir: Testdir) -> Testdir:
testdir.monkeypatch.setenv(
"PYTEST_ADDOPTS", "-o load_entrypoint_plugins='pytester xdist'"
)
return testdir
55 changes: 55 additions & 0 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,61 @@ def pytest_sessionstart(session):
assert result.ret == 0


@pytest.mark.parametrize(
"parse_args,should_load",
[
pytest.param(("-p", "mytestplugin"), True, id="0"),
pytest.param(
("-p", "mytestplugin", "-o", "load_entrypoint_plugins="), False, id="1"
),
pytest.param(
("-p", "mytestplugin", "-o", "load_entrypoint_plugins=mytestplugin"),
True,
id="2",
),
pytest.param(
("-p", "no:mytestplugin", "-o", "load_entrypoint_plugins=mytestplugin"),
False,
id="3",
),
],
)
def test_load_entrypoint_plugins(testdir, monkeypatch, parse_args, should_load):
loaded = False

class PseudoPlugin:
x = 42

class DummyEntryPoint:
project_name = name = "mytestplugin"
group = "pytest11"
version = "1.0"

def load(self):
nonlocal loaded
assert should_load
loaded = True
return PseudoPlugin()

class Distribution:
entry_points = (DummyEntryPoint(),)
files = ()

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

monkeypatch.setattr(importlib_metadata, "distributions", distributions)

config = testdir.parseconfig(*parse_args)
assert loaded is should_load

plugin = config.pluginmanager.get_plugin("mytestplugin")
if should_load:
assert isinstance(plugin, PseudoPlugin)
else:
assert plugin is None


def test_cmdline_processargs_simple(testdir):
testdir.makeconftest(
"""
Expand Down
14 changes: 9 additions & 5 deletions testing/test_pluginmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

@pytest.fixture
def pytestpm():
return PytestPluginManager()
class PytestPluginManagerWithConfig(PytestPluginManager):
class config:
pass

return PytestPluginManagerWithConfig()


class TestPytestPluginInteractions:
Expand Down Expand Up @@ -195,8 +199,8 @@ def test_traceback():


class TestPytestPluginManager:
def test_register_imported_modules(self):
pm = PytestPluginManager()
def test_register_imported_modules(self, pytestpm):
pm = pytestpm
mod = types.ModuleType("x.y.pytest_hello")
pm.register(mod)
assert pm.is_registered(mod)
Expand All @@ -207,10 +211,10 @@ def test_register_imported_modules(self):
# assert not pm.is_registered(mod2)
assert pm.get_plugins() == values

def test_canonical_import(self, monkeypatch):
def test_canonical_import(self, monkeypatch, pytestpm):
mod = types.ModuleType("pytest_xyz")
monkeypatch.setitem(sys.modules, "pytest_xyz", mod)
pm = PytestPluginManager()
pm = pytestpm
pm.import_plugin("pytest_xyz")
assert pm.get_plugin("pytest_xyz") == mod
assert pm.is_registered(mod)
Expand Down