From a0207274f4148c28222b4fdc4466d927a2551ed6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 Feb 2019 20:59:10 -0200 Subject: [PATCH] -p option now can be used to early-load plugins by entry-point name Fixes #4718 --- changelog/4718.feature.rst | 6 ++++ changelog/4718.trivial.rst | 1 + doc/en/plugins.rst | 2 +- doc/en/usage.rst | 16 ++++++++++ setup.py | 2 +- src/_pytest/config/__init__.py | 26 ++++++++++------ src/_pytest/helpconfig.py | 2 +- testing/acceptance_test.py | 55 ++++++++++++++++++++++++++++++++++ testing/test_config.py | 25 +++++++++++++++- 9 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 changelog/4718.feature.rst create mode 100644 changelog/4718.trivial.rst diff --git a/changelog/4718.feature.rst b/changelog/4718.feature.rst new file mode 100644 index 00000000000..35d5fffb911 --- /dev/null +++ b/changelog/4718.feature.rst @@ -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 diff --git a/changelog/4718.trivial.rst b/changelog/4718.trivial.rst new file mode 100644 index 00000000000..8b4e019bc13 --- /dev/null +++ b/changelog/4718.trivial.rst @@ -0,0 +1 @@ +``pluggy>=0.9`` is now required. diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index 7c7e1132d47..e80969193ab 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -27,7 +27,7 @@ Here is a little annotated list for some popular plugins: for `twisted `_ apps, starting a reactor and processing deferreds from test functions. -* `pytest-cov `_: +* `pytest-cov `__: coverage reporting, compatible with distributed testing * `pytest-xdist `_: diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 2efb63ae2f2..52360856935 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -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 `__ plugin you can use:: + + pytest -p pytest_cov + + Disabling plugins ----------------- diff --git a/setup.py b/setup.py index b286a4f2031..d9d68c8ae79 100644 --- a/setup.py +++ b/setup.py @@ -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(): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4258032b4f1..1da72032bb0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -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__) @@ -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 @@ -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 diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index d5c4c043a93..8117ee6bcef 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -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`.", ) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 59771185fa1..17111369b51 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -8,6 +8,7 @@ import textwrap import types +import attr import py import six @@ -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( """ diff --git a/testing/test_config.py b/testing/test_config.py index 1e29b83f1b9..c5c0ca939b5 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -5,6 +5,8 @@ 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