diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 12e0d09a8be..a946660cd84 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1103,6 +1103,18 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] junit_suite_name = my_suite +.. confval:: load_entrypoint_plugins + + .. versionadded:: 4.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,anotherplugin .. confval:: log_cli_date_format diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4542f06ab7c..69d27ef8661 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,9 @@ def directory_arg(path, optname): def get_config(args=None): # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - config = Config(pluginmanager) + # XXX: storing it on the instance is required to make it available in + # is_blocked, which is the callback used with load_entrypoint_plugins. + pluginmanager.config = Config(pluginmanager) if args is not None: # Handle any "-p no:plugin" args. @@ -162,7 +164,7 @@ def get_config(args=None): for spec in default_plugins: pluginmanager.import_plugin(spec) - return config + return pluginmanager.config def get_plugin_manager(): @@ -246,6 +248,20 @@ def __init__(self): # Used to know when we are importing conftests after the pytest_configure stage self._configured = False + def is_blocked(self, name): + ret = super(PytestPluginManager, self).is_blocked(name) + if not ret: + try: + config = self.config + except AttributeError: + pass + else: + load_entrypoint_plugins = config.getini("load_entrypoint_plugins") + if load_entrypoint_plugins is not notset: + if name not in load_entrypoint_plugins: + return True + return ret + def addhooks(self, module_or_class): """ .. deprecated:: 2.8 @@ -334,6 +350,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 @@ -792,6 +816,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 @@ -810,7 +838,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.""" @@ -944,7 +973,7 @@ def _getini(self, name): if value is None: try: value = self.inicfg[name] - except KeyError: + except (AttributeError, KeyError): if default is not None: return default if type is None: diff --git a/tox.ini b/tox.ini index 16984dd43c8..8c7bd3c8989 100644 --- a/tox.ini +++ b/tox.ini @@ -140,6 +140,7 @@ commands = python scripts/release.py {posargs} [pytest] minversion = 2.0 addopts = -ra -p pytester +load_entrypoint_plugins=pytester xdist rsyncdirs = tox.ini doc src testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance