-p option now can be used to early-load plugins by entry-point name

Fixes #4718
This commit is contained in:
Bruno Oliveira 2019-02-07 20:59:10 -02:00
parent 759d7fde5d
commit a0207274f4
9 changed files with 122 additions and 13 deletions

View File

@ -0,0 +1,6 @@
The ``-p`` option can now be used to early-load plugins also by entry-point name, instead of just
by module name.
This makes it possible to early load external plugins like ``pytest-cov`` in the command-line::
pytest -p pytest_cov

View File

@ -0,0 +1 @@
``pluggy>=0.9`` is now required.

View File

@ -27,7 +27,7 @@ Here is a little annotated list for some popular plugins:
for `twisted <http://twistedmatrix.com>`_ apps, starting a reactor and for `twisted <http://twistedmatrix.com>`_ apps, starting a reactor and
processing deferreds from test functions. processing deferreds from test functions.
* `pytest-cov <https://pypi.org/project/pytest-cov/>`_: * `pytest-cov <https://pypi.org/project/pytest-cov/>`__:
coverage reporting, compatible with distributed testing coverage reporting, compatible with distributed testing
* `pytest-xdist <https://pypi.org/project/pytest-xdist/>`_: * `pytest-xdist <https://pypi.org/project/pytest-xdist/>`_:

View File

@ -680,6 +680,22 @@ for example ``-x`` if you only want to send one particular failure.
Currently only pasting to the http://bpaste.net service is implemented. Currently only pasting to the http://bpaste.net service is implemented.
Early loading plugins
---------------------
You can early-load plugins (internal and external) explicitly in the command-line with the ``-p`` option::
pytest -p mypluginmodule
The option receives a ``name`` parameter, which can be:
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is
registered. For example to early-load the `pytest-cov <https://pypi.org/project/pytest-cov/>`__ plugin you can use::
pytest -p pytest_cov
Disabling plugins Disabling plugins
----------------- -----------------

View File

@ -22,7 +22,7 @@ INSTALL_REQUIRES = [
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
# used by tox.ini to test with pluggy master # used by tox.ini to test with pluggy master
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ: if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
INSTALL_REQUIRES.append("pluggy>=0.7") INSTALL_REQUIRES.append("pluggy>=0.9")
def main(): def main():

View File

@ -497,7 +497,7 @@ class PytestPluginManager(PluginManager):
if not name.startswith("pytest_"): if not name.startswith("pytest_"):
self.set_blocked("pytest_" + name) self.set_blocked("pytest_" + name)
else: else:
self.import_plugin(arg) self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(self, conftestmodule): def consider_conftest(self, conftestmodule):
self.register(conftestmodule, name=conftestmodule.__file__) self.register(conftestmodule, name=conftestmodule.__file__)
@ -513,7 +513,11 @@ class PytestPluginManager(PluginManager):
for import_spec in plugins: for import_spec in plugins:
self.import_plugin(import_spec) self.import_plugin(import_spec)
def import_plugin(self, modname): def import_plugin(self, modname, consider_entry_points=False):
"""
Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point
names are also considered to find a plugin.
"""
# most often modname refers to builtin modules, e.g. "pytester", # most often modname refers to builtin modules, e.g. "pytester",
# "terminal" or "capture". Those plugins are registered under their # "terminal" or "capture". Those plugins are registered under their
# basename for historic purposes but must be imported with the # basename for historic purposes but must be imported with the
@ -524,22 +528,26 @@ class PytestPluginManager(PluginManager):
modname = str(modname) modname = str(modname)
if self.is_blocked(modname) or self.get_plugin(modname) is not None: if self.is_blocked(modname) or self.get_plugin(modname) is not None:
return return
if modname in builtin_plugins:
importspec = "_pytest." + modname importspec = "_pytest." + modname if modname in builtin_plugins else modname
else:
importspec = modname
self.rewrite_hook.mark_rewrite(importspec) self.rewrite_hook.mark_rewrite(importspec)
if consider_entry_points:
loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
if loaded:
return
try: try:
__import__(importspec) __import__(importspec)
except ImportError as e: except ImportError as e:
new_exc_type = ImportError
new_exc_message = 'Error importing plugin "%s": %s' % ( new_exc_message = 'Error importing plugin "%s": %s' % (
modname, modname,
safe_str(e.args[0]), safe_str(e.args[0]),
) )
new_exc = new_exc_type(new_exc_message) new_exc = ImportError(new_exc_message)
tb = sys.exc_info()[2]
six.reraise(new_exc_type, new_exc, sys.exc_info()[2]) six.reraise(ImportError, new_exc, tb)
except Skipped as e: except Skipped as e:
from _pytest.warnings import _issue_warning_captured from _pytest.warnings import _issue_warning_captured

View File

@ -60,7 +60,7 @@ def pytest_addoption(parser):
dest="plugins", dest="plugins",
default=[], default=[],
metavar="name", metavar="name",
help="early-load given plugin (multi-allowed). " help="early-load given plugin module name or entry point (multi-allowed). "
"To avoid loading of plugins, use the `no:` prefix, e.g. " "To avoid loading of plugins, use the `no:` prefix, e.g. "
"`no:doctest`.", "`no:doctest`.",
) )

View File

@ -8,6 +8,7 @@ import sys
import textwrap import textwrap
import types import types
import attr
import py import py
import six import six
@ -108,6 +109,60 @@ class TestGeneralUsage(object):
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"]) result.stdout.fnmatch_lines(["*1 passed*"])
@pytest.mark.parametrize("load_cov_early", [True, False])
def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
pkg_resources = pytest.importorskip("pkg_resources")
testdir.makepyfile(mytestplugin1_module="")
testdir.makepyfile(mytestplugin2_module="")
testdir.makepyfile(mycov_module="")
testdir.syspathinsert()
loaded = []
@attr.s
class DummyEntryPoint(object):
name = attr.ib()
module = attr.ib()
version = "1.0"
@property
def project_name(self):
return self.name
def load(self):
__import__(self.module)
loaded.append(self.name)
return sys.modules[self.module]
@property
def dist(self):
return self
def _get_metadata(self, *args):
return []
entry_points = [
DummyEntryPoint("myplugin1", "mytestplugin1_module"),
DummyEntryPoint("myplugin2", "mytestplugin2_module"),
DummyEntryPoint("mycov", "mycov_module"),
]
def my_iter(group, name=None):
assert group == "pytest11"
for ep in entry_points:
if name is not None and ep.name != name:
continue
yield ep
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
params = ("-p", "mycov") if load_cov_early else ()
testdir.runpytest_inprocess(*params)
if load_cov_early:
assert loaded == ["mycov", "myplugin1", "myplugin2"]
else:
assert loaded == ["myplugin1", "myplugin2", "mycov"]
def test_assertion_magic(self, testdir): def test_assertion_magic(self, testdir):
p = testdir.makepyfile( p = testdir.makepyfile(
""" """

View File

@ -5,6 +5,8 @@ from __future__ import print_function
import sys import sys
import textwrap import textwrap
import attr
import _pytest._code import _pytest._code
import pytest import pytest
from _pytest.config import _iter_rewritable_modules from _pytest.config import _iter_rewritable_modules
@ -622,7 +624,28 @@ def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load):
pkg_resources = pytest.importorskip("pkg_resources") pkg_resources = pytest.importorskip("pkg_resources")
def my_iter(group, name=None): def my_iter(group, name=None):
raise AssertionError("Should not be called") assert group == "pytest11"
assert name == "mytestplugin"
return iter([DummyEntryPoint()])
@attr.s
class DummyEntryPoint(object):
name = "mytestplugin"
version = "1.0"
@property
def project_name(self):
return self.name
def load(self):
return sys.modules[self.name]
@property
def dist(self):
return self
def _get_metadata(self, *args):
return []
class PseudoPlugin(object): class PseudoPlugin(object):
x = 42 x = 42