Skip to content

Commit 2cf1de3

Browse files
authored
Merge pull request #4727 from nicoddemus/early-load-4718
Change -p so it is possible to early load setuptools plugins
2 parents c9e6943 + a020727 commit 2cf1de3

File tree

9 files changed

+122
-13
lines changed

9 files changed

+122
-13
lines changed

changelog/4718.feature.rst

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

changelog/4718.trivial.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``pluggy>=0.9`` is now required.

doc/en/plugins.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Here is a little annotated list for some popular plugins:
2727
for `twisted <http://twistedmatrix.com>`_ apps, starting a reactor and
2828
processing deferreds from test functions.
2929

30-
* `pytest-cov <https://pypi.org/project/pytest-cov/>`_:
30+
* `pytest-cov <https://pypi.org/project/pytest-cov/>`__:
3131
coverage reporting, compatible with distributed testing
3232

3333
* `pytest-xdist <https://pypi.org/project/pytest-xdist/>`_:

doc/en/usage.rst

+16
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,22 @@ for example ``-x`` if you only want to send one particular failure.
680680
681681
Currently only pasting to the http://bpaste.net service is implemented.
682682

683+
Early loading plugins
684+
---------------------
685+
686+
You can early-load plugins (internal and external) explicitly in the command-line with the ``-p`` option::
687+
688+
pytest -p mypluginmodule
689+
690+
The option receives a ``name`` parameter, which can be:
691+
692+
* A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable.
693+
* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is
694+
registered. For example to early-load the `pytest-cov <https://pypi.org/project/pytest-cov/>`__ plugin you can use::
695+
696+
pytest -p pytest_cov
697+
698+
683699
Disabling plugins
684700
-----------------
685701

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
2323
# used by tox.ini to test with pluggy master
2424
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
25-
INSTALL_REQUIRES.append("pluggy>=0.7")
25+
INSTALL_REQUIRES.append("pluggy>=0.9")
2626

2727

2828
def main():

src/_pytest/config/__init__.py

+17-9
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ def consider_pluginarg(self, arg):
497497
if not name.startswith("pytest_"):
498498
self.set_blocked("pytest_" + name)
499499
else:
500-
self.import_plugin(arg)
500+
self.import_plugin(arg, consider_entry_points=True)
501501

502502
def consider_conftest(self, conftestmodule):
503503
self.register(conftestmodule, name=conftestmodule.__file__)
@@ -513,7 +513,11 @@ def _import_plugin_specs(self, spec):
513513
for import_spec in plugins:
514514
self.import_plugin(import_spec)
515515

516-
def import_plugin(self, modname):
516+
def import_plugin(self, modname, consider_entry_points=False):
517+
"""
518+
Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point
519+
names are also considered to find a plugin.
520+
"""
517521
# most often modname refers to builtin modules, e.g. "pytester",
518522
# "terminal" or "capture". Those plugins are registered under their
519523
# basename for historic purposes but must be imported with the
@@ -524,22 +528,26 @@ def import_plugin(self, modname):
524528
modname = str(modname)
525529
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
526530
return
527-
if modname in builtin_plugins:
528-
importspec = "_pytest." + modname
529-
else:
530-
importspec = modname
531+
532+
importspec = "_pytest." + modname if modname in builtin_plugins else modname
531533
self.rewrite_hook.mark_rewrite(importspec)
534+
535+
if consider_entry_points:
536+
loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
537+
if loaded:
538+
return
539+
532540
try:
533541
__import__(importspec)
534542
except ImportError as e:
535-
new_exc_type = ImportError
536543
new_exc_message = 'Error importing plugin "%s": %s' % (
537544
modname,
538545
safe_str(e.args[0]),
539546
)
540-
new_exc = new_exc_type(new_exc_message)
547+
new_exc = ImportError(new_exc_message)
548+
tb = sys.exc_info()[2]
541549

542-
six.reraise(new_exc_type, new_exc, sys.exc_info()[2])
550+
six.reraise(ImportError, new_exc, tb)
543551

544552
except Skipped as e:
545553
from _pytest.warnings import _issue_warning_captured

src/_pytest/helpconfig.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def pytest_addoption(parser):
6060
dest="plugins",
6161
default=[],
6262
metavar="name",
63-
help="early-load given plugin (multi-allowed). "
63+
help="early-load given plugin module name or entry point (multi-allowed). "
6464
"To avoid loading of plugins, use the `no:` prefix, e.g. "
6565
"`no:doctest`.",
6666
)

testing/acceptance_test.py

+55
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import textwrap
99
import types
1010

11+
import attr
1112
import py
1213
import six
1314

@@ -108,6 +109,60 @@ def test_option(pytestconfig):
108109
assert result.ret == 0
109110
result.stdout.fnmatch_lines(["*1 passed*"])
110111

112+
@pytest.mark.parametrize("load_cov_early", [True, False])
113+
def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early):
114+
pkg_resources = pytest.importorskip("pkg_resources")
115+
116+
testdir.makepyfile(mytestplugin1_module="")
117+
testdir.makepyfile(mytestplugin2_module="")
118+
testdir.makepyfile(mycov_module="")
119+
testdir.syspathinsert()
120+
121+
loaded = []
122+
123+
@attr.s
124+
class DummyEntryPoint(object):
125+
name = attr.ib()
126+
module = attr.ib()
127+
version = "1.0"
128+
129+
@property
130+
def project_name(self):
131+
return self.name
132+
133+
def load(self):
134+
__import__(self.module)
135+
loaded.append(self.name)
136+
return sys.modules[self.module]
137+
138+
@property
139+
def dist(self):
140+
return self
141+
142+
def _get_metadata(self, *args):
143+
return []
144+
145+
entry_points = [
146+
DummyEntryPoint("myplugin1", "mytestplugin1_module"),
147+
DummyEntryPoint("myplugin2", "mytestplugin2_module"),
148+
DummyEntryPoint("mycov", "mycov_module"),
149+
]
150+
151+
def my_iter(group, name=None):
152+
assert group == "pytest11"
153+
for ep in entry_points:
154+
if name is not None and ep.name != name:
155+
continue
156+
yield ep
157+
158+
monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter)
159+
params = ("-p", "mycov") if load_cov_early else ()
160+
testdir.runpytest_inprocess(*params)
161+
if load_cov_early:
162+
assert loaded == ["mycov", "myplugin1", "myplugin2"]
163+
else:
164+
assert loaded == ["myplugin1", "myplugin2", "mycov"]
165+
111166
def test_assertion_magic(self, testdir):
112167
p = testdir.makepyfile(
113168
"""

testing/test_config.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import sys
66
import textwrap
77

8+
import attr
9+
810
import _pytest._code
911
import pytest
1012
from _pytest.config import _iter_rewritable_modules
@@ -622,7 +624,28 @@ def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load):
622624
pkg_resources = pytest.importorskip("pkg_resources")
623625

624626
def my_iter(group, name=None):
625-
raise AssertionError("Should not be called")
627+
assert group == "pytest11"
628+
assert name == "mytestplugin"
629+
return iter([DummyEntryPoint()])
630+
631+
@attr.s
632+
class DummyEntryPoint(object):
633+
name = "mytestplugin"
634+
version = "1.0"
635+
636+
@property
637+
def project_name(self):
638+
return self.name
639+
640+
def load(self):
641+
return sys.modules[self.name]
642+
643+
@property
644+
def dist(self):
645+
return self
646+
647+
def _get_metadata(self, *args):
648+
return []
626649

627650
class PseudoPlugin(object):
628651
x = 42

0 commit comments

Comments
 (0)