diff --git a/_pytest/config.py b/_pytest/config.py index c6a6403f6..f0551697d 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -87,9 +87,17 @@ def _prepareconfig(args=None, plugins=None): pluginmanager.ensure_shutdown() raise +def exclude_pytest_names(name): + return not name.startswith(name) or name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + class PytestPluginManager(PluginManager): - def __init__(self, hookspecs=[hookspec]): - super(PytestPluginManager, self).__init__(hookspecs=hookspecs) + def __init__(self): + super(PytestPluginManager, self).__init__(prefix="pytest_", + excludefunc=exclude_pytest_names) + self._warnings = [] + self._plugin_distinfo = [] + self.addhooks(hookspec) self.register(self) if os.environ.get('PYTEST_DEBUG'): err = sys.stderr @@ -100,6 +108,14 @@ class PytestPluginManager(PluginManager): pass self.set_tracing(err.write) + def getplugin(self, name): + if name is None: + return name + plugin = super(PytestPluginManager, self).getplugin(name) + if plugin is None: + plugin = super(PytestPluginManager, self).getplugin("_pytest." + name) + return plugin + def pytest_configure(self, config): config.addinivalue_line("markers", "tryfirst: mark a hook implementation function such that the " @@ -110,6 +126,89 @@ class PytestPluginManager(PluginManager): for warning in self._warnings: config.warn(code="I1", message=warning) + # + # API for bootstrapping plugin loading + # + # + def _envlist(self, varname): + val = os.environ.get(varname, None) + if val is not None: + return val.split(',') + return () + + def consider_env(self): + for spec in self._envlist("PYTEST_PLUGINS"): + self.import_plugin(spec) + + def consider_setuptools_entrypoints(self): + try: + from pkg_resources import iter_entry_points, DistributionNotFound + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = ep.name + if name.startswith("pytest_"): + name = name[7:] + if ep.name in self._name2plugin or name in self._name2plugin: + continue + try: + plugin = ep.load() + except DistributionNotFound: + continue + self._plugin_distinfo.append((ep.dist, plugin)) + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.consider_pluginarg(opt2) + + def consider_pluginarg(self, arg): + if arg.startswith("no:"): + name = arg[3:] + plugin = self.getplugin(name) + if plugin is not None: + self.unregister(plugin) + self._name2plugin[name] = -1 + else: + if self.getplugin(arg) is None: + self.import_plugin(arg) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__, + conftest=True): + self.consider_module(conftestmodule) + + def consider_module(self, mod): + attr = getattr(mod, "pytest_plugins", ()) + if attr: + if not isinstance(attr, (list, tuple)): + attr = (attr,) + for spec in attr: + self.import_plugin(spec) + + def import_plugin(self, modname): + assert isinstance(modname, str) + if self.getplugin(modname) is not None: + return + try: + mod = importplugin(modname) + except KeyboardInterrupt: + raise + except ImportError: + if modname.startswith("pytest_"): + return self.import_plugin(modname[7:]) + raise + except: + e = sys.exc_info()[1] + import pytest + if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): + raise + self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + self.register(mod, modname) + self.consider_module(mod) + class Parser: """ Parser for command line arguments and ini-file values. """ @@ -933,3 +1032,15 @@ def setns(obj, dic): #if obj != pytest: # pytest.__all__.append(name) setattr(pytest, name, value) + + +def importplugin(importspec): + name = importspec + try: + mod = "_pytest." + name + __import__(mod) + return sys.modules[mod] + except ImportError: + __import__(importspec) + return sys.modules[importspec] + diff --git a/_pytest/core.py b/_pytest/core.py index 4cc567aad..7d8bc5151 100644 --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,12 +1,10 @@ """ -pytest PluginManager, basic initialization and tracing. +PluginManager, basic initialization and tracing. """ import os import sys import inspect import py -# don't import pytest to avoid circular imports - py3 = sys.version_info > (3,0) @@ -137,16 +135,16 @@ class CallOutcome: class PluginManager(object): - def __init__(self, hookspecs=None, prefix="pytest_"): + def __init__(self, prefix, excludefunc=None): + self._prefix = prefix + self._excludefunc = excludefunc self._name2plugin = {} self._plugins = [] self._conftestplugins = [] self._plugin2hookcallers = {} - self._warnings = [] self.trace = TagTracer().get("pluginmanage") - self._plugin_distinfo = [] self._shutdown = [] - self.hook = HookRelay(hookspecs or [], pm=self, prefix=prefix) + self.hook = HookRelay(pm=self) def set_tracing(self, writer): self.trace.root.setwriter(writer) @@ -174,6 +172,39 @@ class PluginManager(object): assert not hasattr(self, "_registercallback") self._registercallback = callback + def make_hook_caller(self, name, plugins): + caller = getattr(self.hook, name) + methods = self.listattr(name, plugins=plugins) + if methods: + return HookCaller(self.hook, caller.name, caller.firstresult, + argnames=caller.argnames, methods=methods) + return caller + + def _scan_plugin(self, plugin): + def fail(msg, *args): + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) + + for name in dir(plugin): + if name[0] == "_" or not name.startswith(self._prefix): + continue + hook = getattr(self.hook, name, None) + method = getattr(plugin, name) + if hook is None: + if self._excludefunc is not None and self._excludefunc(name): + continue + if getattr(method, 'optionalhook', False): + continue + fail("found unknown hook: %r", name) + for arg in varnames(method): + if arg not in hook.argnames: + fail("argument %r not available\n" + "actual definition: %s\n" + "available hookargs: %s", + arg, formatdef(method), + ", ".join(hook.argnames)) + yield hook + def register(self, plugin, name=None, prepend=False, conftest=False): if self._name2plugin.get(name, None) == -1: return @@ -185,7 +216,7 @@ class PluginManager(object): reg = getattr(self, "_registercallback", None) if reg is not None: reg(plugin, name) # may call addhooks - hookcallers = list(self.hook._scan_plugin(plugin)) + hookcallers = list(self._scan_plugin(plugin)) self._plugin2hookcallers[plugin] = hookcallers self._name2plugin[name] = plugin if conftest: @@ -227,108 +258,29 @@ class PluginManager(object): return True return plugin in self._plugins or plugin in self._conftestplugins - def addhooks(self, spec, prefix="pytest_"): - self.hook._addhooks(spec, prefix=prefix) + def addhooks(self, module_or_class): + isclass = int(inspect.isclass(module_or_class)) + names = [] + for name in dir(module_or_class): + if name.startswith(self._prefix): + method = module_or_class.__dict__[name] + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self.hook, name, firstresult=firstresult, + argnames=varnames(method, startindex=isclass)) + setattr(self.hook, name, hc) + names.append(name) + if not names: + raise ValueError("did not find new %r hooks in %r" + %(self._prefix, module_or_class)) def getplugins(self): return self._plugins + self._conftestplugins - def skipifmissing(self, name): - if not self.hasplugin(name): - import pytest - pytest.skip("plugin %r is missing" % name) - def hasplugin(self, name): return bool(self.getplugin(name)) def getplugin(self, name): - if name is None: - return None - try: - return self._name2plugin[name] - except KeyError: - return self._name2plugin.get("_pytest." + name, None) - - # API for bootstrapping - # - def _envlist(self, varname): - val = os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) - - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points, DistributionNotFound - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = ep.name - if name.startswith("pytest_"): - name = name[7:] - if ep.name in self._name2plugin or name in self._name2plugin: - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - self._plugin_distinfo.append((ep.dist, plugin)) - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.consider_pluginarg(opt2) - - def consider_pluginarg(self, arg): - if arg.startswith("no:"): - name = arg[3:] - plugin = self.getplugin(name) - if plugin is not None: - self.unregister(plugin) - self._name2plugin[name] = -1 - else: - if self.getplugin(arg) is None: - self.import_plugin(arg) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__, - conftest=True): - self.consider_module(conftestmodule) - - def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) - - def import_plugin(self, modname): - assert isinstance(modname, str) - if self.getplugin(modname) is not None: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except ImportError: - if modname.startswith("pytest_"): - return self.import_plugin(modname[7:]) - raise - except: - e = sys.exc_info()[1] - import pytest - if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): - raise - self._warnings.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) + return self._name2plugin.get(name) def listattr(self, attrname, plugins=None): if plugins is None: @@ -358,16 +310,6 @@ class PluginManager(object): kwargs=kwargs, firstresult=True).execute() -def importplugin(importspec): - name = importspec - try: - mod = "_pytest." + name - __import__(mod) - return sys.modules[mod] - except ImportError: - __import__(importspec) - return sys.modules[importspec] - class MultiCall: """ execute a call into multiple python functions/methods. """ @@ -439,60 +381,9 @@ def varnames(func, startindex=None): class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] + def __init__(self, pm): self._pm = pm self.trace = pm.trace.root.get("hook") - self.prefix = prefix - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspec, prefix): - added = False - isclass = int(inspect.isclass(hookspec)) - for name, method in vars(hookspec).items(): - if name.startswith(prefix): - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult, - argnames=varnames(method, startindex=isclass)) - setattr(self, name, hc) - added = True - #print ("setting new hook", name) - if not added: - raise ValueError("did not find new %r hooks in %r" %( - prefix, hookspec,)) - - def _getcaller(self, name, plugins): - caller = getattr(self, name) - methods = self._pm.listattr(name, plugins=plugins) - if methods: - return caller.new_cached_caller(methods) - return caller - - def _scan_plugin(self, plugin): - def fail(msg, *args): - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("plugin %r\n%s" %(name, msg % args)) - - for name in dir(plugin): - if not name.startswith(self.prefix): - continue - hook = getattr(self, name, None) - method = getattr(plugin, name) - if hook is None: - is_optional = getattr(method, 'optionalhook', False) - if not isgenerichook(name) and not is_optional: - fail("found unknown hook: %r", name) - continue - for arg in varnames(method): - if arg not in hook.argnames: - fail("argument %r not available\n" - "actual definition: %s\n" - "available hookargs: %s", - arg, formatdef(method), - ", ".join(hook.argnames)) - yield hook class HookCaller: @@ -505,10 +396,6 @@ class HookCaller: assert "self" not in argnames # sanity check self.methods = methods - def new_cached_caller(self, methods): - return HookCaller(self.hookrelay, self.name, self.firstresult, - argnames=self.argnames, methods=methods) - def __repr__(self): return "" %(self.name,) @@ -529,13 +416,9 @@ class HookCaller: class PluginValidationError(Exception): """ plugin failed validation. """ -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") def formatdef(func): return "%s%s" % ( func.__name__, inspect.formatargspec(*inspect.getargspec(func)) ) - diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index 0cc59f259..d0bc33936 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -6,7 +6,7 @@ def pytest_addhooks(pluginmanager): """called at plugin load time to allow adding new hooks via a call to - pluginmanager.registerhooks(module).""" + pluginmanager.addhooks(module_or_class, prefix).""" def pytest_namespace(): diff --git a/_pytest/main.py b/_pytest/main.py index f70e06d56..1250706db 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -160,7 +160,7 @@ class FSHookProxy(object): def __getattr__(self, name): plugins = self.config._getmatchingplugins(self.fspath) - x = self.config.hook._getcaller(name, plugins) + x = self.config.pluginmanager.make_hook_caller(name, plugins) self.__dict__[name] = x return x diff --git a/testing/test_core.py b/testing/test_core.py index 51c20c871..50399919e 100644 --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,236 +1,62 @@ import pytest, py, os from _pytest.core import * # noqa -from _pytest.config import get_plugin_manager +from _pytest.config import get_plugin_manager, importplugin -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - pytest.raises(ImportError, lambda: pluginmanager.consider_env()) +@pytest.fixture +def pm(): + return PluginManager("he") - def test_preparse_args(self): - pluginmanager = PluginManager() - pytest.raises(ImportError, lambda: - pluginmanager.consider_preparse(["xyz", "-p", "hello123"])) +@pytest.fixture +def pytestpm(): + return PytestPluginManager() - def test_plugin_prevent_register(self): - pluginmanager = PluginManager() - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l1 = pluginmanager.getplugins() - pluginmanager.register(42, name="abc") - l2 = pluginmanager.getplugins() - assert len(l2) == len(l1) - def test_plugin_prevent_register_unregistered_alredy_registered(self): - pluginmanager = PluginManager() - pluginmanager.register(42, name="abc") - l1 = pluginmanager.getplugins() - assert 42 in l1 - pluginmanager.consider_preparse(["xyz", "-p", "no:abc"]) - l2 = pluginmanager.getplugins() - assert 42 not in l2 - - def test_plugin_double_register(self): - pm = PluginManager() +class TestPluginManager: + def test_plugin_double_register(self, pm): pm.register(42, name="abc") - pytest.raises(ValueError, lambda: pm.register(42, name="abc")) + with pytest.raises(ValueError): + pm.register(42, name="abc") - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(skipping1=""" - import pytest - pytest.skip("hello") - """) - p.copy(p.dirpath("skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "WI1*skipped plugin*skipping1*hello*", - "WI1*skipped plugin*skipping2*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(xy123="#") - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pluginmanager.getplugins()) - pluginmanager.consider_env() - l2 = len(pluginmanager.getplugins()) - assert l2 == l1 + 1 - assert pluginmanager.getplugin('xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = pytest.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "pytest_mytestplugin" - dist = None - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - plugin = pluginmanager.getplugin("mytestplugin") - assert plugin.x == 42 - - def test_consider_setuptools_not_installed(self, monkeypatch): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - # ok, we did not explode - - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import pytest - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') - assert plugin is not None - """) - monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) - - def test_import_plugin_importname(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("pytest_hello") - len1 = len(pluginmanager.getplugins()) - pluginmanager.import_plugin("pytest_hello") - len2 = len(pluginmanager.getplugins()) - assert len1 == len2 - plugin1 = pluginmanager.getplugin("pytest_hello") - assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("pytest_hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") - pluginname = "pkg.plug" - pluginmanager.import_plugin(pluginname) - mod = pluginmanager.getplugin("pkg.plug") - assert mod.x == 3 - - def test_consider_module(self, testdir): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_p1="#") - testdir.makepyfile(pytest_p2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_p1", "pytest_p2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("pytest_p1").__name__ == "pytest_p1" - assert pluginmanager.getplugin("pytest_p2").__name__ == "pytest_p2" - - def test_consider_module_import_module(self, testdir): - mod = py.std.types.ModuleType("x") - mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = get_plugin_manager() - reprec = testdir.make_hook_recorder(pluginmanager) - #syspath.prepend(aplugin.dirpath()) - py.std.sys.path.insert(0, str(aplugin.dirpath())) - pluginmanager.consider_module(mod) - call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__name__ == "pytest_a" - - # check that it is not registered twice - pluginmanager.consider_module(mod) - l = reprec.getcalls("pytest_plugin_registered") - assert len(l) == 1 - - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - - def test_consider_conftest_deps(self, testdir): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() - pp = PluginManager() - pytest.raises(ImportError, lambda: pp.consider_conftest(mod)) - - def test_pm(self): - pp = PluginManager() + def test_pm(self, pm): class A: pass a1, a2 = A(), A() - pp.register(a1) - assert pp.isregistered(a1) - pp.register(a2, "hello") - assert pp.isregistered(a2) - l = pp.getplugins() + pm.register(a1) + assert pm.isregistered(a1) + pm.register(a2, "hello") + assert pm.isregistered(a2) + l = pm.getplugins() assert a1 in l assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) + assert pm.getplugin('hello') == a2 + pm.unregister(a1) + assert not pm.isregistered(a1) - def test_pm_ordering(self): - pp = PluginManager() + def test_pm_ordering(self, pm): class A: pass a1, a2 = A(), A() - pp.register(a1) - pp.register(a2, "hello") - l = pp.getplugins() + pm.register(a1) + pm.register(a2, "hello") + l = pm.getplugins() assert l.index(a1) < l.index(a2) a3 = A() - pp.register(a3, prepend=True) - l = pp.getplugins() + pm.register(a3, prepend=True) + l = pm.getplugins() assert l.index(a3) == 0 - def test_register_imported_modules(self): - pp = PluginManager() - mod = py.std.types.ModuleType("x.y.pytest_hello") - pp.register(mod) - assert pp.isregistered(mod) - l = pp.getplugins() - assert mod in l - pytest.raises(ValueError, "pp.register(mod)") - pytest.raises(ValueError, lambda: pp.register(mod)) - #assert not pp.isregistered(mod2) - assert pp.getplugins() == l - - def test_canonical_import(self, monkeypatch): - mod = py.std.types.ModuleType("pytest_xyz") - monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) - pp = PluginManager() - pp.import_plugin('pytest_xyz') - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) - def test_register_mismatch_method(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_gurgel(self): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register_mismatch_arg(self): - pp = get_plugin_manager() + pm = get_plugin_manager() class hello: def pytest_configure(self, asd): pass - pytest.raises(Exception, lambda: pp.register(hello())) + pytest.raises(Exception, lambda: pm.register(hello())) def test_register(self): pm = get_plugin_manager() @@ -250,7 +76,7 @@ class TestBootstrapping: assert pm.getplugins()[-1:] == [my2] def test_listattr(self): - plugins = PluginManager() + plugins = PluginManager("xyz") class api1: x = 41 class api2: @@ -263,27 +89,6 @@ class TestBootstrapping: l = list(plugins.listattr('x')) assert l == [41, 42, 43] - def test_hook_tracing(self): - pm = get_plugin_manager() - saveindent = [] - class api1: - x = 41 - def pytest_plugin_registered(self, plugin): - saveindent.append(pm.trace.root.indent) - raise ValueError(42) - l = [] - pm.set_tracing(l.append) - indent = pm.trace.root.indent - p = api1() - pm.register(p) - - assert pm.trace.root.indent == indent - assert len(l) == 2 - assert 'pytest_plugin_registered' in l[0] - assert 'finish' in l[1] - pytest.raises(ValueError, lambda: pm.register(api1())) - assert pm.trace.root.indent == indent - assert saveindent[0] > indent class TestPytestPluginInteractions: @@ -372,10 +177,33 @@ class TestPytestPluginInteractions: config.pluginmanager.register(A()) assert len(l) == 2 + def test_hook_tracing(self): + pytestpm = get_plugin_manager() # fully initialized with plugins + saveindent = [] + class api1: + x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pytestpm.trace.root.indent) + raise ValueError(42) + l = [] + pytestpm.set_tracing(l.append) + indent = pytestpm.trace.root.indent + p = api1() + pytestpm.register(p) + + assert pytestpm.trace.root.indent == indent + assert len(l) == 2 + assert 'pytest_plugin_registered' in l[0] + assert 'finish' in l[1] + with pytest.raises(ValueError): + pytestpm.register(api1()) + assert pytestpm.trace.root.indent == indent + assert saveindent[0] > indent + # lower level API def test_listattr(self): - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") class My2: x = 42 pluginmanager.register(My2()) @@ -395,7 +223,7 @@ class TestPytestPluginInteractions: def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -572,7 +400,7 @@ class TestMultiCall: def m(self): return 19 - pluginmanager = PluginManager() + pluginmanager = PluginManager("xyz") p1 = P1() p2 = P2() p3 = P3() @@ -624,11 +452,12 @@ class TestMultiCall: class TestHookRelay: - def test_happypath(self): + def test_hapmypath(self): class Api: def hello(self, arg): "api hook 1" - pm = PluginManager([Api], prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) hook = pm.hook assert hasattr(hook, 'hello') assert repr(hook.hello).find("hello") != -1 @@ -647,7 +476,8 @@ class TestHookRelay: class Api: def hello(self, arg): "api hook 1" - pm = PluginManager(Api, prefix="he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, argwrong): return arg + 1 @@ -656,19 +486,20 @@ class TestHookRelay: assert "argwrong" in str(exc.value) def test_only_kwargs(self): - pm = PluginManager() + pm = PluginManager("he") class Api: def hello(self, arg): "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - pytest.raises(TypeError, lambda: mcm.hello(3)) + pm.addhooks(Api) + pytest.raises(TypeError, lambda: pm.hook.hello(3)) def test_firstresult_definition(self): class Api: def hello(self, arg): "api hook 1" hello.firstresult = True - pm = PluginManager([Api], "he") + pm = PluginManager("he") + pm.addhooks(Api) class Plugin: def hello(self, arg): return arg + 1 @@ -779,7 +610,7 @@ def test_importplugin_issue375(testdir): assert "aaaa" in str(excinfo.value) class TestWrapMethod: - def test_basic_happypath(self): + def test_basic_hapmypath(self): class A: def f(self): return "A.f" @@ -880,3 +711,182 @@ class TestWrapMethod: with pytest.raises(ValueError): A().error() assert l == [1] + + +### to be shifted to own test file +from _pytest.config import PytestPluginManager + +class TestPytestPluginManager: + def test_register_imported_modules(self): + pm = PytestPluginManager() + mod = py.std.types.ModuleType("x.y.pytest_hello") + pm.register(mod) + assert pm.isregistered(mod) + l = pm.getplugins() + assert mod in l + pytest.raises(ValueError, "pm.register(mod)") + pytest.raises(ValueError, lambda: pm.register(mod)) + #assert not pm.isregistered(mod2) + assert pm.getplugins() == l + + def test_canonical_import(self, monkeypatch): + mod = py.std.types.ModuleType("pytest_xyz") + monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) + pm = PytestPluginManager() + pm.import_plugin('pytest_xyz') + assert pm.getplugin('pytest_xyz') == mod + assert pm.isregistered(mod) + + def test_consider_module(self, testdir, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(pytest_p1="#") + testdir.makepyfile(pytest_p2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_p1", "pytest_p2"] + pytestpm.consider_module(mod) + assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1" + assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" + + def test_consider_module_import_module(self, testdir): + pytestpm = get_plugin_manager() + mod = py.std.types.ModuleType("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="#") + reprec = testdir.make_hook_recorder(pytestpm) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + pytestpm.consider_module(mod) + call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name) + assert call.plugin.__name__ == "pytest_a" + + # check that it is not registered twice + pytestpm.consider_module(mod) + l = reprec.getcalls("pytest_plugin_registered") + assert len(l) == 1 + + def test_consider_env_fails_to_import(self, monkeypatch, pytestpm): + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + with pytest.raises(ImportError): + pytestpm.consider_env() + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(skipping1=""" + import pytest + pytest.skip("hello") + """) + p.copy(p.dirpath("skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-rw", "-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "WI1*skipped plugin*skipping1*hello*", + "WI1*skipped plugin*skipping2*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm): + testdir.syspathinsert() + testdir.makepyfile(xy123="#") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(pytestpm.getplugins()) + pytestpm.consider_env() + l2 = len(pytestpm.getplugins()) + assert l2 == l1 + 1 + assert pytestpm.getplugin('xy123') + pytestpm.consider_env() + l3 = len(pytestpm.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm): + pkg_resources = pytest.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "pytest_mytestplugin" + dist = None + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pytestpm.consider_setuptools_entrypoints() + plugin = pytestpm.getplugin("mytestplugin") + assert plugin.x == 42 + + def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + pytestpm.consider_setuptools_entrypoints() + # ok, we did not explode + + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): + testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import pytest + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') + assert plugin is not None + """) + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") + result = testdir.runpytest(p) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwx.y")') + + testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pytestpm.import_plugin("pytest_hello") + len1 = len(pytestpm.getplugins()) + pytestpm.import_plugin("pytest_hello") + len2 = len(pytestpm.getplugins()) + assert len1 == len2 + plugin1 = pytestpm.getplugin("pytest_hello") + assert plugin1.__name__.endswith('pytest_hello') + plugin2 = pytestpm.getplugin("pytest_hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir, pytestpm): + pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwex.y")') + + testdir.syspathinsert() + testdir.mkpydir("pkg").join("plug.py").write("x=3") + pluginname = "pkg.plug" + pytestpm.import_plugin(pluginname) + mod = pytestpm.getplugin("pkg.plug") + assert mod.x == 3 + + def test_config_sets_conftesthandle_onimport(self, testdir): + config = testdir.parseconfig([]) + assert config._conftest._onimport == config._onimportconftest + + def test_consider_conftest_deps(self, testdir, pytestpm): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + with pytest.raises(ImportError): + pytestpm.consider_conftest(mod) + + +class TestPytestPluginManagerBootstrapming: + def test_preparse_args(self, pytestpm): + pytest.raises(ImportError, lambda: + pytestpm.consider_preparse(["xyz", "-p", "hello123"])) + + def test_plugin_prevent_register(self, pytestpm): + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l1 = pytestpm.getplugins() + pytestpm.register(42, name="abc") + l2 = pytestpm.getplugins() + assert len(l2) == len(l1) + + def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): + pytestpm.register(42, name="abc") + l1 = pytestpm.getplugins() + assert 42 in l1 + pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) + l2 = pytestpm.getplugins() + assert 42 not in l2 diff --git a/testing/test_pytester.py b/testing/test_pytester.py index d69809c94..3eb2b47c1 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,7 +1,7 @@ import pytest import os from _pytest.pytester import HookRecorder -from _pytest.core import PluginManager +from _pytest.config import PytestPluginManager from _pytest.main import EXIT_OK, EXIT_TESTSFAILED @@ -93,8 +93,8 @@ def make_holder(): @pytest.mark.parametrize("holder", make_holder()) def test_hookrecorder_basic(holder): - pm = PluginManager() - pm.hook._addhooks(holder, "pytest_") + pm = PytestPluginManager() + pm.addhooks(holder) rec = HookRecorder(pm) pm.hook.pytest_xyz(arg=123) call = rec.popcall("pytest_xyz") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index afb79d00c..1ad1a569f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -457,7 +457,9 @@ class TestTerminalFunctional: ]) assert result.ret == 1 - pytestconfig.pluginmanager.skipifmissing("xdist") + if not pytestconfig.pluginmanager.hasplugin("xdist"): + pytest.skip("xdist plugin not installed") + result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ "*FAIL*test_verbose_reporting.py::test_fail*",