From c9edaaa3ba2e11053326de33a7a5875f667c33ca Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 9 Dec 2018 12:56:11 +0100 Subject: [PATCH 1/2] Add load_entrypoint_plugins ini option/marker --- doc/en/reference.rst | 13 ++++++++ src/_pytest/config/__init__.py | 33 ++++++++++++++++++-- testing/test_config.py | 55 ++++++++++++++++++++++++++++++++++ testing/test_pluginmanager.py | 14 +++++---- 4 files changed, 108 insertions(+), 7 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 50e32d660a2..d02cc09e595 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -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 diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ed3334e5fc4..f4b84257f32 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -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() @@ -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 @@ -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 @@ -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 @@ -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.""" diff --git a/testing/test_config.py b/testing/test_config.py index cc54e5b2363..c3c548b26cb 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -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( """ diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 5ddb3e41aa9..c1a82c8cd82 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -11,7 +11,11 @@ @pytest.fixture def pytestpm(): - return PytestPluginManager() + class PytestPluginManagerWithConfig(PytestPluginManager): + class config: + pass + + return PytestPluginManagerWithConfig() class TestPytestPluginInteractions: @@ -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) @@ -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) From b9889d0e3bab34a6cf94512d311530da08a4d52a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 2 Feb 2020 00:19:32 +0100 Subject: [PATCH 2/2] testing/conftest.py: only use pytester and xdist I.e. not e.g. pytest-randomly. Fixes https://github.com/pytest-dev/pytest/issues/4351. --- testing/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing/conftest.py b/testing/conftest.py index 33b817a1226..cfef65c3344 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,6 +1,7 @@ import sys import pytest +from _pytest.pytester import Testdir if sys.gettrace(): @@ -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