Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change -p so it is possible to early load setuptools plugins #4727

Merged
merged 1 commit into from
Feb 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/4718.feature.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions changelog/4718.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``pluggy>=0.9`` is now required.
2 changes: 1 addition & 1 deletion doc/en/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/>`_:
Expand Down
16 changes: 16 additions & 0 deletions doc/en/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# 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():
Expand Down
26 changes: 17 additions & 9 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def consider_pluginarg(self, arg):
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__)
Expand All @@ -513,7 +513,11 @@ def _import_plugin_specs(self, spec):
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
Expand All @@ -524,22 +528,26 @@ def import_plugin(self, modname):
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
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/helpconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.",
)
Expand Down
55 changes: 55 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import textwrap
import types

import attr
import py
import six

Expand Down Expand Up @@ -108,6 +109,60 @@ def test_option(pytestconfig):
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(
"""
Expand Down
25 changes: 24 additions & 1 deletion testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import sys
import textwrap

import attr

import _pytest._code
import pytest
from _pytest.config import _iter_rewritable_modules
Expand Down Expand Up @@ -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
Expand Down