-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
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
* `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.
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
-----------------

View File

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

View File

@ -497,7 +497,7 @@ class PytestPluginManager(PluginManager):
if not name.startswith("pytest_"):
self.set_blocked("pytest_" + name)
else:
self.import_plugin(arg)
self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(self, conftestmodule):
self.register(conftestmodule, name=conftestmodule.__file__)
@ -513,7 +513,11 @@ class PytestPluginManager(PluginManager):
for import_spec in plugins:
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",
# "terminal" or "capture". Those plugins are registered under their
# basename for historic purposes but must be imported with the
@ -524,22 +528,26 @@ class PytestPluginManager(PluginManager):
modname = str(modname)
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
return
if modname in builtin_plugins:
importspec = "_pytest." + modname
else:
importspec = modname
importspec = "_pytest." + modname if modname in builtin_plugins else modname
self.rewrite_hook.mark_rewrite(importspec)
if consider_entry_points:
loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
if loaded:
return
try:
__import__(importspec)
except ImportError as e:
new_exc_type = ImportError
new_exc_message = 'Error importing plugin "%s": %s' % (
modname,
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:
from _pytest.warnings import _issue_warning_captured

View File

@ -60,7 +60,7 @@ def pytest_addoption(parser):
dest="plugins",
default=[],
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. "
"`no:doctest`.",
)

View File

@ -8,6 +8,7 @@ import sys
import textwrap
import types
import attr
import py
import six
@ -108,6 +109,60 @@ class TestGeneralUsage(object):
assert result.ret == 0
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):
p = testdir.makepyfile(
"""

View File

@ -5,6 +5,8 @@ from __future__ import print_function
import sys
import textwrap
import attr
import _pytest._code
import pytest
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")
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):
x = 42