-p option now can be used to early-load plugins by entry-point name
Fixes #4718
This commit is contained in:
		
							parent
							
								
									759d7fde5d
								
							
						
					
					
						commit
						a0207274f4
					
				| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					``pluggy>=0.9`` is now required.
 | 
				
			||||||
| 
						 | 
					@ -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/>`_:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
-----------------
 | 
					-----------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										2
									
								
								setup.py
								
								
								
								
							| 
						 | 
					@ -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():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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`.",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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(
 | 
				
			||||||
            """
 | 
					            """
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue