Merged in hpk42/pytest-patches/more_plugin (pull request #282)
another major pluginmanager refactor and docs
This commit is contained in:
commit
2d8f115d8c
17
CHANGELOG
17
CHANGELOG
|
@ -26,6 +26,23 @@
|
||||||
change but it might still break 3rd party plugins which relied on
|
change but it might still break 3rd party plugins which relied on
|
||||||
details like especially the pluginmanager.add_shutdown() API.
|
details like especially the pluginmanager.add_shutdown() API.
|
||||||
Thanks Holger Krekel.
|
Thanks Holger Krekel.
|
||||||
|
|
||||||
|
- pluginmanagement: introduce ``pytest.hookimpl_opts`` and
|
||||||
|
``pytest.hookspec_opts`` decorators for setting impl/spec
|
||||||
|
specific parameters. This substitutes the previous
|
||||||
|
now deprecated use of ``pytest.mark`` which is meant to
|
||||||
|
contain markers for test functions only.
|
||||||
|
|
||||||
|
- write/refine docs for "writing plugins" which now have their
|
||||||
|
own page and are separate from the "using/installing plugins`` page.
|
||||||
|
|
||||||
|
- fix issue732: properly unregister plugins from any hook calling
|
||||||
|
sites allowing to have temporary plugins during test execution.
|
||||||
|
|
||||||
|
- deprecate and warn about ``__multicall__`` argument in hook
|
||||||
|
implementations. Use the ``hookwrapper`` mechanism instead already
|
||||||
|
introduced with pytest-2.7.
|
||||||
|
|
||||||
|
|
||||||
2.7.1.dev (compared to 2.7.0)
|
2.7.1.dev (compared to 2.7.0)
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
|
@ -29,7 +29,7 @@ def pytest_addoption(parser):
|
||||||
help="shortcut for --capture=no.")
|
help="shortcut for --capture=no.")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_load_initial_conftests(early_config, parser, args):
|
def pytest_load_initial_conftests(early_config, parser, args):
|
||||||
ns = early_config.known_args_namespace
|
ns = early_config.known_args_namespace
|
||||||
pluginmanager = early_config.pluginmanager
|
pluginmanager = early_config.pluginmanager
|
||||||
|
@ -101,7 +101,7 @@ class CaptureManager:
|
||||||
if capfuncarg is not None:
|
if capfuncarg is not None:
|
||||||
capfuncarg.close()
|
capfuncarg.close()
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_make_collect_report(self, collector):
|
def pytest_make_collect_report(self, collector):
|
||||||
if isinstance(collector, pytest.File):
|
if isinstance(collector, pytest.File):
|
||||||
self.resumecapture()
|
self.resumecapture()
|
||||||
|
@ -115,13 +115,13 @@ class CaptureManager:
|
||||||
else:
|
else:
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_runtest_setup(self, item):
|
def pytest_runtest_setup(self, item):
|
||||||
self.resumecapture()
|
self.resumecapture()
|
||||||
yield
|
yield
|
||||||
self.suspendcapture_item(item, "setup")
|
self.suspendcapture_item(item, "setup")
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_runtest_call(self, item):
|
def pytest_runtest_call(self, item):
|
||||||
self.resumecapture()
|
self.resumecapture()
|
||||||
self.activate_funcargs(item)
|
self.activate_funcargs(item)
|
||||||
|
@ -129,17 +129,17 @@ class CaptureManager:
|
||||||
#self.deactivate_funcargs() called from suspendcapture()
|
#self.deactivate_funcargs() called from suspendcapture()
|
||||||
self.suspendcapture_item(item, "call")
|
self.suspendcapture_item(item, "call")
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_runtest_teardown(self, item):
|
def pytest_runtest_teardown(self, item):
|
||||||
self.resumecapture()
|
self.resumecapture()
|
||||||
yield
|
yield
|
||||||
self.suspendcapture_item(item, "teardown")
|
self.suspendcapture_item(item, "teardown")
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_keyboard_interrupt(self, excinfo):
|
def pytest_keyboard_interrupt(self, excinfo):
|
||||||
self.reset_capturings()
|
self.reset_capturings()
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_internalerror(self, excinfo):
|
def pytest_internalerror(self, excinfo):
|
||||||
self.reset_capturings()
|
self.reset_capturings()
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import py
|
||||||
# DON't import pytest here because it causes import cycle troubles
|
# DON't import pytest here because it causes import cycle troubles
|
||||||
import sys, os
|
import sys, os
|
||||||
from _pytest import hookspec # the extension point definitions
|
from _pytest import hookspec # the extension point definitions
|
||||||
from _pytest.core import PluginManager
|
from _pytest.core import PluginManager, hookimpl_opts, varnames
|
||||||
|
|
||||||
# pytest startup
|
# pytest startup
|
||||||
#
|
#
|
||||||
|
@ -38,6 +38,7 @@ def main(args=None, plugins=None):
|
||||||
tw.line("ERROR: could not load %s\n" % (e.path), red=True)
|
tw.line("ERROR: could not load %s\n" % (e.path), red=True)
|
||||||
return 4
|
return 4
|
||||||
else:
|
else:
|
||||||
|
config.pluginmanager.check_pending()
|
||||||
return config.hook.pytest_cmdline_main(config=config)
|
return config.hook.pytest_cmdline_main(config=config)
|
||||||
|
|
||||||
class cmdline: # compatibility namespace
|
class cmdline: # compatibility namespace
|
||||||
|
@ -59,17 +60,17 @@ builtin_plugins.add("pytester")
|
||||||
|
|
||||||
def _preloadplugins():
|
def _preloadplugins():
|
||||||
assert not _preinit
|
assert not _preinit
|
||||||
_preinit.append(get_plugin_manager())
|
_preinit.append(get_config())
|
||||||
|
|
||||||
def get_plugin_manager():
|
def get_config():
|
||||||
if _preinit:
|
if _preinit:
|
||||||
return _preinit.pop(0)
|
return _preinit.pop(0)
|
||||||
# subsequent calls to main will create a fresh instance
|
# subsequent calls to main will create a fresh instance
|
||||||
pluginmanager = PytestPluginManager()
|
pluginmanager = PytestPluginManager()
|
||||||
pluginmanager.config = Config(pluginmanager) # XXX attr needed?
|
config = Config(pluginmanager)
|
||||||
for spec in default_plugins:
|
for spec in default_plugins:
|
||||||
pluginmanager.import_plugin(spec)
|
pluginmanager.import_plugin(spec)
|
||||||
return pluginmanager
|
return config
|
||||||
|
|
||||||
def _prepareconfig(args=None, plugins=None):
|
def _prepareconfig(args=None, plugins=None):
|
||||||
if args is None:
|
if args is None:
|
||||||
|
@ -80,7 +81,7 @@ def _prepareconfig(args=None, plugins=None):
|
||||||
if not isinstance(args, str):
|
if not isinstance(args, str):
|
||||||
raise ValueError("not a string or argument list: %r" % (args,))
|
raise ValueError("not a string or argument list: %r" % (args,))
|
||||||
args = shlex.split(args)
|
args = shlex.split(args)
|
||||||
pluginmanager = get_plugin_manager()
|
pluginmanager = get_config().pluginmanager
|
||||||
if plugins:
|
if plugins:
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
pluginmanager.register(plugin)
|
pluginmanager.register(plugin)
|
||||||
|
@ -97,8 +98,7 @@ class PytestPluginManager(PluginManager):
|
||||||
super(PytestPluginManager, self).__init__(prefix="pytest_",
|
super(PytestPluginManager, self).__init__(prefix="pytest_",
|
||||||
excludefunc=exclude_pytest_names)
|
excludefunc=exclude_pytest_names)
|
||||||
self._warnings = []
|
self._warnings = []
|
||||||
self._plugin_distinfo = []
|
self._conftest_plugins = set()
|
||||||
self._globalplugins = []
|
|
||||||
|
|
||||||
# state related to local conftest plugins
|
# state related to local conftest plugins
|
||||||
self._path2confmods = {}
|
self._path2confmods = {}
|
||||||
|
@ -114,28 +114,35 @@ class PytestPluginManager(PluginManager):
|
||||||
err = py.io.dupfile(err, encoding=encoding)
|
err = py.io.dupfile(err, encoding=encoding)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.set_tracing(err.write)
|
self.trace.root.setwriter(err.write)
|
||||||
|
self.enable_tracing()
|
||||||
|
|
||||||
def register(self, plugin, name=None, conftest=False):
|
|
||||||
|
def _verify_hook(self, hook, plugin):
|
||||||
|
super(PytestPluginManager, self)._verify_hook(hook, plugin)
|
||||||
|
method = getattr(plugin, hook.name)
|
||||||
|
if "__multicall__" in varnames(method):
|
||||||
|
fslineno = py.code.getfslineno(method)
|
||||||
|
warning = dict(code="I1",
|
||||||
|
fslocation=fslineno,
|
||||||
|
message="%r hook uses deprecated __multicall__ "
|
||||||
|
"argument" % (hook.name))
|
||||||
|
self._warnings.append(warning)
|
||||||
|
|
||||||
|
def register(self, plugin, name=None):
|
||||||
ret = super(PytestPluginManager, self).register(plugin, name)
|
ret = super(PytestPluginManager, self).register(plugin, name)
|
||||||
if ret and not conftest:
|
if ret:
|
||||||
self._globalplugins.append(plugin)
|
self.hook.pytest_plugin_registered.call_historic(
|
||||||
|
kwargs=dict(plugin=plugin, manager=self))
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _do_register(self, plugin, name):
|
def getplugin(self, name):
|
||||||
# called from core PluginManager class
|
# support deprecated naming because plugins (xdist e.g.) use it
|
||||||
if hasattr(self, "config"):
|
return self.get_plugin(name)
|
||||||
self.config._register_plugin(plugin, name)
|
|
||||||
return super(PytestPluginManager, self)._do_register(plugin, name)
|
|
||||||
|
|
||||||
def unregister(self, plugin):
|
|
||||||
super(PytestPluginManager, self).unregister(plugin)
|
|
||||||
try:
|
|
||||||
self._globalplugins.remove(plugin)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def pytest_configure(self, config):
|
def pytest_configure(self, config):
|
||||||
|
# XXX now that the pluginmanager exposes hookimpl_opts(tryfirst...)
|
||||||
|
# we should remove tryfirst/trylast as markers
|
||||||
config.addinivalue_line("markers",
|
config.addinivalue_line("markers",
|
||||||
"tryfirst: mark a hook implementation function such that the "
|
"tryfirst: mark a hook implementation function such that the "
|
||||||
"plugin machinery will try to call it first/as early as possible.")
|
"plugin machinery will try to call it first/as early as possible.")
|
||||||
|
@ -143,7 +150,10 @@ class PytestPluginManager(PluginManager):
|
||||||
"trylast: mark a hook implementation function such that the "
|
"trylast: mark a hook implementation function such that the "
|
||||||
"plugin machinery will try to call it last/as late as possible.")
|
"plugin machinery will try to call it last/as late as possible.")
|
||||||
for warning in self._warnings:
|
for warning in self._warnings:
|
||||||
config.warn(code="I1", message=warning)
|
if isinstance(warning, dict):
|
||||||
|
config.warn(**warning)
|
||||||
|
else:
|
||||||
|
config.warn(code="I1", message=warning)
|
||||||
|
|
||||||
#
|
#
|
||||||
# internal API for local conftest plugin handling
|
# internal API for local conftest plugin handling
|
||||||
|
@ -186,14 +196,21 @@ class PytestPluginManager(PluginManager):
|
||||||
try:
|
try:
|
||||||
return self._path2confmods[path]
|
return self._path2confmods[path]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
clist = []
|
if path.isfile():
|
||||||
for parent in path.parts():
|
clist = self._getconftestmodules(path.dirpath())
|
||||||
if self._confcutdir and self._confcutdir.relto(parent):
|
else:
|
||||||
continue
|
# XXX these days we may rather want to use config.rootdir
|
||||||
conftestpath = parent.join("conftest.py")
|
# and allow users to opt into looking into the rootdir parent
|
||||||
if conftestpath.check(file=1):
|
# directories instead of requiring to specify confcutdir
|
||||||
mod = self._importconftest(conftestpath)
|
clist = []
|
||||||
clist.append(mod)
|
for parent in path.parts():
|
||||||
|
if self._confcutdir and self._confcutdir.relto(parent):
|
||||||
|
continue
|
||||||
|
conftestpath = parent.join("conftest.py")
|
||||||
|
if conftestpath.isfile():
|
||||||
|
mod = self._importconftest(conftestpath)
|
||||||
|
clist.append(mod)
|
||||||
|
|
||||||
self._path2confmods[path] = clist
|
self._path2confmods[path] = clist
|
||||||
return clist
|
return clist
|
||||||
|
|
||||||
|
@ -217,6 +234,8 @@ class PytestPluginManager(PluginManager):
|
||||||
mod = conftestpath.pyimport()
|
mod = conftestpath.pyimport()
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ConftestImportFailure(conftestpath, sys.exc_info())
|
raise ConftestImportFailure(conftestpath, sys.exc_info())
|
||||||
|
|
||||||
|
self._conftest_plugins.add(mod)
|
||||||
self._conftestpath2mod[conftestpath] = mod
|
self._conftestpath2mod[conftestpath] = mod
|
||||||
dirpath = conftestpath.dirpath()
|
dirpath = conftestpath.dirpath()
|
||||||
if dirpath in self._path2confmods:
|
if dirpath in self._path2confmods:
|
||||||
|
@ -233,24 +252,6 @@ class PytestPluginManager(PluginManager):
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
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):
|
def consider_preparse(self, args):
|
||||||
for opt1,opt2 in zip(args, args[1:]):
|
for opt1,opt2 in zip(args, args[1:]):
|
||||||
if opt1 == "-p":
|
if opt1 == "-p":
|
||||||
|
@ -258,18 +259,12 @@ class PytestPluginManager(PluginManager):
|
||||||
|
|
||||||
def consider_pluginarg(self, arg):
|
def consider_pluginarg(self, arg):
|
||||||
if arg.startswith("no:"):
|
if arg.startswith("no:"):
|
||||||
name = arg[3:]
|
self.set_blocked(arg[3:])
|
||||||
plugin = self.getplugin(name)
|
|
||||||
if plugin is not None:
|
|
||||||
self.unregister(plugin)
|
|
||||||
self._name2plugin[name] = -1
|
|
||||||
else:
|
else:
|
||||||
if self.getplugin(arg) is None:
|
self.import_plugin(arg)
|
||||||
self.import_plugin(arg)
|
|
||||||
|
|
||||||
def consider_conftest(self, conftestmodule):
|
def consider_conftest(self, conftestmodule):
|
||||||
if self.register(conftestmodule, name=conftestmodule.__file__,
|
if self.register(conftestmodule, name=conftestmodule.__file__):
|
||||||
conftest=True):
|
|
||||||
self.consider_module(conftestmodule)
|
self.consider_module(conftestmodule)
|
||||||
|
|
||||||
def consider_env(self):
|
def consider_env(self):
|
||||||
|
@ -291,7 +286,7 @@ class PytestPluginManager(PluginManager):
|
||||||
# basename for historic purposes but must be imported with the
|
# basename for historic purposes but must be imported with the
|
||||||
# _pytest prefix.
|
# _pytest prefix.
|
||||||
assert isinstance(modname, str)
|
assert isinstance(modname, str)
|
||||||
if self.getplugin(modname) is not None:
|
if self.get_plugin(modname) is not None:
|
||||||
return
|
return
|
||||||
if modname in builtin_plugins:
|
if modname in builtin_plugins:
|
||||||
importspec = "_pytest." + modname
|
importspec = "_pytest." + modname
|
||||||
|
@ -685,6 +680,7 @@ class Notset:
|
||||||
|
|
||||||
notset = Notset()
|
notset = Notset()
|
||||||
FILE_OR_DIR = 'file_or_dir'
|
FILE_OR_DIR = 'file_or_dir'
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
""" access to configuration values, pluginmanager and plugin hooks. """
|
""" access to configuration values, pluginmanager and plugin hooks. """
|
||||||
|
|
||||||
|
@ -706,20 +702,11 @@ class Config(object):
|
||||||
self._cleanup = []
|
self._cleanup = []
|
||||||
self.pluginmanager.register(self, "pytestconfig")
|
self.pluginmanager.register(self, "pytestconfig")
|
||||||
self._configured = False
|
self._configured = False
|
||||||
|
def do_setns(dic):
|
||||||
def _register_plugin(self, plugin, name):
|
|
||||||
call_plugin = self.pluginmanager.call_plugin
|
|
||||||
call_plugin(plugin, "pytest_addhooks",
|
|
||||||
{'pluginmanager': self.pluginmanager})
|
|
||||||
self.hook.pytest_plugin_registered(plugin=plugin,
|
|
||||||
manager=self.pluginmanager)
|
|
||||||
dic = call_plugin(plugin, "pytest_namespace", {}) or {}
|
|
||||||
if dic:
|
|
||||||
import pytest
|
import pytest
|
||||||
setns(pytest, dic)
|
setns(pytest, dic)
|
||||||
call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
|
self.hook.pytest_namespace.call_historic(do_setns, {})
|
||||||
if self._configured:
|
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
|
||||||
call_plugin(plugin, "pytest_configure", {'config': self})
|
|
||||||
|
|
||||||
def add_cleanup(self, func):
|
def add_cleanup(self, func):
|
||||||
""" Add a function to be called when the config object gets out of
|
""" Add a function to be called when the config object gets out of
|
||||||
|
@ -729,26 +716,27 @@ class Config(object):
|
||||||
def _do_configure(self):
|
def _do_configure(self):
|
||||||
assert not self._configured
|
assert not self._configured
|
||||||
self._configured = True
|
self._configured = True
|
||||||
self.hook.pytest_configure(config=self)
|
self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
|
||||||
|
|
||||||
def _ensure_unconfigure(self):
|
def _ensure_unconfigure(self):
|
||||||
if self._configured:
|
if self._configured:
|
||||||
self._configured = False
|
self._configured = False
|
||||||
self.hook.pytest_unconfigure(config=self)
|
self.hook.pytest_unconfigure(config=self)
|
||||||
|
self.hook.pytest_configure._call_history = []
|
||||||
while self._cleanup:
|
while self._cleanup:
|
||||||
fin = self._cleanup.pop()
|
fin = self._cleanup.pop()
|
||||||
fin()
|
fin()
|
||||||
|
|
||||||
def warn(self, code, message):
|
def warn(self, code, message, fslocation=None):
|
||||||
""" generate a warning for this test session. """
|
""" generate a warning for this test session. """
|
||||||
self.hook.pytest_logwarning(code=code, message=message,
|
self.hook.pytest_logwarning(code=code, message=message,
|
||||||
fslocation=None, nodeid=None)
|
fslocation=fslocation, nodeid=None)
|
||||||
|
|
||||||
def get_terminal_writer(self):
|
def get_terminal_writer(self):
|
||||||
return self.pluginmanager.getplugin("terminalreporter")._tw
|
return self.pluginmanager.get_plugin("terminalreporter")._tw
|
||||||
|
|
||||||
def pytest_cmdline_parse(self, pluginmanager, args):
|
def pytest_cmdline_parse(self, pluginmanager, args):
|
||||||
assert self == pluginmanager.config, (self, pluginmanager.config)
|
# REF1 assert self == pluginmanager.config, (self, pluginmanager.config)
|
||||||
self.parse(args)
|
self.parse(args)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -778,8 +766,7 @@ class Config(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromdictargs(cls, option_dict, args):
|
def fromdictargs(cls, option_dict, args):
|
||||||
""" constructor useable for subprocesses. """
|
""" constructor useable for subprocesses. """
|
||||||
pluginmanager = get_plugin_manager()
|
config = get_config()
|
||||||
config = pluginmanager.config
|
|
||||||
config._preparse(args, addopts=False)
|
config._preparse(args, addopts=False)
|
||||||
config.option.__dict__.update(option_dict)
|
config.option.__dict__.update(option_dict)
|
||||||
for x in config.option.plugins:
|
for x in config.option.plugins:
|
||||||
|
@ -794,13 +781,9 @@ class Config(object):
|
||||||
if not hasattr(self.option, opt.dest):
|
if not hasattr(self.option, opt.dest):
|
||||||
setattr(self.option, opt.dest, opt.default)
|
setattr(self.option, opt.dest, opt.default)
|
||||||
|
|
||||||
def _getmatchingplugins(self, fspath):
|
@hookimpl_opts(trylast=True)
|
||||||
return self.pluginmanager._globalplugins + \
|
|
||||||
self.pluginmanager._getconftestmodules(fspath)
|
|
||||||
|
|
||||||
def pytest_load_initial_conftests(self, early_config):
|
def pytest_load_initial_conftests(self, early_config):
|
||||||
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
|
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
|
||||||
pytest_load_initial_conftests.trylast = True
|
|
||||||
|
|
||||||
def _initini(self, args):
|
def _initini(self, args):
|
||||||
parsed_args = self._parser.parse_known_args(args)
|
parsed_args = self._parser.parse_known_args(args)
|
||||||
|
@ -817,7 +800,10 @@ class Config(object):
|
||||||
args[:] = self.getini("addopts") + args
|
args[:] = self.getini("addopts") + args
|
||||||
self._checkversion()
|
self._checkversion()
|
||||||
self.pluginmanager.consider_preparse(args)
|
self.pluginmanager.consider_preparse(args)
|
||||||
self.pluginmanager.consider_setuptools_entrypoints()
|
try:
|
||||||
|
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||||
|
except ImportError as e:
|
||||||
|
self.warn("I2", "could not load setuptools entry import: %s" % (e,))
|
||||||
self.pluginmanager.consider_env()
|
self.pluginmanager.consider_env()
|
||||||
self.known_args_namespace = ns = self._parser.parse_known_args(args)
|
self.known_args_namespace = ns = self._parser.parse_known_args(args)
|
||||||
try:
|
try:
|
||||||
|
@ -850,6 +836,8 @@ class Config(object):
|
||||||
assert not hasattr(self, 'args'), (
|
assert not hasattr(self, 'args'), (
|
||||||
"can only parse cmdline args at most once per Config object")
|
"can only parse cmdline args at most once per Config object")
|
||||||
self._origargs = args
|
self._origargs = args
|
||||||
|
self.hook.pytest_addhooks.call_historic(
|
||||||
|
kwargs=dict(pluginmanager=self.pluginmanager))
|
||||||
self._preparse(args)
|
self._preparse(args)
|
||||||
# XXX deprecated hook:
|
# XXX deprecated hook:
|
||||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||||
|
|
523
_pytest/core.py
523
_pytest/core.py
|
@ -2,11 +2,65 @@
|
||||||
PluginManager, basic initialization and tracing.
|
PluginManager, basic initialization and tracing.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
from inspect import isfunction, ismethod, isclass, formatargspec, getargspec
|
||||||
import py
|
import py
|
||||||
|
|
||||||
py3 = sys.version_info > (3,0)
|
py3 = sys.version_info > (3,0)
|
||||||
|
|
||||||
|
def hookspec_opts(firstresult=False, historic=False):
|
||||||
|
""" returns a decorator which will define a function as a hook specfication.
|
||||||
|
|
||||||
|
If firstresult is True the 1:N hook call (N being the number of registered
|
||||||
|
hook implementation functions) will stop at I<=N when the I'th function
|
||||||
|
returns a non-None result.
|
||||||
|
|
||||||
|
If historic is True calls to a hook will be memorized and replayed
|
||||||
|
on later registered plugins.
|
||||||
|
"""
|
||||||
|
def setattr_hookspec_opts(func):
|
||||||
|
if historic and firstresult:
|
||||||
|
raise ValueError("cannot have a historic firstresult hook")
|
||||||
|
if firstresult:
|
||||||
|
func.firstresult = firstresult
|
||||||
|
if historic:
|
||||||
|
func.historic = historic
|
||||||
|
return func
|
||||||
|
return setattr_hookspec_opts
|
||||||
|
|
||||||
|
|
||||||
|
def hookimpl_opts(hookwrapper=False, optionalhook=False,
|
||||||
|
tryfirst=False, trylast=False):
|
||||||
|
""" Return a decorator which marks a function as a hook implementation.
|
||||||
|
|
||||||
|
If optionalhook is True a missing matching hook specification will not result
|
||||||
|
in an error (by default it is an error if no matching spec is found).
|
||||||
|
|
||||||
|
If tryfirst is True this hook implementation will run as early as possible
|
||||||
|
in the chain of N hook implementations for a specfication.
|
||||||
|
|
||||||
|
If trylast is True this hook implementation will run as late as possible
|
||||||
|
in the chain of N hook implementations.
|
||||||
|
|
||||||
|
If hookwrapper is True the hook implementations needs to execute exactly
|
||||||
|
one "yield". The code before the yield is run early before any non-hookwrapper
|
||||||
|
function is run. The code after the yield is run after all non-hookwrapper
|
||||||
|
function have run. The yield receives an ``CallOutcome`` object representing
|
||||||
|
the exception or result outcome of the inner calls (including other hookwrapper
|
||||||
|
calls).
|
||||||
|
"""
|
||||||
|
def setattr_hookimpl_opts(func):
|
||||||
|
if hookwrapper:
|
||||||
|
func.hookwrapper = True
|
||||||
|
if optionalhook:
|
||||||
|
func.optionalhook = True
|
||||||
|
if tryfirst:
|
||||||
|
func.tryfirst = True
|
||||||
|
if trylast:
|
||||||
|
func.trylast = True
|
||||||
|
return func
|
||||||
|
return setattr_hookimpl_opts
|
||||||
|
|
||||||
|
|
||||||
class TagTracer:
|
class TagTracer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._tag2proc = {}
|
self._tag2proc = {}
|
||||||
|
@ -53,42 +107,28 @@ class TagTracer:
|
||||||
assert isinstance(tags, tuple)
|
assert isinstance(tags, tuple)
|
||||||
self._tag2proc[tags] = processor
|
self._tag2proc[tags] = processor
|
||||||
|
|
||||||
|
|
||||||
class TagTracerSub:
|
class TagTracerSub:
|
||||||
def __init__(self, root, tags):
|
def __init__(self, root, tags):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
|
||||||
def __call__(self, *args):
|
def __call__(self, *args):
|
||||||
self.root.processmessage(self.tags, args)
|
self.root.processmessage(self.tags, args)
|
||||||
|
|
||||||
def setmyprocessor(self, processor):
|
def setmyprocessor(self, processor):
|
||||||
self.root.setprocessor(self.tags, processor)
|
self.root.setprocessor(self.tags, processor)
|
||||||
|
|
||||||
def get(self, name):
|
def get(self, name):
|
||||||
return self.__class__(self.root, self.tags + (name,))
|
return self.__class__(self.root, self.tags + (name,))
|
||||||
|
|
||||||
|
|
||||||
def add_method_wrapper(cls, wrapper_func):
|
|
||||||
""" Substitute the function named "wrapperfunc.__name__" at class
|
|
||||||
"cls" with a function that wraps the call to the original function.
|
|
||||||
Return an undo function which can be called to reset the class to use
|
|
||||||
the old method again.
|
|
||||||
|
|
||||||
wrapper_func is called with the same arguments as the method
|
|
||||||
it wraps and its result is used as a wrap_controller for
|
|
||||||
calling the original function.
|
|
||||||
"""
|
|
||||||
name = wrapper_func.__name__
|
|
||||||
oldcall = getattr(cls, name)
|
|
||||||
def wrap_exec(*args, **kwargs):
|
|
||||||
gen = wrapper_func(*args, **kwargs)
|
|
||||||
return wrapped_call(gen, lambda: oldcall(*args, **kwargs))
|
|
||||||
|
|
||||||
setattr(cls, name, wrap_exec)
|
|
||||||
return lambda: setattr(cls, name, oldcall)
|
|
||||||
|
|
||||||
def raise_wrapfail(wrap_controller, msg):
|
def raise_wrapfail(wrap_controller, msg):
|
||||||
co = wrap_controller.gi_code
|
co = wrap_controller.gi_code
|
||||||
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
||||||
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
||||||
|
|
||||||
|
|
||||||
def wrapped_call(wrap_controller, func):
|
def wrapped_call(wrap_controller, func):
|
||||||
""" Wrap calling to a function with a generator which needs to yield
|
""" Wrap calling to a function with a generator which needs to yield
|
||||||
exactly once. The yield point will trigger calling the wrapped function
|
exactly once. The yield point will trigger calling the wrapped function
|
||||||
|
@ -133,6 +173,25 @@ class CallOutcome:
|
||||||
py.builtin._reraise(*ex)
|
py.builtin._reraise(*ex)
|
||||||
|
|
||||||
|
|
||||||
|
class TracedHookExecution:
|
||||||
|
def __init__(self, pluginmanager, before, after):
|
||||||
|
self.pluginmanager = pluginmanager
|
||||||
|
self.before = before
|
||||||
|
self.after = after
|
||||||
|
self.oldcall = pluginmanager._inner_hookexec
|
||||||
|
assert not isinstance(self.oldcall, TracedHookExecution)
|
||||||
|
self.pluginmanager._inner_hookexec = self
|
||||||
|
|
||||||
|
def __call__(self, hook, methods, kwargs):
|
||||||
|
self.before(hook, methods, kwargs)
|
||||||
|
outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs))
|
||||||
|
self.after(outcome, hook, methods, kwargs)
|
||||||
|
return outcome.get_result()
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.pluginmanager._inner_hookexec = self.oldcall
|
||||||
|
|
||||||
|
|
||||||
class PluginManager(object):
|
class PluginManager(object):
|
||||||
""" Core Pluginmanager class which manages registration
|
""" Core Pluginmanager class which manages registration
|
||||||
of plugin objects and 1:N hook calling.
|
of plugin objects and 1:N hook calling.
|
||||||
|
@ -144,197 +203,228 @@ class PluginManager(object):
|
||||||
plugin objects. An optional excludefunc allows to blacklist names which
|
plugin objects. An optional excludefunc allows to blacklist names which
|
||||||
are not considered as hooks despite a matching prefix.
|
are not considered as hooks despite a matching prefix.
|
||||||
|
|
||||||
For debugging purposes you can call ``set_tracing(writer)``
|
For debugging purposes you can call ``enable_tracing()``
|
||||||
which will subsequently send debug information to the specified
|
which will subsequently send debug information to the trace helper.
|
||||||
write function.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, prefix, excludefunc=None):
|
def __init__(self, prefix, excludefunc=None):
|
||||||
self._prefix = prefix
|
self._prefix = prefix
|
||||||
self._excludefunc = excludefunc
|
self._excludefunc = excludefunc
|
||||||
self._name2plugin = {}
|
self._name2plugin = {}
|
||||||
self._plugins = []
|
|
||||||
self._plugin2hookcallers = {}
|
self._plugin2hookcallers = {}
|
||||||
|
self._plugin_distinfo = []
|
||||||
self.trace = TagTracer().get("pluginmanage")
|
self.trace = TagTracer().get("pluginmanage")
|
||||||
self.hook = HookRelay(pm=self)
|
self.hook = HookRelay(self.trace.root.get("hook"))
|
||||||
|
self._inner_hookexec = lambda hook, methods, kwargs: \
|
||||||
|
MultiCall(methods, kwargs, hook.firstresult).execute()
|
||||||
|
|
||||||
def set_tracing(self, writer):
|
def _hookexec(self, hook, methods, kwargs):
|
||||||
""" turn on tracing to the given writer method and
|
# called from all hookcaller instances.
|
||||||
return an undo function. """
|
# enable_tracing will set its own wrapping function at self._inner_hookexec
|
||||||
self.trace.root.setwriter(writer)
|
return self._inner_hookexec(hook, methods, kwargs)
|
||||||
# reconfigure HookCalling to perform tracing
|
|
||||||
assert not hasattr(self, "_wrapping")
|
|
||||||
self._wrapping = True
|
|
||||||
|
|
||||||
hooktrace = self.hook.trace
|
def enable_tracing(self):
|
||||||
|
""" enable tracing of hook calls and return an undo function. """
|
||||||
|
hooktrace = self.hook._trace
|
||||||
|
|
||||||
def _docall(self, methods, kwargs):
|
def before(hook, methods, kwargs):
|
||||||
hooktrace.root.indent += 1
|
hooktrace.root.indent += 1
|
||||||
hooktrace(self.name, kwargs)
|
hooktrace(hook.name, kwargs)
|
||||||
box = yield
|
|
||||||
if box.excinfo is None:
|
def after(outcome, hook, methods, kwargs):
|
||||||
hooktrace("finish", self.name, "-->", box.result)
|
if outcome.excinfo is None:
|
||||||
|
hooktrace("finish", hook.name, "-->", outcome.result)
|
||||||
hooktrace.root.indent -= 1
|
hooktrace.root.indent -= 1
|
||||||
|
|
||||||
return add_method_wrapper(HookCaller, _docall)
|
return TracedHookExecution(self, before, after).undo
|
||||||
|
|
||||||
def make_hook_caller(self, name, plugins):
|
def subset_hook_caller(self, name, remove_plugins):
|
||||||
caller = getattr(self.hook, name)
|
""" Return a new HookCaller instance for the named method
|
||||||
methods = self.listattr(name, plugins=plugins)
|
which manages calls to all registered plugins except the
|
||||||
return HookCaller(caller.name, caller.firstresult,
|
ones from remove_plugins. """
|
||||||
argnames=caller.argnames, methods=methods)
|
orig = getattr(self.hook, name)
|
||||||
|
plugins_to_remove = [plugin for plugin in remove_plugins
|
||||||
|
if hasattr(plugin, name)]
|
||||||
|
if plugins_to_remove:
|
||||||
|
hc = HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class)
|
||||||
|
for plugin in orig._plugins:
|
||||||
|
if plugin not in plugins_to_remove:
|
||||||
|
hc._add_plugin(plugin)
|
||||||
|
# we also keep track of this hook caller so it
|
||||||
|
# gets properly removed on plugin unregistration
|
||||||
|
self._plugin2hookcallers.setdefault(plugin, []).append(hc)
|
||||||
|
return hc
|
||||||
|
return orig
|
||||||
|
|
||||||
def register(self, plugin, name=None):
|
def register(self, plugin, name=None):
|
||||||
""" Register a plugin with the given name and ensure that all its
|
""" Register a plugin and return its canonical name or None if the name
|
||||||
hook implementations are integrated. If the name is not specified
|
is blocked from registering. Raise a ValueError if the plugin is already
|
||||||
we use the ``__name__`` attribute of the plugin object or, if that
|
registered. """
|
||||||
doesn't exist, the id of the plugin. This method will raise a
|
plugin_name = name or self.get_canonical_name(plugin)
|
||||||
ValueError if the eventual name is already registered. """
|
|
||||||
name = name or self._get_canonical_name(plugin)
|
if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
|
||||||
if self._name2plugin.get(name, None) == -1:
|
if self._name2plugin.get(plugin_name, -1) is None:
|
||||||
return
|
return # blocked plugin, return None to indicate no registration
|
||||||
if self.hasplugin(name):
|
|
||||||
raise ValueError("Plugin already registered: %s=%s\n%s" %(
|
raise ValueError("Plugin already registered: %s=%s\n%s" %(
|
||||||
name, plugin, self._name2plugin))
|
plugin_name, plugin, self._name2plugin))
|
||||||
#self.trace("registering", name, plugin)
|
|
||||||
# allow subclasses to intercept here by calling a helper
|
|
||||||
return self._do_register(plugin, name)
|
|
||||||
|
|
||||||
def _do_register(self, plugin, name):
|
self._name2plugin[plugin_name] = plugin
|
||||||
hookcallers = list(self._scan_plugin(plugin))
|
|
||||||
self._plugin2hookcallers[plugin] = hookcallers
|
|
||||||
self._name2plugin[name] = plugin
|
|
||||||
self._plugins.append(plugin)
|
|
||||||
# rescan all methods for the hookcallers we found
|
|
||||||
for hookcaller in hookcallers:
|
|
||||||
self._scan_methods(hookcaller)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def unregister(self, plugin):
|
# register prefix-matching hook specs of the plugin
|
||||||
""" unregister the plugin object and all its contained hook implementations
|
self._plugin2hookcallers[plugin] = hookcallers = []
|
||||||
|
for name in dir(plugin):
|
||||||
|
if name.startswith(self._prefix):
|
||||||
|
hook = getattr(self.hook, name, None)
|
||||||
|
if hook is None:
|
||||||
|
if self._excludefunc is not None and self._excludefunc(name):
|
||||||
|
continue
|
||||||
|
hook = HookCaller(name, self._hookexec)
|
||||||
|
setattr(self.hook, name, hook)
|
||||||
|
elif hook.has_spec():
|
||||||
|
self._verify_hook(hook, plugin)
|
||||||
|
hook._maybe_apply_history(getattr(plugin, name))
|
||||||
|
hookcallers.append(hook)
|
||||||
|
hook._add_plugin(plugin)
|
||||||
|
return plugin_name
|
||||||
|
|
||||||
|
def unregister(self, plugin=None, name=None):
|
||||||
|
""" unregister a plugin object and all its contained hook implementations
|
||||||
from internal data structures. """
|
from internal data structures. """
|
||||||
self._plugins.remove(plugin)
|
if name is None:
|
||||||
for name, value in list(self._name2plugin.items()):
|
assert plugin is not None, "one of name or plugin needs to be specified"
|
||||||
if value == plugin:
|
name = self.get_name(plugin)
|
||||||
del self._name2plugin[name]
|
|
||||||
hookcallers = self._plugin2hookcallers.pop(plugin)
|
if plugin is None:
|
||||||
for hookcaller in hookcallers:
|
plugin = self.get_plugin(name)
|
||||||
self._scan_methods(hookcaller)
|
|
||||||
|
# if self._name2plugin[name] == None registration was blocked: ignore
|
||||||
|
if self._name2plugin.get(name):
|
||||||
|
del self._name2plugin[name]
|
||||||
|
|
||||||
|
for hookcaller in self._plugin2hookcallers.pop(plugin, []):
|
||||||
|
hookcaller._remove_plugin(plugin)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
def set_blocked(self, name):
|
||||||
|
""" block registrations of the given name, unregister if already registered. """
|
||||||
|
self.unregister(name=name)
|
||||||
|
self._name2plugin[name] = None
|
||||||
|
|
||||||
def addhooks(self, module_or_class):
|
def addhooks(self, module_or_class):
|
||||||
""" add new hook definitions from the given module_or_class using
|
""" add new hook definitions from the given module_or_class using
|
||||||
the prefix/excludefunc with which the PluginManager was initialized. """
|
the prefix/excludefunc with which the PluginManager was initialized. """
|
||||||
isclass = int(inspect.isclass(module_or_class))
|
|
||||||
names = []
|
names = []
|
||||||
for name in dir(module_or_class):
|
for name in dir(module_or_class):
|
||||||
if name.startswith(self._prefix):
|
if name.startswith(self._prefix):
|
||||||
method = module_or_class.__dict__[name]
|
hc = getattr(self.hook, name, None)
|
||||||
firstresult = getattr(method, 'firstresult', False)
|
if hc is None:
|
||||||
hc = HookCaller(name, firstresult=firstresult,
|
hc = HookCaller(name, self._hookexec, module_or_class)
|
||||||
argnames=varnames(method, startindex=isclass))
|
setattr(self.hook, name, hc)
|
||||||
setattr(self.hook, name, hc)
|
else:
|
||||||
|
# plugins registered this hook without knowing the spec
|
||||||
|
hc.set_specification(module_or_class)
|
||||||
|
for plugin in hc._plugins:
|
||||||
|
self._verify_hook(hc, plugin)
|
||||||
names.append(name)
|
names.append(name)
|
||||||
|
|
||||||
if not names:
|
if not names:
|
||||||
raise ValueError("did not find new %r hooks in %r"
|
raise ValueError("did not find new %r hooks in %r"
|
||||||
%(self._prefix, module_or_class))
|
%(self._prefix, module_or_class))
|
||||||
|
|
||||||
def getplugins(self):
|
def get_plugins(self):
|
||||||
""" return the complete list of registered plugins. NOTE that
|
""" return the set of registered plugins. """
|
||||||
you will get the internal list and need to make a copy if you
|
return set(self._plugin2hookcallers)
|
||||||
modify the list."""
|
|
||||||
return self._plugins
|
|
||||||
|
|
||||||
def isregistered(self, plugin):
|
def is_registered(self, plugin):
|
||||||
""" Return True if the plugin is already registered under its
|
""" Return True if the plugin is already registered. """
|
||||||
canonical name. """
|
return plugin in self._plugin2hookcallers
|
||||||
return self.hasplugin(self._get_canonical_name(plugin)) or \
|
|
||||||
plugin in self._plugins
|
|
||||||
|
|
||||||
def hasplugin(self, name):
|
def get_canonical_name(self, plugin):
|
||||||
""" Return True if there is a registered with the given name. """
|
""" Return canonical name for a plugin object. Note that a plugin
|
||||||
return name in self._name2plugin
|
may be registered under a different name which was specified
|
||||||
|
by the caller of register(plugin, name). To obtain the name
|
||||||
|
of an registered plugin use ``get_name(plugin)`` instead."""
|
||||||
|
return getattr(plugin, "__name__", None) or str(id(plugin))
|
||||||
|
|
||||||
def getplugin(self, name):
|
def get_plugin(self, name):
|
||||||
""" Return a plugin or None for the given name. """
|
""" Return a plugin or None for the given name. """
|
||||||
return self._name2plugin.get(name)
|
return self._name2plugin.get(name)
|
||||||
|
|
||||||
def listattr(self, attrname, plugins=None):
|
def get_name(self, plugin):
|
||||||
if plugins is None:
|
""" Return name for registered plugin or None if not registered. """
|
||||||
plugins = self._plugins
|
for name, val in self._name2plugin.items():
|
||||||
l = []
|
if plugin == val:
|
||||||
last = []
|
return name
|
||||||
wrappers = []
|
|
||||||
for plugin in plugins:
|
def _verify_hook(self, hook, plugin):
|
||||||
|
method = getattr(plugin, hook.name)
|
||||||
|
pluginname = self.get_name(plugin)
|
||||||
|
|
||||||
|
if hook.is_historic() and hasattr(method, "hookwrapper"):
|
||||||
|
raise PluginValidationError(
|
||||||
|
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %(
|
||||||
|
pluginname, hook.name))
|
||||||
|
|
||||||
|
for arg in varnames(method):
|
||||||
|
if arg not in hook.argnames:
|
||||||
|
raise PluginValidationError(
|
||||||
|
"Plugin %r\nhook %r\nargument %r not available\n"
|
||||||
|
"plugin definition: %s\n"
|
||||||
|
"available hookargs: %s" %(
|
||||||
|
pluginname, hook.name, arg, formatdef(method),
|
||||||
|
", ".join(hook.argnames)))
|
||||||
|
|
||||||
|
def check_pending(self):
|
||||||
|
""" Verify that all hooks which have not been verified against
|
||||||
|
a hook specification are optional, otherwise raise PluginValidationError"""
|
||||||
|
for name in self.hook.__dict__:
|
||||||
|
if name.startswith(self._prefix):
|
||||||
|
hook = getattr(self.hook, name)
|
||||||
|
if not hook.has_spec():
|
||||||
|
for plugin in hook._plugins:
|
||||||
|
method = getattr(plugin, hook.name)
|
||||||
|
if not getattr(method, "optionalhook", False):
|
||||||
|
raise PluginValidationError(
|
||||||
|
"unknown hook %r in plugin %r" %(name, plugin))
|
||||||
|
|
||||||
|
def load_setuptools_entrypoints(self, entrypoint_name):
|
||||||
|
""" Load modules from querying the specified setuptools entrypoint name.
|
||||||
|
Return the number of loaded plugins. """
|
||||||
|
from pkg_resources import iter_entry_points, DistributionNotFound
|
||||||
|
for ep in iter_entry_points(entrypoint_name):
|
||||||
|
# is the plugin registered or blocked?
|
||||||
|
if self.get_plugin(ep.name) or ep.name in self._name2plugin:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
meth = getattr(plugin, attrname)
|
plugin = ep.load()
|
||||||
except AttributeError:
|
except DistributionNotFound:
|
||||||
continue
|
continue
|
||||||
if hasattr(meth, 'hookwrapper'):
|
self.register(plugin, name=ep.name)
|
||||||
wrappers.append(meth)
|
self._plugin_distinfo.append((ep.dist, plugin))
|
||||||
elif hasattr(meth, 'tryfirst'):
|
return len(self._plugin_distinfo)
|
||||||
last.append(meth)
|
|
||||||
elif hasattr(meth, 'trylast'):
|
|
||||||
l.insert(0, meth)
|
|
||||||
else:
|
|
||||||
l.append(meth)
|
|
||||||
l.extend(last)
|
|
||||||
l.extend(wrappers)
|
|
||||||
return l
|
|
||||||
|
|
||||||
def _scan_methods(self, hookcaller):
|
|
||||||
hookcaller.methods = self.listattr(hookcaller.name)
|
|
||||||
|
|
||||||
def call_plugin(self, plugin, methname, kwargs):
|
|
||||||
return MultiCall(methods=self.listattr(methname, plugins=[plugin]),
|
|
||||||
kwargs=kwargs, firstresult=True).execute()
|
|
||||||
|
|
||||||
|
|
||||||
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 _get_canonical_name(self, plugin):
|
|
||||||
return getattr(plugin, "__name__", None) or str(id(plugin))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MultiCall:
|
class MultiCall:
|
||||||
""" execute a call into multiple python functions/methods. """
|
""" execute a call into multiple python functions/methods. """
|
||||||
|
|
||||||
|
# XXX note that the __multicall__ argument is supported only
|
||||||
|
# for pytest compatibility reasons. It was never officially
|
||||||
|
# supported there and is explicitely deprecated since 2.8
|
||||||
|
# so we can remove it soon, allowing to avoid the below recursion
|
||||||
|
# in execute() and simplify/speed up the execute loop.
|
||||||
|
|
||||||
def __init__(self, methods, kwargs, firstresult=False):
|
def __init__(self, methods, kwargs, firstresult=False):
|
||||||
self.methods = list(methods)
|
self.methods = methods
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.kwargs["__multicall__"] = self
|
self.kwargs["__multicall__"] = self
|
||||||
self.results = []
|
|
||||||
self.firstresult = firstresult
|
self.firstresult = firstresult
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
status = "%d results, %d meths" % (len(self.results), len(self.methods))
|
|
||||||
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
|
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
all_kwargs = self.kwargs
|
all_kwargs = self.kwargs
|
||||||
|
self.results = results = []
|
||||||
|
firstresult = self.firstresult
|
||||||
|
|
||||||
while self.methods:
|
while self.methods:
|
||||||
method = self.methods.pop()
|
method = self.methods.pop()
|
||||||
args = [all_kwargs[argname] for argname in varnames(method)]
|
args = [all_kwargs[argname] for argname in varnames(method)]
|
||||||
|
@ -342,11 +432,19 @@ class MultiCall:
|
||||||
return wrapped_call(method(*args), self.execute)
|
return wrapped_call(method(*args), self.execute)
|
||||||
res = method(*args)
|
res = method(*args)
|
||||||
if res is not None:
|
if res is not None:
|
||||||
self.results.append(res)
|
if firstresult:
|
||||||
if self.firstresult:
|
|
||||||
return res
|
return res
|
||||||
if not self.firstresult:
|
results.append(res)
|
||||||
return self.results
|
|
||||||
|
if not firstresult:
|
||||||
|
return results
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
status = "%d meths" % (len(self.methods),)
|
||||||
|
if hasattr(self, "results"):
|
||||||
|
status = ("%d results, " % len(self.results)) + status
|
||||||
|
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def varnames(func, startindex=None):
|
def varnames(func, startindex=None):
|
||||||
|
@ -361,17 +459,17 @@ def varnames(func, startindex=None):
|
||||||
return cache["_varnames"]
|
return cache["_varnames"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
if inspect.isclass(func):
|
if isclass(func):
|
||||||
try:
|
try:
|
||||||
func = func.__init__
|
func = func.__init__
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return ()
|
return ()
|
||||||
startindex = 1
|
startindex = 1
|
||||||
else:
|
else:
|
||||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
if not isfunction(func) and not ismethod(func):
|
||||||
func = getattr(func, '__call__', func)
|
func = getattr(func, '__call__', func)
|
||||||
if startindex is None:
|
if startindex is None:
|
||||||
startindex = int(inspect.ismethod(func))
|
startindex = int(ismethod(func))
|
||||||
|
|
||||||
rawcode = py.code.getrawcode(func)
|
rawcode = py.code.getrawcode(func)
|
||||||
try:
|
try:
|
||||||
|
@ -390,32 +488,95 @@ def varnames(func, startindex=None):
|
||||||
|
|
||||||
|
|
||||||
class HookRelay:
|
class HookRelay:
|
||||||
def __init__(self, pm):
|
def __init__(self, trace):
|
||||||
self._pm = pm
|
self._trace = trace
|
||||||
self.trace = pm.trace.root.get("hook")
|
|
||||||
|
|
||||||
|
|
||||||
class HookCaller:
|
class HookCaller(object):
|
||||||
def __init__(self, name, firstresult, argnames, methods=()):
|
def __init__(self, name, hook_execute, specmodule_or_class=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.firstresult = firstresult
|
self._plugins = []
|
||||||
self.argnames = ["__multicall__"]
|
self._wrappers = []
|
||||||
self.argnames.extend(argnames)
|
self._nonwrappers = []
|
||||||
|
self._hookexec = hook_execute
|
||||||
|
if specmodule_or_class is not None:
|
||||||
|
self.set_specification(specmodule_or_class)
|
||||||
|
|
||||||
|
def has_spec(self):
|
||||||
|
return hasattr(self, "_specmodule_or_class")
|
||||||
|
|
||||||
|
def set_specification(self, specmodule_or_class):
|
||||||
|
assert not self.has_spec()
|
||||||
|
self._specmodule_or_class = specmodule_or_class
|
||||||
|
specfunc = getattr(specmodule_or_class, self.name)
|
||||||
|
argnames = varnames(specfunc, startindex=isclass(specmodule_or_class))
|
||||||
assert "self" not in argnames # sanity check
|
assert "self" not in argnames # sanity check
|
||||||
self.methods = methods
|
self.argnames = ["__multicall__"] + list(argnames)
|
||||||
|
self.firstresult = getattr(specfunc, 'firstresult', False)
|
||||||
|
if hasattr(specfunc, "historic"):
|
||||||
|
self._call_history = []
|
||||||
|
|
||||||
|
def is_historic(self):
|
||||||
|
return hasattr(self, "_call_history")
|
||||||
|
|
||||||
|
def _remove_plugin(self, plugin):
|
||||||
|
self._plugins.remove(plugin)
|
||||||
|
meth = getattr(plugin, self.name)
|
||||||
|
try:
|
||||||
|
self._nonwrappers.remove(meth)
|
||||||
|
except ValueError:
|
||||||
|
self._wrappers.remove(meth)
|
||||||
|
|
||||||
|
def _add_plugin(self, plugin):
|
||||||
|
self._plugins.append(plugin)
|
||||||
|
self._add_method(getattr(plugin, self.name))
|
||||||
|
|
||||||
|
def _add_method(self, meth):
|
||||||
|
if hasattr(meth, 'hookwrapper'):
|
||||||
|
methods = self._wrappers
|
||||||
|
else:
|
||||||
|
methods = self._nonwrappers
|
||||||
|
|
||||||
|
if hasattr(meth, 'trylast'):
|
||||||
|
methods.insert(0, meth)
|
||||||
|
elif hasattr(meth, 'tryfirst'):
|
||||||
|
methods.append(meth)
|
||||||
|
else:
|
||||||
|
# find last non-tryfirst method
|
||||||
|
i = len(methods) - 1
|
||||||
|
while i >= 0 and hasattr(methods[i], "tryfirst"):
|
||||||
|
i -= 1
|
||||||
|
methods.insert(i + 1, meth)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<HookCaller %r>" %(self.name,)
|
return "<HookCaller %r>" %(self.name,)
|
||||||
|
|
||||||
def __call__(self, **kwargs):
|
def __call__(self, **kwargs):
|
||||||
return self._docall(self.methods, kwargs)
|
assert not self.is_historic()
|
||||||
|
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||||
|
|
||||||
def callextra(self, methods, **kwargs):
|
def call_historic(self, proc=None, kwargs=None):
|
||||||
return self._docall(self.methods + methods, kwargs)
|
self._call_history.append((kwargs or {}, proc))
|
||||||
|
# historizing hooks don't return results
|
||||||
|
self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||||
|
|
||||||
def _docall(self, methods, kwargs):
|
def call_extra(self, methods, kwargs):
|
||||||
return MultiCall(methods, kwargs,
|
""" Call the hook with some additional temporarily participating
|
||||||
firstresult=self.firstresult).execute()
|
methods using the specified kwargs as call parameters. """
|
||||||
|
old = list(self._nonwrappers), list(self._wrappers)
|
||||||
|
for method in methods:
|
||||||
|
self._add_method(method)
|
||||||
|
try:
|
||||||
|
return self(**kwargs)
|
||||||
|
finally:
|
||||||
|
self._nonwrappers, self._wrappers = old
|
||||||
|
|
||||||
|
def _maybe_apply_history(self, method):
|
||||||
|
if self.is_historic():
|
||||||
|
for kwargs, proc in self._call_history:
|
||||||
|
res = self._hookexec(self, [method], kwargs)
|
||||||
|
if res and proc is not None:
|
||||||
|
proc(res[0])
|
||||||
|
|
||||||
|
|
||||||
class PluginValidationError(Exception):
|
class PluginValidationError(Exception):
|
||||||
|
@ -425,5 +586,5 @@ class PluginValidationError(Exception):
|
||||||
def formatdef(func):
|
def formatdef(func):
|
||||||
return "%s%s" % (
|
return "%s%s" % (
|
||||||
func.__name__,
|
func.__name__,
|
||||||
inspect.formatargspec(*inspect.getargspec(func))
|
formatargspec(*getargspec(func))
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,7 +22,7 @@ def pytest_addoption(parser):
|
||||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_cmdline_parse():
|
def pytest_cmdline_parse():
|
||||||
outcome = yield
|
outcome = yield
|
||||||
config = outcome.get_result()
|
config = outcome.get_result()
|
||||||
|
@ -34,13 +34,15 @@ def pytest_cmdline_parse():
|
||||||
pytest.__version__, py.__version__,
|
pytest.__version__, py.__version__,
|
||||||
".".join(map(str, sys.version_info)),
|
".".join(map(str, sys.version_info)),
|
||||||
os.getcwd(), config._origargs))
|
os.getcwd(), config._origargs))
|
||||||
config.pluginmanager.set_tracing(debugfile.write)
|
config.trace.root.setwriter(debugfile.write)
|
||||||
|
undo_tracing = config.pluginmanager.enable_tracing()
|
||||||
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
||||||
def unset_tracing():
|
def unset_tracing():
|
||||||
debugfile.close()
|
debugfile.close()
|
||||||
sys.stderr.write("wrote pytestdebug information to %s\n" %
|
sys.stderr.write("wrote pytestdebug information to %s\n" %
|
||||||
debugfile.name)
|
debugfile.name)
|
||||||
config.trace.root.setwriter(None)
|
config.trace.root.setwriter(None)
|
||||||
|
undo_tracing()
|
||||||
config.add_cleanup(unset_tracing)
|
config.add_cleanup(unset_tracing)
|
||||||
|
|
||||||
def pytest_cmdline_main(config):
|
def pytest_cmdline_main(config):
|
||||||
|
|
|
@ -1,27 +1,30 @@
|
||||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||||
|
|
||||||
|
from _pytest.core import hookspec_opts
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Initialization
|
# Initialization hooks called for every plugin
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
def pytest_addhooks(pluginmanager):
|
def pytest_addhooks(pluginmanager):
|
||||||
"""called at plugin load time to allow adding new hooks via a call to
|
"""called at plugin registration time to allow adding new hooks via a call to
|
||||||
pluginmanager.addhooks(module_or_class, prefix)."""
|
pluginmanager.addhooks(module_or_class, prefix)."""
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
def pytest_namespace():
|
def pytest_namespace():
|
||||||
"""return dict of name->object to be made globally available in
|
"""return dict of name->object to be made globally available in
|
||||||
the pytest namespace. This hook is called before command line options
|
the pytest namespace. This hook is called at plugin registration
|
||||||
are parsed.
|
time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def pytest_cmdline_parse(pluginmanager, args):
|
@hookspec_opts(historic=True)
|
||||||
"""return initialized config object, parsing the specified args. """
|
def pytest_plugin_registered(plugin, manager):
|
||||||
pytest_cmdline_parse.firstresult = True
|
""" a new pytest plugin got registered. """
|
||||||
|
|
||||||
def pytest_cmdline_preparse(config, args):
|
|
||||||
"""(deprecated) modify command line arguments before option parsing. """
|
|
||||||
|
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
"""register argparse-style options and ini-style config values.
|
"""register argparse-style options and ini-style config values.
|
||||||
|
|
||||||
|
@ -47,35 +50,43 @@ def pytest_addoption(parser):
|
||||||
via (deprecated) ``pytest.config``.
|
via (deprecated) ``pytest.config``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
|
def pytest_configure(config):
|
||||||
|
""" called after command line options have been parsed
|
||||||
|
and all plugins and initial conftest files been loaded.
|
||||||
|
This hook is called for every plugin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# Bootstrapping hooks called for plugins registered early enough:
|
||||||
|
# internal and 3rd party plugins as well as directly
|
||||||
|
# discoverable conftest.py local plugins.
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
|
def pytest_cmdline_parse(pluginmanager, args):
|
||||||
|
"""return initialized config object, parsing the specified args. """
|
||||||
|
|
||||||
|
def pytest_cmdline_preparse(config, args):
|
||||||
|
"""(deprecated) modify command line arguments before option parsing. """
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_cmdline_main(config):
|
def pytest_cmdline_main(config):
|
||||||
""" called for performing the main command line action. The default
|
""" called for performing the main command line action. The default
|
||||||
implementation will invoke the configure hooks and runtest_mainloop. """
|
implementation will invoke the configure hooks and runtest_mainloop. """
|
||||||
pytest_cmdline_main.firstresult = True
|
|
||||||
|
|
||||||
def pytest_load_initial_conftests(args, early_config, parser):
|
def pytest_load_initial_conftests(args, early_config, parser):
|
||||||
""" implements the loading of initial conftest files ahead
|
""" implements the loading of initial conftest files ahead
|
||||||
of command line option parsing. """
|
of command line option parsing. """
|
||||||
|
|
||||||
def pytest_configure(config):
|
|
||||||
""" called after command line options have been parsed
|
|
||||||
and all plugins and initial conftest files been loaded.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def pytest_unconfigure(config):
|
|
||||||
""" called before test process is exited. """
|
|
||||||
|
|
||||||
def pytest_runtestloop(session):
|
|
||||||
""" called for performing the main runtest loop
|
|
||||||
(after collection finished). """
|
|
||||||
pytest_runtestloop.firstresult = True
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# collection hooks
|
# collection hooks
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_collection(session):
|
def pytest_collection(session):
|
||||||
""" perform the collection protocol for the given session. """
|
""" perform the collection protocol for the given session. """
|
||||||
pytest_collection.firstresult = True
|
|
||||||
|
|
||||||
def pytest_collection_modifyitems(session, config, items):
|
def pytest_collection_modifyitems(session, config, items):
|
||||||
""" called after collection has been performed, may filter or re-order
|
""" called after collection has been performed, may filter or re-order
|
||||||
|
@ -84,16 +95,16 @@ def pytest_collection_modifyitems(session, config, items):
|
||||||
def pytest_collection_finish(session):
|
def pytest_collection_finish(session):
|
||||||
""" called after collection has been performed and modified. """
|
""" called after collection has been performed and modified. """
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_ignore_collect(path, config):
|
def pytest_ignore_collect(path, config):
|
||||||
""" return True to prevent considering this path for collection.
|
""" return True to prevent considering this path for collection.
|
||||||
This hook is consulted for all files and directories prior to calling
|
This hook is consulted for all files and directories prior to calling
|
||||||
more specific hooks.
|
more specific hooks.
|
||||||
"""
|
"""
|
||||||
pytest_ignore_collect.firstresult = True
|
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_collect_directory(path, parent):
|
def pytest_collect_directory(path, parent):
|
||||||
""" called before traversing a directory for collection files. """
|
""" called before traversing a directory for collection files. """
|
||||||
pytest_collect_directory.firstresult = True
|
|
||||||
|
|
||||||
def pytest_collect_file(path, parent):
|
def pytest_collect_file(path, parent):
|
||||||
""" return collection Node or None for the given path. Any new node
|
""" return collection Node or None for the given path. Any new node
|
||||||
|
@ -112,29 +123,29 @@ def pytest_collectreport(report):
|
||||||
def pytest_deselected(items):
|
def pytest_deselected(items):
|
||||||
""" called for test items deselected by keyword. """
|
""" called for test items deselected by keyword. """
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_make_collect_report(collector):
|
def pytest_make_collect_report(collector):
|
||||||
""" perform ``collector.collect()`` and return a CollectReport. """
|
""" perform ``collector.collect()`` and return a CollectReport. """
|
||||||
pytest_make_collect_report.firstresult = True
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Python test function related hooks
|
# Python test function related hooks
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_pycollect_makemodule(path, parent):
|
def pytest_pycollect_makemodule(path, parent):
|
||||||
""" return a Module collector or None for the given path.
|
""" return a Module collector or None for the given path.
|
||||||
This hook will be called for each matching test module path.
|
This hook will be called for each matching test module path.
|
||||||
The pytest_collect_file hook needs to be used if you want to
|
The pytest_collect_file hook needs to be used if you want to
|
||||||
create test modules for files that do not match as a test module.
|
create test modules for files that do not match as a test module.
|
||||||
"""
|
"""
|
||||||
pytest_pycollect_makemodule.firstresult = True
|
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_pycollect_makeitem(collector, name, obj):
|
def pytest_pycollect_makeitem(collector, name, obj):
|
||||||
""" return custom item/collector for a python object in a module, or None. """
|
""" return custom item/collector for a python object in a module, or None. """
|
||||||
pytest_pycollect_makeitem.firstresult = True
|
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_pyfunc_call(pyfuncitem):
|
def pytest_pyfunc_call(pyfuncitem):
|
||||||
""" call underlying test function. """
|
""" call underlying test function. """
|
||||||
pytest_pyfunc_call.firstresult = True
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
""" generate (multiple) parametrized calls to a test function."""
|
""" generate (multiple) parametrized calls to a test function."""
|
||||||
|
@ -142,9 +153,16 @@ def pytest_generate_tests(metafunc):
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# generic runtest related hooks
|
# generic runtest related hooks
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
|
def pytest_runtestloop(session):
|
||||||
|
""" called for performing the main runtest loop
|
||||||
|
(after collection finished). """
|
||||||
|
|
||||||
def pytest_itemstart(item, node):
|
def pytest_itemstart(item, node):
|
||||||
""" (deprecated, use pytest_runtest_logstart). """
|
""" (deprecated, use pytest_runtest_logstart). """
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_runtest_protocol(item, nextitem):
|
def pytest_runtest_protocol(item, nextitem):
|
||||||
""" implements the runtest_setup/call/teardown protocol for
|
""" implements the runtest_setup/call/teardown protocol for
|
||||||
the given test item, including capturing exceptions and calling
|
the given test item, including capturing exceptions and calling
|
||||||
|
@ -158,7 +176,6 @@ def pytest_runtest_protocol(item, nextitem):
|
||||||
|
|
||||||
:return boolean: True if no further hook implementations should be invoked.
|
:return boolean: True if no further hook implementations should be invoked.
|
||||||
"""
|
"""
|
||||||
pytest_runtest_protocol.firstresult = True
|
|
||||||
|
|
||||||
def pytest_runtest_logstart(nodeid, location):
|
def pytest_runtest_logstart(nodeid, location):
|
||||||
""" signal the start of running a single test item. """
|
""" signal the start of running a single test item. """
|
||||||
|
@ -178,12 +195,12 @@ def pytest_runtest_teardown(item, nextitem):
|
||||||
so that nextitem only needs to call setup-functions.
|
so that nextitem only needs to call setup-functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
""" return a :py:class:`_pytest.runner.TestReport` object
|
""" return a :py:class:`_pytest.runner.TestReport` object
|
||||||
for the given :py:class:`pytest.Item` and
|
for the given :py:class:`pytest.Item` and
|
||||||
:py:class:`_pytest.runner.CallInfo`.
|
:py:class:`_pytest.runner.CallInfo`.
|
||||||
"""
|
"""
|
||||||
pytest_runtest_makereport.firstresult = True
|
|
||||||
|
|
||||||
def pytest_runtest_logreport(report):
|
def pytest_runtest_logreport(report):
|
||||||
""" process a test setup/call/teardown report relating to
|
""" process a test setup/call/teardown report relating to
|
||||||
|
@ -199,6 +216,9 @@ def pytest_sessionstart(session):
|
||||||
def pytest_sessionfinish(session, exitstatus):
|
def pytest_sessionfinish(session, exitstatus):
|
||||||
""" whole test run finishes. """
|
""" whole test run finishes. """
|
||||||
|
|
||||||
|
def pytest_unconfigure(config):
|
||||||
|
""" called before test process is exited. """
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# hooks for customising the assert methods
|
# hooks for customising the assert methods
|
||||||
|
@ -220,9 +240,9 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||||
def pytest_report_header(config, startdir):
|
def pytest_report_header(config, startdir):
|
||||||
""" return a string to be displayed as header info for terminal reporting."""
|
""" return a string to be displayed as header info for terminal reporting."""
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_report_teststatus(report):
|
def pytest_report_teststatus(report):
|
||||||
""" return result-category, shortletter and verbose word for reporting."""
|
""" return result-category, shortletter and verbose word for reporting."""
|
||||||
pytest_report_teststatus.firstresult = True
|
|
||||||
|
|
||||||
def pytest_terminal_summary(terminalreporter):
|
def pytest_terminal_summary(terminalreporter):
|
||||||
""" add additional section in terminal summary reporting. """
|
""" add additional section in terminal summary reporting. """
|
||||||
|
@ -236,17 +256,14 @@ def pytest_logwarning(message, code, nodeid, fslocation):
|
||||||
# doctest hooks
|
# doctest hooks
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
def pytest_doctest_prepare_content(content):
|
def pytest_doctest_prepare_content(content):
|
||||||
""" return processed content for a given doctest"""
|
""" return processed content for a given doctest"""
|
||||||
pytest_doctest_prepare_content.firstresult = True
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# error handling and internal debugging hooks
|
# error handling and internal debugging hooks
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
def pytest_plugin_registered(plugin, manager):
|
|
||||||
""" a new pytest plugin got registered. """
|
|
||||||
|
|
||||||
def pytest_internalerror(excrepr, excinfo):
|
def pytest_internalerror(excrepr, excinfo):
|
||||||
""" called for internal errors. """
|
""" called for internal errors. """
|
||||||
|
|
||||||
|
|
|
@ -151,18 +151,17 @@ def pytest_ignore_collect(path, config):
|
||||||
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||||||
return path in ignore_paths
|
return path in ignore_paths
|
||||||
|
|
||||||
class FSHookProxy(object):
|
class FSHookProxy:
|
||||||
def __init__(self, fspath, config):
|
def __init__(self, fspath, pm, remove_mods):
|
||||||
self.fspath = fspath
|
self.fspath = fspath
|
||||||
self.config = config
|
self.pm = pm
|
||||||
|
self.remove_mods = remove_mods
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
plugins = self.config._getmatchingplugins(self.fspath)
|
x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
|
||||||
x = self.config.pluginmanager.make_hook_caller(name, plugins)
|
|
||||||
self.__dict__[name] = x
|
self.__dict__[name] = x
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
def compatproperty(name):
|
def compatproperty(name):
|
||||||
def fget(self):
|
def fget(self):
|
||||||
# deprecated - use pytest.name
|
# deprecated - use pytest.name
|
||||||
|
@ -362,9 +361,6 @@ class Node(object):
|
||||||
def listnames(self):
|
def listnames(self):
|
||||||
return [x.name for x in self.listchain()]
|
return [x.name for x in self.listchain()]
|
||||||
|
|
||||||
def getplugins(self):
|
|
||||||
return self.config._getmatchingplugins(self.fspath)
|
|
||||||
|
|
||||||
def addfinalizer(self, fin):
|
def addfinalizer(self, fin):
|
||||||
""" register a function to be called when this node is finalized.
|
""" register a function to be called when this node is finalized.
|
||||||
|
|
||||||
|
@ -519,12 +515,12 @@ class Session(FSCollector):
|
||||||
def _makeid(self):
|
def _makeid(self):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_collectstart(self):
|
def pytest_collectstart(self):
|
||||||
if self.shouldstop:
|
if self.shouldstop:
|
||||||
raise self.Interrupted(self.shouldstop)
|
raise self.Interrupted(self.shouldstop)
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_runtest_logreport(self, report):
|
def pytest_runtest_logreport(self, report):
|
||||||
if report.failed and not hasattr(report, 'wasxfail'):
|
if report.failed and not hasattr(report, 'wasxfail'):
|
||||||
self._testsfailed += 1
|
self._testsfailed += 1
|
||||||
|
@ -541,8 +537,20 @@ class Session(FSCollector):
|
||||||
try:
|
try:
|
||||||
return self._fs2hookproxy[fspath]
|
return self._fs2hookproxy[fspath]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config)
|
# check if we have the common case of running
|
||||||
return x
|
# hooks with all conftest.py filesall conftest.py
|
||||||
|
pm = self.config.pluginmanager
|
||||||
|
my_conftestmodules = pm._getconftestmodules(fspath)
|
||||||
|
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||||
|
if remove_mods:
|
||||||
|
# one or more conftests are not in use at this fspath
|
||||||
|
proxy = FSHookProxy(fspath, pm, remove_mods)
|
||||||
|
else:
|
||||||
|
# all plugis are active for this fspath
|
||||||
|
proxy = self.config.hook
|
||||||
|
|
||||||
|
self._fs2hookproxy[fspath] = proxy
|
||||||
|
return proxy
|
||||||
|
|
||||||
def perform_collect(self, args=None, genitems=True):
|
def perform_collect(self, args=None, genitems=True):
|
||||||
hook = self.config.hook
|
hook = self.config.hook
|
||||||
|
|
|
@ -24,7 +24,7 @@ def pytest_runtest_makereport(item, call):
|
||||||
call.excinfo = call2.excinfo
|
call.excinfo = call2.excinfo
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.trylast
|
@pytest.hookimpl_opts(trylast=True)
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
if is_potential_nosetest(item):
|
if is_potential_nosetest(item):
|
||||||
if isinstance(item.parent, pytest.Generator):
|
if isinstance(item.parent, pytest.Generator):
|
||||||
|
|
|
@ -11,7 +11,7 @@ def pytest_addoption(parser):
|
||||||
choices=['failed', 'all'],
|
choices=['failed', 'all'],
|
||||||
help="send failed|all info to bpaste.net pastebin service.")
|
help="send failed|all info to bpaste.net pastebin service.")
|
||||||
|
|
||||||
@pytest.mark.trylast
|
@pytest.hookimpl_opts(trylast=True)
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
if config.option.pastebin == "all":
|
if config.option.pastebin == "all":
|
||||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||||
|
|
|
@ -11,7 +11,7 @@ import subprocess
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
from py.builtin import print_
|
from py.builtin import print_
|
||||||
from _pytest.core import HookCaller, add_method_wrapper
|
from _pytest.core import TracedHookExecution
|
||||||
|
|
||||||
from _pytest.main import Session, EXIT_OK
|
from _pytest.main import Session, EXIT_OK
|
||||||
|
|
||||||
|
@ -79,12 +79,12 @@ class HookRecorder:
|
||||||
self._pluginmanager = pluginmanager
|
self._pluginmanager = pluginmanager
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
def _docall(hookcaller, methods, kwargs):
|
def before(hook, method, kwargs):
|
||||||
self.calls.append(ParsedCall(hookcaller.name, kwargs))
|
self.calls.append(ParsedCall(hook.name, kwargs))
|
||||||
yield
|
def after(outcome, hook, method, kwargs):
|
||||||
self._undo_wrapping = add_method_wrapper(HookCaller, _docall)
|
pass
|
||||||
#if hasattr(pluginmanager, "config"):
|
executor = TracedHookExecution(pluginmanager, before, after)
|
||||||
# pluginmanager.add_shutdown(self._undo_wrapping)
|
self._undo_wrapping = executor.undo
|
||||||
|
|
||||||
def finish_recording(self):
|
def finish_recording(self):
|
||||||
self._undo_wrapping()
|
self._undo_wrapping()
|
||||||
|
|
|
@ -172,7 +172,7 @@ def pytest_configure(config):
|
||||||
def pytest_sessionstart(session):
|
def pytest_sessionstart(session):
|
||||||
session._fixturemanager = FixtureManager(session)
|
session._fixturemanager = FixtureManager(session)
|
||||||
|
|
||||||
@pytest.mark.trylast
|
@pytest.hookimpl_opts(trylast=True)
|
||||||
def pytest_namespace():
|
def pytest_namespace():
|
||||||
raises.Exception = pytest.fail.Exception
|
raises.Exception = pytest.fail.Exception
|
||||||
return {
|
return {
|
||||||
|
@ -191,7 +191,7 @@ def pytestconfig(request):
|
||||||
return request.config
|
return request.config
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.trylast
|
@pytest.hookimpl_opts(trylast=True)
|
||||||
def pytest_pyfunc_call(pyfuncitem):
|
def pytest_pyfunc_call(pyfuncitem):
|
||||||
testfunction = pyfuncitem.obj
|
testfunction = pyfuncitem.obj
|
||||||
if pyfuncitem._isyieldedfunction():
|
if pyfuncitem._isyieldedfunction():
|
||||||
|
@ -219,7 +219,7 @@ def pytest_collect_file(path, parent):
|
||||||
def pytest_pycollect_makemodule(path, parent):
|
def pytest_pycollect_makemodule(path, parent):
|
||||||
return Module(path, parent)
|
return Module(path, parent)
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_pycollect_makeitem(collector, name, obj):
|
def pytest_pycollect_makeitem(collector, name, obj):
|
||||||
outcome = yield
|
outcome = yield
|
||||||
res = outcome.get_result()
|
res = outcome.get_result()
|
||||||
|
@ -375,13 +375,16 @@ class PyCollector(PyobjMixin, pytest.Collector):
|
||||||
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
|
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
|
||||||
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
|
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
|
||||||
cls=cls, module=module)
|
cls=cls, module=module)
|
||||||
try:
|
methods = []
|
||||||
methods = [module.pytest_generate_tests]
|
if hasattr(module, "pytest_generate_tests"):
|
||||||
except AttributeError:
|
methods.append(module.pytest_generate_tests)
|
||||||
methods = []
|
|
||||||
if hasattr(cls, "pytest_generate_tests"):
|
if hasattr(cls, "pytest_generate_tests"):
|
||||||
methods.append(cls().pytest_generate_tests)
|
methods.append(cls().pytest_generate_tests)
|
||||||
self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc)
|
if methods:
|
||||||
|
self.ihook.pytest_generate_tests.call_extra(methods,
|
||||||
|
dict(metafunc=metafunc))
|
||||||
|
else:
|
||||||
|
self.ihook.pytest_generate_tests(metafunc=metafunc)
|
||||||
|
|
||||||
Function = self._getcustomclass("Function")
|
Function = self._getcustomclass("Function")
|
||||||
if not metafunc._calls:
|
if not metafunc._calls:
|
||||||
|
@ -1621,7 +1624,6 @@ class FixtureManager:
|
||||||
self.session = session
|
self.session = session
|
||||||
self.config = session.config
|
self.config = session.config
|
||||||
self._arg2fixturedefs = {}
|
self._arg2fixturedefs = {}
|
||||||
self._seenplugins = set()
|
|
||||||
self._holderobjseen = set()
|
self._holderobjseen = set()
|
||||||
self._arg2finish = {}
|
self._arg2finish = {}
|
||||||
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
||||||
|
@ -1646,11 +1648,7 @@ class FixtureManager:
|
||||||
node)
|
node)
|
||||||
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
|
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
### XXX this hook should be called for historic events like pytest_configure
|
|
||||||
### so that we don't have to do the below pytest_configure hook
|
|
||||||
def pytest_plugin_registered(self, plugin):
|
def pytest_plugin_registered(self, plugin):
|
||||||
if plugin in self._seenplugins:
|
|
||||||
return
|
|
||||||
nodeid = None
|
nodeid = None
|
||||||
try:
|
try:
|
||||||
p = py.path.local(plugin.__file__)
|
p = py.path.local(plugin.__file__)
|
||||||
|
@ -1665,13 +1663,6 @@ class FixtureManager:
|
||||||
if p.sep != "/":
|
if p.sep != "/":
|
||||||
nodeid = nodeid.replace(p.sep, "/")
|
nodeid = nodeid.replace(p.sep, "/")
|
||||||
self.parsefactories(plugin, nodeid)
|
self.parsefactories(plugin, nodeid)
|
||||||
self._seenplugins.add(plugin)
|
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
|
||||||
def pytest_configure(self, config):
|
|
||||||
plugins = config.pluginmanager.getplugins()
|
|
||||||
for plugin in plugins:
|
|
||||||
self.pytest_plugin_registered(plugin)
|
|
||||||
|
|
||||||
def _getautousenames(self, nodeid):
|
def _getautousenames(self, nodeid):
|
||||||
""" return a tuple of fixture names to be used. """
|
""" return a tuple of fixture names to be used. """
|
||||||
|
|
|
@ -133,7 +133,7 @@ class MarkEvaluator:
|
||||||
return expl
|
return expl
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
evalskip = MarkEvaluator(item, 'skipif')
|
evalskip = MarkEvaluator(item, 'skipif')
|
||||||
if evalskip.istrue():
|
if evalskip.istrue():
|
||||||
|
@ -151,7 +151,7 @@ def check_xfail_no_run(item):
|
||||||
if not evalxfail.get('run', True):
|
if not evalxfail.get('run', True):
|
||||||
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
outcome = yield
|
outcome = yield
|
||||||
rep = outcome.get_result()
|
rep = outcome.get_result()
|
||||||
|
|
|
@ -164,6 +164,8 @@ class TerminalReporter:
|
||||||
|
|
||||||
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
||||||
warnings = self.stats.setdefault("warnings", [])
|
warnings = self.stats.setdefault("warnings", [])
|
||||||
|
if isinstance(fslocation, tuple):
|
||||||
|
fslocation = "%s:%d" % fslocation
|
||||||
warning = WarningReport(code=code, fslocation=fslocation,
|
warning = WarningReport(code=code, fslocation=fslocation,
|
||||||
message=message, nodeid=nodeid)
|
message=message, nodeid=nodeid)
|
||||||
warnings.append(warning)
|
warnings.append(warning)
|
||||||
|
@ -265,7 +267,7 @@ class TerminalReporter:
|
||||||
def pytest_collection_modifyitems(self):
|
def pytest_collection_modifyitems(self):
|
||||||
self.report_collect(True)
|
self.report_collect(True)
|
||||||
|
|
||||||
@pytest.mark.trylast
|
@pytest.hookimpl_opts(trylast=True)
|
||||||
def pytest_sessionstart(self, session):
|
def pytest_sessionstart(self, session):
|
||||||
self._sessionstarttime = time.time()
|
self._sessionstarttime = time.time()
|
||||||
if not self.showheader:
|
if not self.showheader:
|
||||||
|
@ -350,7 +352,7 @@ class TerminalReporter:
|
||||||
indent = (len(stack) - 1) * " "
|
indent = (len(stack) - 1) * " "
|
||||||
self._tw.line("%s%s" % (indent, col))
|
self._tw.line("%s%s" % (indent, col))
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_sessionfinish(self, exitstatus):
|
def pytest_sessionfinish(self, exitstatus):
|
||||||
outcome = yield
|
outcome = yield
|
||||||
outcome.get_result()
|
outcome.get_result()
|
||||||
|
|
|
@ -140,7 +140,7 @@ class TestCaseFunction(pytest.Function):
|
||||||
if traceback:
|
if traceback:
|
||||||
excinfo.traceback = traceback
|
excinfo.traceback = traceback
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_runtest_makereport(item, call):
|
def pytest_runtest_makereport(item, call):
|
||||||
if isinstance(item, TestCaseFunction):
|
if isinstance(item, TestCaseFunction):
|
||||||
if item._excinfo:
|
if item._excinfo:
|
||||||
|
@ -152,7 +152,7 @@ def pytest_runtest_makereport(item, call):
|
||||||
|
|
||||||
# twisted trial support
|
# twisted trial support
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_runtest_protocol(item):
|
def pytest_runtest_protocol(item):
|
||||||
if isinstance(item, TestCaseFunction) and \
|
if isinstance(item, TestCaseFunction) and \
|
||||||
'twisted.trial.unittest' in sys.modules:
|
'twisted.trial.unittest' in sys.modules:
|
||||||
|
|
|
@ -201,9 +201,9 @@ You can ask which markers exist for your test suite - the list includes our just
|
||||||
|
|
||||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
||||||
|
|
||||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
@pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||||
|
|
||||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
@pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||||
|
|
||||||
|
|
||||||
For an example on how to add and work with markers from a plugin, see
|
For an example on how to add and work with markers from a plugin, see
|
||||||
|
@ -375,9 +375,9 @@ The ``--markers`` option always gives you a list of available markers::
|
||||||
|
|
||||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
||||||
|
|
||||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
@pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||||
|
|
||||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
@pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||||
|
|
||||||
|
|
||||||
Reading markers which were set from multiple places
|
Reading markers which were set from multiple places
|
||||||
|
|
|
@ -534,7 +534,7 @@ case we just write some informations out to a ``failures`` file::
|
||||||
import pytest
|
import pytest
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_runtest_makereport(item, call, __multicall__):
|
def pytest_runtest_makereport(item, call, __multicall__):
|
||||||
# execute all other hooks to obtain the report object
|
# execute all other hooks to obtain the report object
|
||||||
rep = __multicall__.execute()
|
rep = __multicall__.execute()
|
||||||
|
@ -607,7 +607,7 @@ here is a little example implemented via a local plugin::
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.hookimpl_opts(tryfirst=True)
|
||||||
def pytest_runtest_makereport(item, call, __multicall__):
|
def pytest_runtest_makereport(item, call, __multicall__):
|
||||||
# execute all other hooks to obtain the report object
|
# execute all other hooks to obtain the report object
|
||||||
rep = __multicall__.execute()
|
rep = __multicall__.execute()
|
||||||
|
|
|
@ -56,6 +56,7 @@ pytest: helps you write better programs
|
||||||
- all collection, reporting, running aspects are delegated to hook functions
|
- all collection, reporting, running aspects are delegated to hook functions
|
||||||
- customizations can be per-directory, per-project or per PyPI released plugin
|
- customizations can be per-directory, per-project or per PyPI released plugin
|
||||||
- it is easy to add command line options or customize existing behaviour
|
- it is easy to add command line options or customize existing behaviour
|
||||||
|
- :ref:`easy to write your own plugins <writing-plugins>`
|
||||||
|
|
||||||
|
|
||||||
.. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html
|
.. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html
|
||||||
|
|
|
@ -1,64 +1,14 @@
|
||||||
.. _plugins:
|
|
||||||
|
|
||||||
Working with plugins and conftest files
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
``pytest`` implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic location types:
|
|
||||||
|
|
||||||
* `builtin plugins`_: loaded from pytest's internal ``_pytest`` directory.
|
|
||||||
* `external plugins`_: modules discovered through `setuptools entry points`_
|
|
||||||
* `conftest.py plugins`_: modules auto-discovered in test directories
|
|
||||||
|
|
||||||
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
|
||||||
.. _`conftest.py plugins`:
|
|
||||||
.. _`conftest.py`:
|
|
||||||
.. _`localplugin`:
|
|
||||||
.. _`conftest`:
|
|
||||||
|
|
||||||
conftest.py: local per-directory plugins
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
local ``conftest.py`` plugins contain directory-specific hook
|
|
||||||
implementations. Session and test running activities will
|
|
||||||
invoke all hooks defined in ``conftest.py`` files closer to the
|
|
||||||
root of the filesystem. Example: Assume the following layout
|
|
||||||
and content of files::
|
|
||||||
|
|
||||||
a/conftest.py:
|
|
||||||
def pytest_runtest_setup(item):
|
|
||||||
# called for running each test in 'a' directory
|
|
||||||
print ("setting up", item)
|
|
||||||
|
|
||||||
a/test_sub.py:
|
|
||||||
def test_sub():
|
|
||||||
pass
|
|
||||||
|
|
||||||
test_flat.py:
|
|
||||||
def test_flat():
|
|
||||||
pass
|
|
||||||
|
|
||||||
Here is how you might run it::
|
|
||||||
|
|
||||||
py.test test_flat.py # will not show "setting up"
|
|
||||||
py.test a/test_sub.py # will show "setting up"
|
|
||||||
|
|
||||||
.. Note::
|
|
||||||
If you have ``conftest.py`` files which do not reside in a
|
|
||||||
python package directory (i.e. one containing an ``__init__.py``) then
|
|
||||||
"import conftest" can be ambiguous because there might be other
|
|
||||||
``conftest.py`` files as well on your PYTHONPATH or ``sys.path``.
|
|
||||||
It is thus good practise for projects to either put ``conftest.py``
|
|
||||||
under a package scope or to never import anything from a
|
|
||||||
conftest.py file.
|
|
||||||
|
|
||||||
.. _`external plugins`:
|
.. _`external plugins`:
|
||||||
.. _`extplugins`:
|
.. _`extplugins`:
|
||||||
|
.. _`using plugins`:
|
||||||
|
|
||||||
Installing External Plugins / Searching
|
Installing and Using plugins
|
||||||
---------------------------------------
|
============================
|
||||||
|
|
||||||
Installing a plugin happens through any usual Python installation
|
This section talks about installing and using third party plugins.
|
||||||
tool, for example::
|
For writing your own plugins, please refer to :ref:`writing-plugins`.
|
||||||
|
|
||||||
|
Installing a third party plugin can be easily done with ``pip``::
|
||||||
|
|
||||||
pip install pytest-NAME
|
pip install pytest-NAME
|
||||||
pip uninstall pytest-NAME
|
pip uninstall pytest-NAME
|
||||||
|
@ -120,118 +70,20 @@ You may also discover more plugins through a `pytest- pypi.python.org search`_.
|
||||||
.. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search
|
.. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search
|
||||||
|
|
||||||
|
|
||||||
Writing a plugin by looking at examples
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. _`setuptools`: http://pypi.python.org/pypi/setuptools
|
|
||||||
|
|
||||||
If you want to write a plugin, there are many real-life examples
|
|
||||||
you can copy from:
|
|
||||||
|
|
||||||
* a custom collection example plugin: :ref:`yaml plugin`
|
|
||||||
* around 20 `builtin plugins`_ which provide pytest's own functionality
|
|
||||||
* many `external plugins`_ providing additional features
|
|
||||||
|
|
||||||
All of these plugins implement the documented `well specified hooks`_
|
|
||||||
to extend and add functionality.
|
|
||||||
|
|
||||||
You can also :ref:`contribute your plugin to pytest-dev<submitplugin>`
|
|
||||||
once it has some happy users other than yourself.
|
|
||||||
|
|
||||||
|
|
||||||
.. _`setuptools entry points`:
|
|
||||||
|
|
||||||
Making your plugin installable by others
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
If you want to make your plugin externally available, you
|
|
||||||
may define a so-called entry point for your distribution so
|
|
||||||
that ``pytest`` finds your plugin module. Entry points are
|
|
||||||
a feature that is provided by `setuptools`_. pytest looks up
|
|
||||||
the ``pytest11`` entrypoint to discover its
|
|
||||||
plugins and you can thus make your plugin available by defining
|
|
||||||
it in your setuptools-invocation:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
# sample ./setup.py file
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="myproject",
|
|
||||||
packages = ['myproject']
|
|
||||||
|
|
||||||
# the following makes a plugin available to pytest
|
|
||||||
entry_points = {
|
|
||||||
'pytest11': [
|
|
||||||
'name_of_plugin = myproject.pluginmodule',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
If a package is installed this way, ``pytest`` will load
|
|
||||||
``myproject.pluginmodule`` as a plugin which can define
|
|
||||||
`well specified hooks`_.
|
|
||||||
|
|
||||||
|
|
||||||
.. _`pluginorder`:
|
|
||||||
|
|
||||||
Plugin discovery order at tool startup
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
``pytest`` loads plugin modules at tool startup in the following way:
|
|
||||||
|
|
||||||
* by loading all builtin plugins
|
|
||||||
|
|
||||||
* by loading all plugins registered through `setuptools entry points`_.
|
|
||||||
|
|
||||||
* by pre-scanning the command line for the ``-p name`` option
|
|
||||||
and loading the specified plugin before actual command line parsing.
|
|
||||||
|
|
||||||
* by loading all :file:`conftest.py` files as inferred by the command line
|
|
||||||
invocation:
|
|
||||||
|
|
||||||
- if no test paths are specified use current dir as a test path
|
|
||||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
|
||||||
to the directory part of the first test path.
|
|
||||||
|
|
||||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
|
||||||
sub directories at tool startup. It is usually a good idea to keep
|
|
||||||
your conftest.py file in the top level test or project root directory.
|
|
||||||
|
|
||||||
* by recursively loading all plugins specified by the
|
|
||||||
``pytest_plugins`` variable in ``conftest.py`` files
|
|
||||||
|
|
||||||
|
|
||||||
Requiring/Loading plugins in a test module or conftest file
|
Requiring/Loading plugins in a test module or conftest file
|
||||||
-----------------------------------------------------------
|
-----------------------------------------------------------
|
||||||
|
|
||||||
You can require plugins in a test module or a conftest file like this::
|
You can require plugins in a test module or a conftest file like this::
|
||||||
|
|
||||||
pytest_plugins = "name1", "name2",
|
pytest_plugins = "myapp.testsupport.myplugin",
|
||||||
|
|
||||||
When the test module or conftest plugin is loaded the specified plugins
|
When the test module or conftest plugin is loaded the specified plugins
|
||||||
will be loaded as well. You can also use dotted path like this::
|
will be loaded as well.
|
||||||
|
|
||||||
pytest_plugins = "myapp.testsupport.myplugin"
|
pytest_plugins = "myapp.testsupport.myplugin"
|
||||||
|
|
||||||
which will import the specified module as a ``pytest`` plugin.
|
which will import the specified module as a ``pytest`` plugin.
|
||||||
|
|
||||||
|
|
||||||
Accessing another plugin by name
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
If a plugin wants to collaborate with code from
|
|
||||||
another plugin it can obtain a reference through
|
|
||||||
the plugin manager like this:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
plugin = config.pluginmanager.getplugin("name_of_plugin")
|
|
||||||
|
|
||||||
If you want to look at the names of existing plugins, use
|
|
||||||
the ``--traceconfig`` option.
|
|
||||||
|
|
||||||
.. _`findpluginname`:
|
.. _`findpluginname`:
|
||||||
|
|
||||||
Finding out which plugins are active
|
Finding out which plugins are active
|
||||||
|
@ -293,223 +145,3 @@ in the `pytest repository <http://bitbucket.org/pytest-dev/pytest/>`_.
|
||||||
_pytest.tmpdir
|
_pytest.tmpdir
|
||||||
_pytest.unittest
|
_pytest.unittest
|
||||||
|
|
||||||
.. _`well specified hooks`:
|
|
||||||
|
|
||||||
pytest hook reference
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Hook specification and validation
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
``pytest`` calls hook functions to implement initialization, running,
|
|
||||||
test execution and reporting. When ``pytest`` loads a plugin it validates
|
|
||||||
that each hook function conforms to its respective hook specification.
|
|
||||||
Each hook function name and its argument names need to match a hook
|
|
||||||
specification. However, a hook function may accept *fewer* parameters
|
|
||||||
by simply not specifying them. If you mistype argument names or the
|
|
||||||
hook name itself you get an error showing the available arguments.
|
|
||||||
|
|
||||||
Initialization, command line and configuration hooks
|
|
||||||
----------------------------------------------------
|
|
||||||
|
|
||||||
.. currentmodule:: _pytest.hookspec
|
|
||||||
|
|
||||||
.. autofunction:: pytest_load_initial_conftests
|
|
||||||
.. autofunction:: pytest_cmdline_preparse
|
|
||||||
.. autofunction:: pytest_cmdline_parse
|
|
||||||
.. autofunction:: pytest_namespace
|
|
||||||
.. autofunction:: pytest_addoption
|
|
||||||
.. autofunction:: pytest_cmdline_main
|
|
||||||
.. autofunction:: pytest_configure
|
|
||||||
.. autofunction:: pytest_unconfigure
|
|
||||||
|
|
||||||
Generic "runtest" hooks
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
All runtest related hooks receive a :py:class:`pytest.Item` object.
|
|
||||||
|
|
||||||
.. autofunction:: pytest_runtest_protocol
|
|
||||||
.. autofunction:: pytest_runtest_setup
|
|
||||||
.. autofunction:: pytest_runtest_call
|
|
||||||
.. autofunction:: pytest_runtest_teardown
|
|
||||||
.. autofunction:: pytest_runtest_makereport
|
|
||||||
|
|
||||||
For deeper understanding you may look at the default implementation of
|
|
||||||
these hooks in :py:mod:`_pytest.runner` and maybe also
|
|
||||||
in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture`
|
|
||||||
and its input/output capturing in order to immediately drop
|
|
||||||
into interactive debugging when a test failure occurs.
|
|
||||||
|
|
||||||
The :py:mod:`_pytest.terminal` reported specifically uses
|
|
||||||
the reporting hook to print information about a test run.
|
|
||||||
|
|
||||||
Collection hooks
|
|
||||||
----------------
|
|
||||||
|
|
||||||
``pytest`` calls the following hooks for collecting files and directories:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_ignore_collect
|
|
||||||
.. autofunction:: pytest_collect_directory
|
|
||||||
.. autofunction:: pytest_collect_file
|
|
||||||
|
|
||||||
For influencing the collection of objects in Python modules
|
|
||||||
you can use the following hook:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_pycollect_makeitem
|
|
||||||
.. autofunction:: pytest_generate_tests
|
|
||||||
|
|
||||||
After collection is complete, you can modify the order of
|
|
||||||
items, delete or otherwise amend the test items:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_collection_modifyitems
|
|
||||||
|
|
||||||
Reporting hooks
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Session related reporting hooks:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_collectstart
|
|
||||||
.. autofunction:: pytest_itemcollected
|
|
||||||
.. autofunction:: pytest_collectreport
|
|
||||||
.. autofunction:: pytest_deselected
|
|
||||||
|
|
||||||
And here is the central hook for reporting about
|
|
||||||
test execution:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_runtest_logreport
|
|
||||||
|
|
||||||
|
|
||||||
Debugging/Interaction hooks
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
There are few hooks which can be used for special
|
|
||||||
reporting or interaction with exceptions:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_internalerror
|
|
||||||
.. autofunction:: pytest_keyboard_interrupt
|
|
||||||
.. autofunction:: pytest_exception_interact
|
|
||||||
|
|
||||||
|
|
||||||
Declaring new hooks
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
Plugins and ``conftest.py`` files may declare new hooks that can then be
|
|
||||||
implemented by other plugins in order to alter behaviour or interact with
|
|
||||||
the new plugin:
|
|
||||||
|
|
||||||
.. autofunction:: pytest_addhooks
|
|
||||||
|
|
||||||
Hooks are usually declared as do-nothing functions that contain only
|
|
||||||
documentation describing when the hook will be called and what return values
|
|
||||||
are expected.
|
|
||||||
|
|
||||||
For an example, see `newhooks.py`_ from :ref:`xdist`.
|
|
||||||
|
|
||||||
.. _`newhooks.py`: https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default
|
|
||||||
|
|
||||||
|
|
||||||
Using hooks from 3rd party plugins
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
Using new hooks from plugins as explained above might be a little tricky
|
|
||||||
because the standard `Hook specification and validation`_ mechanism:
|
|
||||||
if you depend on a plugin that is not installed,
|
|
||||||
validation will fail and the error message will not make much sense to your users.
|
|
||||||
|
|
||||||
One approach is to defer the hook implementation to a new plugin instead of
|
|
||||||
declaring the hook functions directly in your plugin module, for example::
|
|
||||||
|
|
||||||
# contents of myplugin.py
|
|
||||||
|
|
||||||
class DeferPlugin(object):
|
|
||||||
"""Simple plugin to defer pytest-xdist hook functions."""
|
|
||||||
|
|
||||||
def pytest_testnodedown(self, node, error):
|
|
||||||
"""standard xdist hook function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
|
||||||
if config.pluginmanager.hasplugin('xdist'):
|
|
||||||
config.pluginmanager.register(DeferPlugin())
|
|
||||||
|
|
||||||
|
|
||||||
This has the added benefit of allowing you to conditionally install hooks
|
|
||||||
depending on which plugins are installed.
|
|
||||||
|
|
||||||
hookwrapper: executing around other hooks
|
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
.. currentmodule:: _pytest.core
|
|
||||||
|
|
||||||
.. versionadded:: 2.7 (experimental)
|
|
||||||
|
|
||||||
pytest plugins can implement hook wrappers which which wrap the execution
|
|
||||||
of other hook implementations. A hook wrapper is a generator function
|
|
||||||
which yields exactly once. When pytest invokes hooks it first executes
|
|
||||||
hook wrappers and passes the same arguments as to the regular hooks.
|
|
||||||
|
|
||||||
At the yield point of the hook wrapper pytest will execute the next hook
|
|
||||||
implementations and return their result to the yield point in the form of
|
|
||||||
a :py:class:`CallOutcome` instance which encapsulates a result or
|
|
||||||
exception info. The yield point itself will thus typically not raise
|
|
||||||
exceptions (unless there are bugs).
|
|
||||||
|
|
||||||
Here is an example definition of a hook wrapper::
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
@pytest.mark.hookwrapper
|
|
||||||
def pytest_pyfunc_call(pyfuncitem):
|
|
||||||
# do whatever you want before the next hook executes
|
|
||||||
outcome = yield
|
|
||||||
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
|
||||||
res = outcome.get_result() # will raise if outcome was exception
|
|
||||||
# postprocess result
|
|
||||||
|
|
||||||
Note that hook wrappers don't return results themselves, they merely
|
|
||||||
perform tracing or other side effects around the actual hook implementations.
|
|
||||||
If the result of the underlying hook is a mutable object, they may modify
|
|
||||||
that result, however.
|
|
||||||
|
|
||||||
|
|
||||||
Reference of objects involved in hooks
|
|
||||||
======================================
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.config.Config()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.config.Parser()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.main.Node()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.main.Collector()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.main.Item()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.Module()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.Class()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.Function()
|
|
||||||
:members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.runner.CallInfo()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.runner.TestReport()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.core.CallOutcome()
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,495 @@
|
||||||
|
.. _plugins:
|
||||||
|
.. _`writing-plugins`:
|
||||||
|
|
||||||
|
Writing plugins
|
||||||
|
===============
|
||||||
|
|
||||||
|
It is easy to implement `local conftest plugins`_ for your own project
|
||||||
|
or `pip-installable plugins`_ that can be used throughout many projects,
|
||||||
|
including third party projects. Please refer to :ref:`using plugins` if you
|
||||||
|
only want to use but not write plugins.
|
||||||
|
|
||||||
|
A plugin contains one or multiple hook functions. :ref:`Writing hooks <writinghooks>`
|
||||||
|
explains the basics and details of how you can write a hook function yourself.
|
||||||
|
``pytest`` implements all aspects of configuration, collection, running and
|
||||||
|
reporting by calling `well specified hooks`_ of the following plugins:
|
||||||
|
|
||||||
|
* :ref:`builtin plugins`: loaded from pytest's internal ``_pytest`` directory.
|
||||||
|
|
||||||
|
* :ref:`external plugins <extplugin>`: modules discovered through
|
||||||
|
`setuptools entry points`_
|
||||||
|
|
||||||
|
* `conftest.py plugins`_: modules auto-discovered in test directories
|
||||||
|
|
||||||
|
In principle, each hook call is a ``1:N`` Python function call where ``N`` is the
|
||||||
|
number of registered implementation functions for a given specification.
|
||||||
|
All specifications and implementations following the ``pytest_`` prefix
|
||||||
|
naming convention, making them easy to distinguish and find.
|
||||||
|
|
||||||
|
.. _`pluginorder`:
|
||||||
|
|
||||||
|
Plugin discovery order at tool startup
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
``pytest`` loads plugin modules at tool startup in the following way:
|
||||||
|
|
||||||
|
* by loading all builtin plugins
|
||||||
|
|
||||||
|
* by loading all plugins registered through `setuptools entry points`_.
|
||||||
|
|
||||||
|
* by pre-scanning the command line for the ``-p name`` option
|
||||||
|
and loading the specified plugin before actual command line parsing.
|
||||||
|
|
||||||
|
* by loading all :file:`conftest.py` files as inferred by the command line
|
||||||
|
invocation:
|
||||||
|
|
||||||
|
- if no test paths are specified use current dir as a test path
|
||||||
|
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||||
|
to the directory part of the first test path.
|
||||||
|
|
||||||
|
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||||
|
sub directories at tool startup. It is usually a good idea to keep
|
||||||
|
your conftest.py file in the top level test or project root directory.
|
||||||
|
|
||||||
|
* by recursively loading all plugins specified by the
|
||||||
|
``pytest_plugins`` variable in ``conftest.py`` files
|
||||||
|
|
||||||
|
|
||||||
|
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
||||||
|
.. _`conftest.py plugins`:
|
||||||
|
.. _`conftest.py`:
|
||||||
|
.. _`localplugin`:
|
||||||
|
.. _`conftest`:
|
||||||
|
.. _`local conftest plugins`:
|
||||||
|
|
||||||
|
conftest.py: local per-directory plugins
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
Local ``conftest.py`` plugins contain directory-specific hook
|
||||||
|
implementations. Hook Session and test running activities will
|
||||||
|
invoke all hooks defined in ``conftest.py`` files closer to the
|
||||||
|
root of the filesystem. Example of implementing the
|
||||||
|
``pytest_runtest_setup`` hook so that is called for tests in the ``a``
|
||||||
|
sub directory but not for other directories::
|
||||||
|
|
||||||
|
a/conftest.py:
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
# called for running each test in 'a' directory
|
||||||
|
print ("setting up", item)
|
||||||
|
|
||||||
|
a/test_sub.py:
|
||||||
|
def test_sub():
|
||||||
|
pass
|
||||||
|
|
||||||
|
test_flat.py:
|
||||||
|
def test_flat():
|
||||||
|
pass
|
||||||
|
|
||||||
|
Here is how you might run it::
|
||||||
|
|
||||||
|
py.test test_flat.py # will not show "setting up"
|
||||||
|
py.test a/test_sub.py # will show "setting up"
|
||||||
|
|
||||||
|
.. Note::
|
||||||
|
If you have ``conftest.py`` files which do not reside in a
|
||||||
|
python package directory (i.e. one containing an ``__init__.py``) then
|
||||||
|
"import conftest" can be ambiguous because there might be other
|
||||||
|
``conftest.py`` files as well on your PYTHONPATH or ``sys.path``.
|
||||||
|
It is thus good practise for projects to either put ``conftest.py``
|
||||||
|
under a package scope or to never import anything from a
|
||||||
|
conftest.py file.
|
||||||
|
|
||||||
|
|
||||||
|
Writing a plugin by looking at examples
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. _`setuptools`: http://pypi.python.org/pypi/setuptools
|
||||||
|
|
||||||
|
If you want to write a plugin, there are many real-life examples
|
||||||
|
you can copy from:
|
||||||
|
|
||||||
|
* a custom collection example plugin: :ref:`yaml plugin`
|
||||||
|
* around 20 doc:`builtin plugins` which provide pytest's own functionality
|
||||||
|
* many :doc:`external plugins` providing additional features
|
||||||
|
|
||||||
|
All of these plugins implement the documented `well specified hooks`_
|
||||||
|
to extend and add functionality.
|
||||||
|
|
||||||
|
You can also :ref:`contribute your plugin to pytest-dev<submitplugin>`
|
||||||
|
once it has some happy users other than yourself.
|
||||||
|
|
||||||
|
|
||||||
|
.. _`setuptools entry points`:
|
||||||
|
.. _`pip-installable plugins`:
|
||||||
|
|
||||||
|
Making your plugin installable by others
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
If you want to make your plugin externally available, you
|
||||||
|
may define a so-called entry point for your distribution so
|
||||||
|
that ``pytest`` finds your plugin module. Entry points are
|
||||||
|
a feature that is provided by `setuptools`_. pytest looks up
|
||||||
|
the ``pytest11`` entrypoint to discover its
|
||||||
|
plugins and you can thus make your plugin available by defining
|
||||||
|
it in your setuptools-invocation:
|
||||||
|
|
||||||
|
.. sourcecode:: python
|
||||||
|
|
||||||
|
# sample ./setup.py file
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="myproject",
|
||||||
|
packages = ['myproject']
|
||||||
|
|
||||||
|
# the following makes a plugin available to pytest
|
||||||
|
entry_points = {
|
||||||
|
'pytest11': [
|
||||||
|
'name_of_plugin = myproject.pluginmodule',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
If a package is installed this way, ``pytest`` will load
|
||||||
|
``myproject.pluginmodule`` as a plugin which can define
|
||||||
|
`well specified hooks`_.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Requiring/Loading plugins in a test module or conftest file
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
You can require plugins in a test module or a conftest file like this::
|
||||||
|
|
||||||
|
pytest_plugins = "name1", "name2",
|
||||||
|
|
||||||
|
When the test module or conftest plugin is loaded the specified plugins
|
||||||
|
will be loaded as well. You can also use dotted path like this::
|
||||||
|
|
||||||
|
pytest_plugins = "myapp.testsupport.myplugin"
|
||||||
|
|
||||||
|
which will import the specified module as a ``pytest`` plugin.
|
||||||
|
|
||||||
|
|
||||||
|
Accessing another plugin by name
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
If a plugin wants to collaborate with code from
|
||||||
|
another plugin it can obtain a reference through
|
||||||
|
the plugin manager like this:
|
||||||
|
|
||||||
|
.. sourcecode:: python
|
||||||
|
|
||||||
|
plugin = config.pluginmanager.getplugin("name_of_plugin")
|
||||||
|
|
||||||
|
If you want to look at the names of existing plugins, use
|
||||||
|
the ``--traceconfig`` option.
|
||||||
|
|
||||||
|
|
||||||
|
.. _`writinghooks`:
|
||||||
|
|
||||||
|
Writing hook functions
|
||||||
|
======================
|
||||||
|
|
||||||
|
.. _validation:
|
||||||
|
|
||||||
|
hook function validation and execution
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
pytest calls hook functions from registered plugins for any
|
||||||
|
given hook specification. Let's look at a typical hook function
|
||||||
|
for the ``pytest_collection_modifyitems(session, config,
|
||||||
|
items)`` hook which pytest calls after collection of all test items is
|
||||||
|
completed.
|
||||||
|
|
||||||
|
When we implement a ``pytest_collection_modifyitems`` function in our plugin
|
||||||
|
pytest will during registration verify that you use argument
|
||||||
|
names which match the specification and bail out if not.
|
||||||
|
|
||||||
|
Let's look at a possible implementation::
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(config, items):
|
||||||
|
# called after collectin is completed
|
||||||
|
# you can modify the ``items`` list
|
||||||
|
|
||||||
|
Here, ``pytest`` will pass in ``config`` (the pytest config object)
|
||||||
|
and ``items`` (the list of collected test items) but will not pass
|
||||||
|
in the ``session`` argument because we didn't list it in the function
|
||||||
|
signature. This dynamic "pruning" of arguments allows ``pytest`` to
|
||||||
|
be "future-compatible": we can introduce new hook named parameters without
|
||||||
|
breaking the signatures of existing hook implementations. It is one of
|
||||||
|
the reasons for the general long-lived compatibility of pytest plugins.
|
||||||
|
|
||||||
|
Note that hook functions other than ``pytest_runtest_*`` are not
|
||||||
|
allowed to raise exceptions. Doing so will break the pytest run.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
firstresult: stop at first non-None result
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Most calls to ``pytest`` hooks result in a **list of results** which contains
|
||||||
|
all non-None results of the called hook functions.
|
||||||
|
|
||||||
|
Some hook specifications use the ``firstresult=True`` option so that the hook
|
||||||
|
call only executes until the first of N registered functions returns a
|
||||||
|
non-None result which is then taken as result of the overall hook call.
|
||||||
|
The remaining hook functions will not be called in this case.
|
||||||
|
|
||||||
|
|
||||||
|
hookwrapper: executing around other hooks
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
|
.. currentmodule:: _pytest.core
|
||||||
|
|
||||||
|
.. versionadded:: 2.7 (experimental)
|
||||||
|
|
||||||
|
pytest plugins can implement hook wrappers which wrap the execution
|
||||||
|
of other hook implementations. A hook wrapper is a generator function
|
||||||
|
which yields exactly once. When pytest invokes hooks it first executes
|
||||||
|
hook wrappers and passes the same arguments as to the regular hooks.
|
||||||
|
|
||||||
|
At the yield point of the hook wrapper pytest will execute the next hook
|
||||||
|
implementations and return their result to the yield point in the form of
|
||||||
|
a :py:class:`CallOutcome` instance which encapsulates a result or
|
||||||
|
exception info. The yield point itself will thus typically not raise
|
||||||
|
exceptions (unless there are bugs).
|
||||||
|
|
||||||
|
Here is an example definition of a hook wrapper::
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
|
def pytest_pyfunc_call(pyfuncitem):
|
||||||
|
# do whatever you want before the next hook executes
|
||||||
|
|
||||||
|
outcome = yield
|
||||||
|
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
||||||
|
|
||||||
|
res = outcome.get_result() # will raise if outcome was exception
|
||||||
|
# postprocess result
|
||||||
|
|
||||||
|
Note that hook wrappers don't return results themselves, they merely
|
||||||
|
perform tracing or other side effects around the actual hook implementations.
|
||||||
|
If the result of the underlying hook is a mutable object, they may modify
|
||||||
|
that result, however.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Hook function ordering / call example
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
For any given hook specification there may be more than one
|
||||||
|
implementation and we thus generally view ``hook`` execution as a
|
||||||
|
``1:N`` function call where ``N`` is the number of registered functions.
|
||||||
|
There are ways to influence if a hook implementation comes before or
|
||||||
|
after others, i.e. the position in the ``N``-sized list of functions::
|
||||||
|
|
||||||
|
# Plugin 1
|
||||||
|
@pytest.hookimpl_spec(tryfirst=True)
|
||||||
|
def pytest_collection_modifyitems(items):
|
||||||
|
# will execute as early as possible
|
||||||
|
|
||||||
|
# Plugin 2
|
||||||
|
@pytest.hookimpl_spec(trylast=True)
|
||||||
|
def pytest_collection_modifyitems(items):
|
||||||
|
# will execute as late as possible
|
||||||
|
|
||||||
|
# Plugin 3
|
||||||
|
@pytest.hookimpl_spec(hookwrapper=True)
|
||||||
|
def pytest_collection_modifyitems(items):
|
||||||
|
# will execute even before the tryfirst one above!
|
||||||
|
outcome = yield
|
||||||
|
# will execute after all non-hookwrappers executed
|
||||||
|
|
||||||
|
Here is the order of execution:
|
||||||
|
|
||||||
|
1. Plugin3's pytest_collection_modifyitems called until the yield point
|
||||||
|
2. Plugin1's pytest_collection_modifyitems is called
|
||||||
|
3. Plugin2's pytest_collection_modifyitems is called
|
||||||
|
4. Plugin3's pytest_collection_modifyitems called for executing after the yield
|
||||||
|
The yield receives a :py:class:`CallOutcome` instance which encapsulates
|
||||||
|
the result from calling the non-wrappers. Wrappers cannot modify the result.
|
||||||
|
|
||||||
|
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
|
||||||
|
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
|
||||||
|
among each other.
|
||||||
|
|
||||||
|
Declaring new hooks
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. currentmodule:: _pytest.hookspec
|
||||||
|
|
||||||
|
Plugins and ``conftest.py`` files may declare new hooks that can then be
|
||||||
|
implemented by other plugins in order to alter behaviour or interact with
|
||||||
|
the new plugin:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_addhooks
|
||||||
|
|
||||||
|
Hooks are usually declared as do-nothing functions that contain only
|
||||||
|
documentation describing when the hook will be called and what return values
|
||||||
|
are expected.
|
||||||
|
|
||||||
|
For an example, see `newhooks.py`_ from :ref:`xdist`.
|
||||||
|
|
||||||
|
.. _`newhooks.py`: https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default
|
||||||
|
|
||||||
|
|
||||||
|
Using hooks from 3rd party plugins
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
Using new hooks from plugins as explained above might be a little tricky
|
||||||
|
because the standard :ref:`validation mechanism <validation>`:
|
||||||
|
if you depend on a plugin that is not installed, validation will fail and
|
||||||
|
the error message will not make much sense to your users.
|
||||||
|
|
||||||
|
One approach is to defer the hook implementation to a new plugin instead of
|
||||||
|
declaring the hook functions directly in your plugin module, for example::
|
||||||
|
|
||||||
|
# contents of myplugin.py
|
||||||
|
|
||||||
|
class DeferPlugin(object):
|
||||||
|
"""Simple plugin to defer pytest-xdist hook functions."""
|
||||||
|
|
||||||
|
def pytest_testnodedown(self, node, error):
|
||||||
|
"""standard xdist hook function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
if config.pluginmanager.hasplugin('xdist'):
|
||||||
|
config.pluginmanager.register(DeferPlugin())
|
||||||
|
|
||||||
|
This has the added benefit of allowing you to conditionally install hooks
|
||||||
|
depending on which plugins are installed.
|
||||||
|
|
||||||
|
|
||||||
|
.. _`well specified hooks`:
|
||||||
|
|
||||||
|
.. currentmodule:: _pytest.hookspec
|
||||||
|
|
||||||
|
pytest hook reference
|
||||||
|
=====================
|
||||||
|
|
||||||
|
|
||||||
|
Initialization, command line and configuration hooks
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
|
.. autofunction:: pytest_load_initial_conftests
|
||||||
|
.. autofunction:: pytest_cmdline_preparse
|
||||||
|
.. autofunction:: pytest_cmdline_parse
|
||||||
|
.. autofunction:: pytest_namespace
|
||||||
|
.. autofunction:: pytest_addoption
|
||||||
|
.. autofunction:: pytest_cmdline_main
|
||||||
|
.. autofunction:: pytest_configure
|
||||||
|
.. autofunction:: pytest_unconfigure
|
||||||
|
|
||||||
|
Generic "runtest" hooks
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
All runtest related hooks receive a :py:class:`pytest.Item` object.
|
||||||
|
|
||||||
|
.. autofunction:: pytest_runtest_protocol
|
||||||
|
.. autofunction:: pytest_runtest_setup
|
||||||
|
.. autofunction:: pytest_runtest_call
|
||||||
|
.. autofunction:: pytest_runtest_teardown
|
||||||
|
.. autofunction:: pytest_runtest_makereport
|
||||||
|
|
||||||
|
For deeper understanding you may look at the default implementation of
|
||||||
|
these hooks in :py:mod:`_pytest.runner` and maybe also
|
||||||
|
in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture`
|
||||||
|
and its input/output capturing in order to immediately drop
|
||||||
|
into interactive debugging when a test failure occurs.
|
||||||
|
|
||||||
|
The :py:mod:`_pytest.terminal` reported specifically uses
|
||||||
|
the reporting hook to print information about a test run.
|
||||||
|
|
||||||
|
Collection hooks
|
||||||
|
----------------
|
||||||
|
|
||||||
|
``pytest`` calls the following hooks for collecting files and directories:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_ignore_collect
|
||||||
|
.. autofunction:: pytest_collect_directory
|
||||||
|
.. autofunction:: pytest_collect_file
|
||||||
|
|
||||||
|
For influencing the collection of objects in Python modules
|
||||||
|
you can use the following hook:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_pycollect_makeitem
|
||||||
|
.. autofunction:: pytest_generate_tests
|
||||||
|
|
||||||
|
After collection is complete, you can modify the order of
|
||||||
|
items, delete or otherwise amend the test items:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_collection_modifyitems
|
||||||
|
|
||||||
|
Reporting hooks
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Session related reporting hooks:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_collectstart
|
||||||
|
.. autofunction:: pytest_itemcollected
|
||||||
|
.. autofunction:: pytest_collectreport
|
||||||
|
.. autofunction:: pytest_deselected
|
||||||
|
|
||||||
|
And here is the central hook for reporting about
|
||||||
|
test execution:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_runtest_logreport
|
||||||
|
|
||||||
|
|
||||||
|
Debugging/Interaction hooks
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
There are few hooks which can be used for special
|
||||||
|
reporting or interaction with exceptions:
|
||||||
|
|
||||||
|
.. autofunction:: pytest_internalerror
|
||||||
|
.. autofunction:: pytest_keyboard_interrupt
|
||||||
|
.. autofunction:: pytest_exception_interact
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reference of objects involved in hooks
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.config.Config()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.config.Parser()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.main.Node()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.main.Collector()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.main.Item()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.Module()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.Class()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.Function()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.runner.CallInfo()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.runner.TestReport()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.core.CallOutcome()
|
||||||
|
:members:
|
||||||
|
|
|
@ -12,6 +12,7 @@ if __name__ == '__main__': # if run as a script or by 'python -m pytest'
|
||||||
# else we are imported
|
# else we are imported
|
||||||
|
|
||||||
from _pytest.config import main, UsageError, _preloadplugins, cmdline
|
from _pytest.config import main, UsageError, _preloadplugins, cmdline
|
||||||
|
from _pytest.core import hookspec_opts, hookimpl_opts
|
||||||
from _pytest import __version__
|
from _pytest import __version__
|
||||||
|
|
||||||
_preloadplugins() # to populate pytest.* namespace so help(pytest) works
|
_preloadplugins() # to populate pytest.* namespace so help(pytest) works
|
||||||
|
|
|
@ -66,13 +66,12 @@ def check_open_files(config):
|
||||||
error.append(error[0])
|
error.append(error[0])
|
||||||
raise AssertionError("\n".join(error))
|
raise AssertionError("\n".join(error))
|
||||||
|
|
||||||
@pytest.mark.trylast
|
@pytest.hookimpl_opts(hookwrapper=True, trylast=True)
|
||||||
def pytest_runtest_teardown(item, __multicall__):
|
def pytest_runtest_teardown(item):
|
||||||
|
yield
|
||||||
item.config._basedir.chdir()
|
item.config._basedir.chdir()
|
||||||
if hasattr(item.config, '_openfiles'):
|
if hasattr(item.config, '_openfiles'):
|
||||||
x = __multicall__.execute()
|
|
||||||
check_open_files(item.config)
|
check_open_files(item.config)
|
||||||
return x
|
|
||||||
|
|
||||||
# XXX copied from execnet's conftest.py - needs to be merged
|
# XXX copied from execnet's conftest.py - needs to be merged
|
||||||
winpymap = {
|
winpymap = {
|
||||||
|
|
|
@ -563,7 +563,7 @@ class TestConftestCustomization:
|
||||||
b = testdir.mkdir("a").mkdir("b")
|
b = testdir.mkdir("a").mkdir("b")
|
||||||
b.join("conftest.py").write(py.code.Source("""
|
b.join("conftest.py").write(py.code.Source("""
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_pycollect_makeitem():
|
def pytest_pycollect_makeitem():
|
||||||
outcome = yield
|
outcome = yield
|
||||||
if outcome.excinfo is None:
|
if outcome.excinfo is None:
|
||||||
|
|
|
@ -313,7 +313,7 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch):
|
||||||
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
|
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
|
||||||
config = testdir.parseconfig("-p", "no:mytestplugin")
|
config = testdir.parseconfig("-p", "no:mytestplugin")
|
||||||
plugin = config.pluginmanager.getplugin("mytestplugin")
|
plugin = config.pluginmanager.getplugin("mytestplugin")
|
||||||
assert plugin == -1
|
assert plugin is None
|
||||||
|
|
||||||
def test_cmdline_processargs_simple(testdir):
|
def test_cmdline_processargs_simple(testdir):
|
||||||
testdir.makeconftest("""
|
testdir.makeconftest("""
|
||||||
|
@ -348,14 +348,15 @@ def test_notify_exception(testdir, capfd):
|
||||||
|
|
||||||
|
|
||||||
def test_load_initial_conftest_last_ordering(testdir):
|
def test_load_initial_conftest_last_ordering(testdir):
|
||||||
from _pytest.config import get_plugin_manager
|
from _pytest.config import get_config
|
||||||
pm = get_plugin_manager()
|
pm = get_config().pluginmanager
|
||||||
class My:
|
class My:
|
||||||
def pytest_load_initial_conftests(self):
|
def pytest_load_initial_conftests(self):
|
||||||
pass
|
pass
|
||||||
m = My()
|
m = My()
|
||||||
pm.register(m)
|
pm.register(m)
|
||||||
l = pm.listattr("pytest_load_initial_conftests")
|
hc = pm.hook.pytest_load_initial_conftests
|
||||||
|
l = hc._nonwrappers + hc._wrappers
|
||||||
assert l[-1].__module__ == "_pytest.capture"
|
assert l[-1].__module__ == "_pytest.capture"
|
||||||
assert l[-2] == m.pytest_load_initial_conftests
|
assert l[-2] == m.pytest_load_initial_conftests
|
||||||
assert l[-3].__module__ == "_pytest.config"
|
assert l[-3].__module__ == "_pytest.config"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import pytest, py, os
|
import pytest, py, os
|
||||||
from _pytest.core import * # noqa
|
from _pytest.core import * # noqa
|
||||||
from _pytest.config import get_plugin_manager
|
from _pytest.config import get_config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -17,65 +17,410 @@ class TestPluginManager:
|
||||||
pm.register(42, name="abc")
|
pm.register(42, name="abc")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
pm.register(42, name="abc")
|
pm.register(42, name="abc")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
pm.register(42, name="def")
|
||||||
|
|
||||||
def test_pm(self, pm):
|
def test_pm(self, pm):
|
||||||
class A: pass
|
class A: pass
|
||||||
a1, a2 = A(), A()
|
a1, a2 = A(), A()
|
||||||
pm.register(a1)
|
pm.register(a1)
|
||||||
assert pm.isregistered(a1)
|
assert pm.is_registered(a1)
|
||||||
pm.register(a2, "hello")
|
pm.register(a2, "hello")
|
||||||
assert pm.isregistered(a2)
|
assert pm.is_registered(a2)
|
||||||
l = pm.getplugins()
|
l = pm.get_plugins()
|
||||||
assert a1 in l
|
assert a1 in l
|
||||||
assert a2 in l
|
assert a2 in l
|
||||||
assert pm.getplugin('hello') == a2
|
assert pm.get_plugin('hello') == a2
|
||||||
pm.unregister(a1)
|
assert pm.unregister(a1) == a1
|
||||||
assert not pm.isregistered(a1)
|
assert not pm.is_registered(a1)
|
||||||
|
|
||||||
def test_register_mismatch_method(self):
|
def test_pm_name(self, pm):
|
||||||
pm = get_plugin_manager()
|
class A: pass
|
||||||
|
a1 = A()
|
||||||
|
name = pm.register(a1, name="hello")
|
||||||
|
assert name == "hello"
|
||||||
|
pm.unregister(a1)
|
||||||
|
assert pm.get_plugin(a1) is None
|
||||||
|
assert not pm.is_registered(a1)
|
||||||
|
assert not pm.get_plugins()
|
||||||
|
name2 = pm.register(a1, name="hello")
|
||||||
|
assert name2 == name
|
||||||
|
pm.unregister(name="hello")
|
||||||
|
assert pm.get_plugin(a1) is None
|
||||||
|
assert not pm.is_registered(a1)
|
||||||
|
assert not pm.get_plugins()
|
||||||
|
|
||||||
|
def test_set_blocked(self, pm):
|
||||||
|
class A: pass
|
||||||
|
a1 = A()
|
||||||
|
name = pm.register(a1)
|
||||||
|
assert pm.is_registered(a1)
|
||||||
|
pm.set_blocked(name)
|
||||||
|
assert not pm.is_registered(a1)
|
||||||
|
|
||||||
|
pm.set_blocked("somename")
|
||||||
|
assert not pm.register(A(), "somename")
|
||||||
|
pm.unregister(name="somename")
|
||||||
|
|
||||||
|
def test_register_mismatch_method(self, pytestpm):
|
||||||
class hello:
|
class hello:
|
||||||
def pytest_gurgel(self):
|
def pytest_gurgel(self):
|
||||||
pass
|
pass
|
||||||
pytest.raises(Exception, lambda: pm.register(hello()))
|
pytestpm.register(hello())
|
||||||
|
with pytest.raises(PluginValidationError):
|
||||||
|
pytestpm.check_pending()
|
||||||
|
|
||||||
def test_register_mismatch_arg(self):
|
def test_register_mismatch_arg(self):
|
||||||
pm = get_plugin_manager()
|
pm = get_config().pluginmanager
|
||||||
class hello:
|
class hello:
|
||||||
def pytest_configure(self, asd):
|
def pytest_configure(self, asd):
|
||||||
pass
|
pass
|
||||||
pytest.raises(Exception, lambda: pm.register(hello()))
|
pytest.raises(Exception, lambda: pm.register(hello()))
|
||||||
|
|
||||||
def test_register(self):
|
def test_register(self):
|
||||||
pm = get_plugin_manager()
|
pm = get_config().pluginmanager
|
||||||
class MyPlugin:
|
class MyPlugin:
|
||||||
pass
|
pass
|
||||||
my = MyPlugin()
|
my = MyPlugin()
|
||||||
pm.register(my)
|
pm.register(my)
|
||||||
assert pm.getplugins()
|
assert pm.get_plugins()
|
||||||
my2 = MyPlugin()
|
my2 = MyPlugin()
|
||||||
pm.register(my2)
|
pm.register(my2)
|
||||||
assert pm.getplugins()[-2:] == [my, my2]
|
assert set([my,my2]).issubset(pm.get_plugins())
|
||||||
|
|
||||||
assert pm.isregistered(my)
|
assert pm.is_registered(my)
|
||||||
assert pm.isregistered(my2)
|
assert pm.is_registered(my2)
|
||||||
pm.unregister(my)
|
pm.unregister(my)
|
||||||
assert not pm.isregistered(my)
|
assert not pm.is_registered(my)
|
||||||
assert pm.getplugins()[-1:] == [my2]
|
assert my not in pm.get_plugins()
|
||||||
|
|
||||||
def test_listattr(self):
|
def test_register_unknown_hooks(self, pm):
|
||||||
plugins = PluginManager("xyz")
|
class Plugin1:
|
||||||
class api1:
|
def he_method1(self, arg):
|
||||||
x = 41
|
return arg + 1
|
||||||
class api2:
|
|
||||||
x = 42
|
pm.register(Plugin1())
|
||||||
class api3:
|
class Hooks:
|
||||||
x = 43
|
def he_method1(self, arg):
|
||||||
plugins.register(api1())
|
pass
|
||||||
plugins.register(api2())
|
pm.addhooks(Hooks)
|
||||||
plugins.register(api3())
|
#assert not pm._unverified_hooks
|
||||||
l = list(plugins.listattr('x'))
|
assert pm.hook.he_method1(arg=1) == [2]
|
||||||
assert l == [41, 42, 43]
|
|
||||||
|
def test_register_historic(self, pm):
|
||||||
|
class Hooks:
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
|
def he_method1(self, arg):
|
||||||
|
pass
|
||||||
|
pm.addhooks(Hooks)
|
||||||
|
|
||||||
|
pm.hook.he_method1.call_historic(kwargs=dict(arg=1))
|
||||||
|
l = []
|
||||||
|
class Plugin:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
l.append(arg)
|
||||||
|
|
||||||
|
pm.register(Plugin())
|
||||||
|
assert l == [1]
|
||||||
|
|
||||||
|
class Plugin2:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
l.append(arg*10)
|
||||||
|
pm.register(Plugin2())
|
||||||
|
assert l == [1, 10]
|
||||||
|
pm.hook.he_method1.call_historic(kwargs=dict(arg=12))
|
||||||
|
assert l == [1, 10, 120, 12]
|
||||||
|
|
||||||
|
def test_with_result_memorized(self, pm):
|
||||||
|
class Hooks:
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
|
def he_method1(self, arg):
|
||||||
|
pass
|
||||||
|
pm.addhooks(Hooks)
|
||||||
|
|
||||||
|
he_method1 = pm.hook.he_method1
|
||||||
|
he_method1.call_historic(lambda res: l.append(res), dict(arg=1))
|
||||||
|
l = []
|
||||||
|
class Plugin:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
return arg * 10
|
||||||
|
|
||||||
|
pm.register(Plugin())
|
||||||
|
assert l == [10]
|
||||||
|
|
||||||
|
def test_register_historic_incompat_hookwrapper(self, pm):
|
||||||
|
class Hooks:
|
||||||
|
@hookspec_opts(historic=True)
|
||||||
|
def he_method1(self, arg):
|
||||||
|
pass
|
||||||
|
pm.addhooks(Hooks)
|
||||||
|
|
||||||
|
l = []
|
||||||
|
class Plugin:
|
||||||
|
@hookimpl_opts(hookwrapper=True)
|
||||||
|
def he_method1(self, arg):
|
||||||
|
l.append(arg)
|
||||||
|
|
||||||
|
with pytest.raises(PluginValidationError):
|
||||||
|
pm.register(Plugin())
|
||||||
|
|
||||||
|
def test_call_extra(self, pm):
|
||||||
|
class Hooks:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
pass
|
||||||
|
pm.addhooks(Hooks)
|
||||||
|
|
||||||
|
def he_method1(arg):
|
||||||
|
return arg * 10
|
||||||
|
|
||||||
|
l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1))
|
||||||
|
assert l == [10]
|
||||||
|
|
||||||
|
def test_subset_hook_caller(self, pm):
|
||||||
|
class Hooks:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
pass
|
||||||
|
pm.addhooks(Hooks)
|
||||||
|
|
||||||
|
l = []
|
||||||
|
class Plugin1:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
l.append(arg)
|
||||||
|
class Plugin2:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
l.append(arg*10)
|
||||||
|
class PluginNo:
|
||||||
|
pass
|
||||||
|
|
||||||
|
plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
|
||||||
|
pm.register(plugin1)
|
||||||
|
pm.register(plugin2)
|
||||||
|
pm.register(plugin3)
|
||||||
|
pm.hook.he_method1(arg=1)
|
||||||
|
assert l == [10, 1]
|
||||||
|
l[:] = []
|
||||||
|
|
||||||
|
hc = pm.subset_hook_caller("he_method1", [plugin1])
|
||||||
|
hc(arg=2)
|
||||||
|
assert l == [20]
|
||||||
|
l[:] = []
|
||||||
|
|
||||||
|
hc = pm.subset_hook_caller("he_method1", [plugin2])
|
||||||
|
hc(arg=2)
|
||||||
|
assert l == [2]
|
||||||
|
l[:] = []
|
||||||
|
|
||||||
|
pm.unregister(plugin1)
|
||||||
|
hc(arg=2)
|
||||||
|
assert l == []
|
||||||
|
l[:] = []
|
||||||
|
|
||||||
|
pm.hook.he_method1(arg=1)
|
||||||
|
assert l == [10]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddMethodOrdering:
|
||||||
|
@pytest.fixture
|
||||||
|
def hc(self, pm):
|
||||||
|
class Hooks:
|
||||||
|
def he_method1(self, arg):
|
||||||
|
pass
|
||||||
|
pm.addhooks(Hooks)
|
||||||
|
return pm.hook.he_method1
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def addmeth(self, hc):
|
||||||
|
def addmeth(tryfirst=False, trylast=False, hookwrapper=False):
|
||||||
|
def wrap(func):
|
||||||
|
if tryfirst:
|
||||||
|
func.tryfirst = True
|
||||||
|
if trylast:
|
||||||
|
func.trylast = True
|
||||||
|
if hookwrapper:
|
||||||
|
func.hookwrapper = True
|
||||||
|
hc._add_method(func)
|
||||||
|
return func
|
||||||
|
return wrap
|
||||||
|
return addmeth
|
||||||
|
|
||||||
|
def test_adding_nonwrappers(self, hc, addmeth):
|
||||||
|
@addmeth()
|
||||||
|
def he_method1():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method2():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method3():
|
||||||
|
pass
|
||||||
|
assert hc._nonwrappers == [he_method1, he_method2, he_method3]
|
||||||
|
|
||||||
|
def test_adding_nonwrappers_trylast(self, hc, addmeth):
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_middle():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth(trylast=True)
|
||||||
|
def he_method1():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_b():
|
||||||
|
pass
|
||||||
|
assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b]
|
||||||
|
|
||||||
|
def test_adding_nonwrappers_trylast3(self, hc, addmeth):
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_a():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth(trylast=True)
|
||||||
|
def he_method1_b():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_c():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth(trylast=True)
|
||||||
|
def he_method1_d():
|
||||||
|
pass
|
||||||
|
assert hc._nonwrappers == [he_method1_d, he_method1_b,
|
||||||
|
he_method1_a, he_method1_c]
|
||||||
|
|
||||||
|
|
||||||
|
def test_adding_nonwrappers_trylast2(self, hc, addmeth):
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_middle():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_b():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth(trylast=True)
|
||||||
|
def he_method1():
|
||||||
|
pass
|
||||||
|
assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b]
|
||||||
|
|
||||||
|
def test_adding_nonwrappers_tryfirst(self, hc, addmeth):
|
||||||
|
@addmeth(tryfirst=True)
|
||||||
|
def he_method1():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_middle():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_b():
|
||||||
|
pass
|
||||||
|
assert hc._nonwrappers == [he_method1_middle, he_method1_b, he_method1]
|
||||||
|
|
||||||
|
def test_adding_wrappers_ordering(self, hc, addmeth):
|
||||||
|
@addmeth(hookwrapper=True)
|
||||||
|
def he_method1():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth()
|
||||||
|
def he_method1_middle():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth(hookwrapper=True)
|
||||||
|
def he_method3():
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert hc._nonwrappers == [he_method1_middle]
|
||||||
|
assert hc._wrappers == [he_method1, he_method3]
|
||||||
|
|
||||||
|
def test_adding_wrappers_ordering_tryfirst(self, hc, addmeth):
|
||||||
|
@addmeth(hookwrapper=True, tryfirst=True)
|
||||||
|
def he_method1():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@addmeth(hookwrapper=True)
|
||||||
|
def he_method2():
|
||||||
|
pass
|
||||||
|
|
||||||
|
assert hc._nonwrappers == []
|
||||||
|
assert hc._wrappers == [he_method2, he_method1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_hookspec_opts(self, pm):
|
||||||
|
class HookSpec:
|
||||||
|
@hookspec_opts()
|
||||||
|
def he_myhook1(self, arg1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
|
def he_myhook2(self, arg1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@hookspec_opts(firstresult=False)
|
||||||
|
def he_myhook3(self, arg1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
pm.addhooks(HookSpec)
|
||||||
|
assert not pm.hook.he_myhook1.firstresult
|
||||||
|
assert pm.hook.he_myhook2.firstresult
|
||||||
|
assert not pm.hook.he_myhook3.firstresult
|
||||||
|
|
||||||
|
|
||||||
|
def test_hookimpl_opts(self):
|
||||||
|
for name in ["hookwrapper", "optionalhook", "tryfirst", "trylast"]:
|
||||||
|
for val in [True, False]:
|
||||||
|
@hookimpl_opts(**{name: val})
|
||||||
|
def he_myhook1(self, arg1):
|
||||||
|
pass
|
||||||
|
if val:
|
||||||
|
assert getattr(he_myhook1, name)
|
||||||
|
else:
|
||||||
|
assert not hasattr(he_myhook1, name)
|
||||||
|
|
||||||
|
def test_decorator_functional(self, pm):
|
||||||
|
class HookSpec:
|
||||||
|
@hookspec_opts(firstresult=True)
|
||||||
|
def he_myhook(self, arg1):
|
||||||
|
""" add to arg1 """
|
||||||
|
pm.addhooks(HookSpec)
|
||||||
|
|
||||||
|
class Plugin:
|
||||||
|
@hookimpl_opts()
|
||||||
|
def he_myhook(self, arg1):
|
||||||
|
return arg1 + 1
|
||||||
|
|
||||||
|
pm.register(Plugin())
|
||||||
|
results = pm.hook.he_myhook(arg1=17)
|
||||||
|
assert results == 18
|
||||||
|
|
||||||
|
def test_load_setuptools_instantiation(self, monkeypatch, pm):
|
||||||
|
pkg_resources = pytest.importorskip("pkg_resources")
|
||||||
|
def my_iter(name):
|
||||||
|
assert name == "hello"
|
||||||
|
class EntryPoint:
|
||||||
|
name = "myname"
|
||||||
|
dist = None
|
||||||
|
def load(self):
|
||||||
|
class PseudoPlugin:
|
||||||
|
x = 42
|
||||||
|
return PseudoPlugin()
|
||||||
|
return iter([EntryPoint()])
|
||||||
|
|
||||||
|
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
|
||||||
|
num = pm.load_setuptools_entrypoints("hello")
|
||||||
|
assert num == 1
|
||||||
|
plugin = pm.get_plugin("myname")
|
||||||
|
assert plugin.x == 42
|
||||||
|
assert pm._plugin_distinfo == [(None, plugin)]
|
||||||
|
|
||||||
|
def test_load_setuptools_not_installed(self, monkeypatch, pm):
|
||||||
|
monkeypatch.setitem(py.std.sys.modules, 'pkg_resources',
|
||||||
|
py.std.types.ModuleType("pkg_resources"))
|
||||||
|
with pytest.raises(ImportError):
|
||||||
|
pm.load_setuptools_entrypoints("qwe")
|
||||||
|
|
||||||
|
|
||||||
class TestPytestPluginInteractions:
|
class TestPytestPluginInteractions:
|
||||||
|
@ -93,9 +438,12 @@ class TestPytestPluginInteractions:
|
||||||
def pytest_myhook(xyz):
|
def pytest_myhook(xyz):
|
||||||
return xyz + 1
|
return xyz + 1
|
||||||
""")
|
""")
|
||||||
config = get_plugin_manager().config
|
config = get_config()
|
||||||
|
pm = config.pluginmanager
|
||||||
|
pm.hook.pytest_addhooks.call_historic(
|
||||||
|
kwargs=dict(pluginmanager=config.pluginmanager))
|
||||||
config.pluginmanager._importconftest(conf)
|
config.pluginmanager._importconftest(conf)
|
||||||
print(config.pluginmanager.getplugins())
|
#print(config.pluginmanager.get_plugins())
|
||||||
res = config.hook.pytest_myhook(xyz=10)
|
res = config.hook.pytest_myhook(xyz=10)
|
||||||
assert res == [11]
|
assert res == [11]
|
||||||
|
|
||||||
|
@ -166,64 +514,44 @@ class TestPytestPluginInteractions:
|
||||||
assert len(l) == 2
|
assert len(l) == 2
|
||||||
|
|
||||||
def test_hook_tracing(self):
|
def test_hook_tracing(self):
|
||||||
pytestpm = get_plugin_manager() # fully initialized with plugins
|
pytestpm = get_config().pluginmanager # fully initialized with plugins
|
||||||
saveindent = []
|
saveindent = []
|
||||||
class api1:
|
class api1:
|
||||||
x = 41
|
def pytest_plugin_registered(self):
|
||||||
def pytest_plugin_registered(self, plugin):
|
|
||||||
saveindent.append(pytestpm.trace.root.indent)
|
saveindent.append(pytestpm.trace.root.indent)
|
||||||
raise ValueError(42)
|
class api2:
|
||||||
|
def pytest_plugin_registered(self):
|
||||||
|
saveindent.append(pytestpm.trace.root.indent)
|
||||||
|
raise ValueError()
|
||||||
l = []
|
l = []
|
||||||
pytestpm.set_tracing(l.append)
|
pytestpm.trace.root.setwriter(l.append)
|
||||||
indent = pytestpm.trace.root.indent
|
undo = pytestpm.enable_tracing()
|
||||||
p = api1()
|
try:
|
||||||
pytestpm.register(p)
|
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]
|
||||||
|
|
||||||
assert pytestpm.trace.root.indent == indent
|
l[:] = []
|
||||||
assert len(l) == 2
|
with pytest.raises(ValueError):
|
||||||
assert 'pytest_plugin_registered' in l[0]
|
pytestpm.register(api2())
|
||||||
assert 'finish' in l[1]
|
assert pytestpm.trace.root.indent == indent
|
||||||
with pytest.raises(ValueError):
|
assert saveindent[0] > indent
|
||||||
pytestpm.register(api1())
|
finally:
|
||||||
assert pytestpm.trace.root.indent == indent
|
undo()
|
||||||
assert saveindent[0] > indent
|
|
||||||
|
|
||||||
# lower level API
|
def test_warn_on_deprecated_multicall(self, pytestpm):
|
||||||
|
class Plugin:
|
||||||
|
def pytest_configure(self, __multicall__):
|
||||||
|
pass
|
||||||
|
|
||||||
def test_listattr(self):
|
before = list(pytestpm._warnings)
|
||||||
pluginmanager = PluginManager("xyz")
|
pytestpm.register(Plugin())
|
||||||
class My2:
|
assert len(pytestpm._warnings) == len(before) + 1
|
||||||
x = 42
|
assert "deprecated" in pytestpm._warnings[-1]["message"]
|
||||||
pluginmanager.register(My2())
|
|
||||||
assert not pluginmanager.listattr("hello")
|
|
||||||
assert pluginmanager.listattr("x") == [42]
|
|
||||||
|
|
||||||
def test_listattr_tryfirst(self):
|
|
||||||
class P1:
|
|
||||||
@pytest.mark.tryfirst
|
|
||||||
def m(self):
|
|
||||||
return 17
|
|
||||||
|
|
||||||
class P2:
|
|
||||||
def m(self):
|
|
||||||
return 23
|
|
||||||
class P3:
|
|
||||||
def m(self):
|
|
||||||
return 19
|
|
||||||
|
|
||||||
pluginmanager = PluginManager("xyz")
|
|
||||||
p1 = P1()
|
|
||||||
p2 = P2()
|
|
||||||
p3 = P3()
|
|
||||||
pluginmanager.register(p1)
|
|
||||||
pluginmanager.register(p2)
|
|
||||||
pluginmanager.register(p3)
|
|
||||||
methods = pluginmanager.listattr('m')
|
|
||||||
assert methods == [p2.m, p3.m, p1.m]
|
|
||||||
del P1.m.__dict__['tryfirst']
|
|
||||||
pytest.mark.trylast(getattr(P2.m, 'im_func', P2.m))
|
|
||||||
methods = pluginmanager.listattr('m')
|
|
||||||
assert methods == [p2.m, p1.m, p3.m]
|
|
||||||
|
|
||||||
|
|
||||||
def test_namespace_has_default_and_env_plugins(testdir):
|
def test_namespace_has_default_and_env_plugins(testdir):
|
||||||
|
@ -373,35 +701,6 @@ class TestMultiCall:
|
||||||
assert res == []
|
assert res == []
|
||||||
assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"]
|
assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"]
|
||||||
|
|
||||||
def test_listattr_hookwrapper_ordering(self):
|
|
||||||
class P1:
|
|
||||||
@pytest.mark.hookwrapper
|
|
||||||
def m(self):
|
|
||||||
return 17
|
|
||||||
|
|
||||||
class P2:
|
|
||||||
def m(self):
|
|
||||||
return 23
|
|
||||||
|
|
||||||
class P3:
|
|
||||||
@pytest.mark.tryfirst
|
|
||||||
def m(self):
|
|
||||||
return 19
|
|
||||||
|
|
||||||
pluginmanager = PluginManager("xyz")
|
|
||||||
p1 = P1()
|
|
||||||
p2 = P2()
|
|
||||||
p3 = P3()
|
|
||||||
pluginmanager.register(p1)
|
|
||||||
pluginmanager.register(p2)
|
|
||||||
pluginmanager.register(p3)
|
|
||||||
methods = pluginmanager.listattr('m')
|
|
||||||
assert methods == [p2.m, p3.m, p1.m]
|
|
||||||
## listattr keeps a cache and deleting
|
|
||||||
## a function attribute requires clearing it
|
|
||||||
#pluginmanager._listattrcache.clear()
|
|
||||||
#del P1.m.__dict__['tryfirst']
|
|
||||||
|
|
||||||
def test_hookwrapper_not_yield(self):
|
def test_hookwrapper_not_yield(self):
|
||||||
def m1():
|
def m1():
|
||||||
pass
|
pass
|
||||||
|
@ -598,109 +897,6 @@ def test_importplugin_issue375(testdir, pytestpm):
|
||||||
assert "qwe" not in str(excinfo.value)
|
assert "qwe" not in str(excinfo.value)
|
||||||
assert "aaaa" in str(excinfo.value)
|
assert "aaaa" in str(excinfo.value)
|
||||||
|
|
||||||
class TestWrapMethod:
|
|
||||||
def test_basic_hapmypath(self):
|
|
||||||
class A:
|
|
||||||
def f(self):
|
|
||||||
return "A.f"
|
|
||||||
|
|
||||||
l = []
|
|
||||||
def f(self):
|
|
||||||
l.append(1)
|
|
||||||
box = yield
|
|
||||||
assert box.result == "A.f"
|
|
||||||
l.append(2)
|
|
||||||
undo = add_method_wrapper(A, f)
|
|
||||||
|
|
||||||
assert A().f() == "A.f"
|
|
||||||
assert l == [1,2]
|
|
||||||
undo()
|
|
||||||
l[:] = []
|
|
||||||
assert A().f() == "A.f"
|
|
||||||
assert l == []
|
|
||||||
|
|
||||||
def test_no_yield(self):
|
|
||||||
class A:
|
|
||||||
def method(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
def method(self):
|
|
||||||
if 0:
|
|
||||||
yield
|
|
||||||
|
|
||||||
add_method_wrapper(A, method)
|
|
||||||
with pytest.raises(RuntimeError) as excinfo:
|
|
||||||
A().method()
|
|
||||||
|
|
||||||
assert "method" in str(excinfo.value)
|
|
||||||
assert "did not yield" in str(excinfo.value)
|
|
||||||
|
|
||||||
def test_method_raises(self):
|
|
||||||
class A:
|
|
||||||
def error(self, val):
|
|
||||||
raise ValueError(val)
|
|
||||||
|
|
||||||
l = []
|
|
||||||
def error(self, val):
|
|
||||||
l.append(val)
|
|
||||||
yield
|
|
||||||
l.append(None)
|
|
||||||
|
|
||||||
undo = add_method_wrapper(A, error)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
A().error(42)
|
|
||||||
assert l == [42, None]
|
|
||||||
undo()
|
|
||||||
l[:] = []
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
A().error(42)
|
|
||||||
assert l == []
|
|
||||||
|
|
||||||
def test_controller_swallows_method_raises(self):
|
|
||||||
class A:
|
|
||||||
def error(self, val):
|
|
||||||
raise ValueError(val)
|
|
||||||
|
|
||||||
def error(self, val):
|
|
||||||
box = yield
|
|
||||||
box.force_result(2)
|
|
||||||
|
|
||||||
add_method_wrapper(A, error)
|
|
||||||
assert A().error(42) == 2
|
|
||||||
|
|
||||||
def test_reraise_on_controller_StopIteration(self):
|
|
||||||
class A:
|
|
||||||
def error(self, val):
|
|
||||||
raise ValueError(val)
|
|
||||||
|
|
||||||
def error(self, val):
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
add_method_wrapper(A, error)
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
A().error(42)
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="if needed later")
|
|
||||||
def test_modify_call_args(self):
|
|
||||||
class A:
|
|
||||||
def error(self, val1, val2):
|
|
||||||
raise ValueError(val1+val2)
|
|
||||||
|
|
||||||
l = []
|
|
||||||
def error(self):
|
|
||||||
box = yield (1,), {'val2': 2}
|
|
||||||
assert box.excinfo[1].args == (3,)
|
|
||||||
l.append(1)
|
|
||||||
|
|
||||||
add_method_wrapper(A, error)
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
A().error()
|
|
||||||
assert l == [1]
|
|
||||||
|
|
||||||
|
|
||||||
### to be shifted to own test file
|
### to be shifted to own test file
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
|
@ -710,21 +906,21 @@ class TestPytestPluginManager:
|
||||||
pm = PytestPluginManager()
|
pm = PytestPluginManager()
|
||||||
mod = py.std.types.ModuleType("x.y.pytest_hello")
|
mod = py.std.types.ModuleType("x.y.pytest_hello")
|
||||||
pm.register(mod)
|
pm.register(mod)
|
||||||
assert pm.isregistered(mod)
|
assert pm.is_registered(mod)
|
||||||
l = pm.getplugins()
|
l = pm.get_plugins()
|
||||||
assert mod in l
|
assert mod in l
|
||||||
pytest.raises(ValueError, "pm.register(mod)")
|
pytest.raises(ValueError, "pm.register(mod)")
|
||||||
pytest.raises(ValueError, lambda: pm.register(mod))
|
pytest.raises(ValueError, lambda: pm.register(mod))
|
||||||
#assert not pm.isregistered(mod2)
|
#assert not pm.is_registered(mod2)
|
||||||
assert pm.getplugins() == l
|
assert pm.get_plugins() == l
|
||||||
|
|
||||||
def test_canonical_import(self, monkeypatch):
|
def test_canonical_import(self, monkeypatch):
|
||||||
mod = py.std.types.ModuleType("pytest_xyz")
|
mod = py.std.types.ModuleType("pytest_xyz")
|
||||||
monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod)
|
monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod)
|
||||||
pm = PytestPluginManager()
|
pm = PytestPluginManager()
|
||||||
pm.import_plugin('pytest_xyz')
|
pm.import_plugin('pytest_xyz')
|
||||||
assert pm.getplugin('pytest_xyz') == mod
|
assert pm.get_plugin('pytest_xyz') == mod
|
||||||
assert pm.isregistered(mod)
|
assert pm.is_registered(mod)
|
||||||
|
|
||||||
def test_consider_module(self, testdir, pytestpm):
|
def test_consider_module(self, testdir, pytestpm):
|
||||||
testdir.syspathinsert()
|
testdir.syspathinsert()
|
||||||
|
@ -733,11 +929,11 @@ class TestPytestPluginManager:
|
||||||
mod = py.std.types.ModuleType("temp")
|
mod = py.std.types.ModuleType("temp")
|
||||||
mod.pytest_plugins = ["pytest_p1", "pytest_p2"]
|
mod.pytest_plugins = ["pytest_p1", "pytest_p2"]
|
||||||
pytestpm.consider_module(mod)
|
pytestpm.consider_module(mod)
|
||||||
assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1"
|
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
|
||||||
assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2"
|
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
|
||||||
|
|
||||||
def test_consider_module_import_module(self, testdir):
|
def test_consider_module_import_module(self, testdir):
|
||||||
pytestpm = get_plugin_manager()
|
pytestpm = get_config().pluginmanager
|
||||||
mod = py.std.types.ModuleType("x")
|
mod = py.std.types.ModuleType("x")
|
||||||
mod.pytest_plugins = "pytest_a"
|
mod.pytest_plugins = "pytest_a"
|
||||||
aplugin = testdir.makepyfile(pytest_a="#")
|
aplugin = testdir.makepyfile(pytest_a="#")
|
||||||
|
@ -776,51 +972,27 @@ class TestPytestPluginManager:
|
||||||
testdir.syspathinsert()
|
testdir.syspathinsert()
|
||||||
testdir.makepyfile(xy123="#")
|
testdir.makepyfile(xy123="#")
|
||||||
monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123')
|
monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123')
|
||||||
l1 = len(pytestpm.getplugins())
|
l1 = len(pytestpm.get_plugins())
|
||||||
pytestpm.consider_env()
|
pytestpm.consider_env()
|
||||||
l2 = len(pytestpm.getplugins())
|
l2 = len(pytestpm.get_plugins())
|
||||||
assert l2 == l1 + 1
|
assert l2 == l1 + 1
|
||||||
assert pytestpm.getplugin('xy123')
|
assert pytestpm.get_plugin('xy123')
|
||||||
pytestpm.consider_env()
|
pytestpm.consider_env()
|
||||||
l3 = len(pytestpm.getplugins())
|
l3 = len(pytestpm.get_plugins())
|
||||||
assert l2 == l3
|
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):
|
def test_pluginmanager_ENV_startup(self, testdir, monkeypatch):
|
||||||
testdir.makepyfile(pytest_x500="#")
|
testdir.makepyfile(pytest_x500="#")
|
||||||
p = testdir.makepyfile("""
|
p = testdir.makepyfile("""
|
||||||
import pytest
|
import pytest
|
||||||
def test_hello(pytestconfig):
|
def test_hello(pytestconfig):
|
||||||
plugin = pytestconfig.pluginmanager.getplugin('pytest_x500')
|
plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
|
||||||
assert plugin is not None
|
assert plugin is not None
|
||||||
""")
|
""")
|
||||||
monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",")
|
monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",")
|
||||||
result = testdir.runpytest(p)
|
result = testdir.runpytest(p)
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(["*1 passed in*"])
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
|
||||||
def test_import_plugin_importname(self, testdir, pytestpm):
|
def test_import_plugin_importname(self, testdir, pytestpm):
|
||||||
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
|
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
|
||||||
|
@ -830,13 +1002,13 @@ class TestPytestPluginManager:
|
||||||
pluginname = "pytest_hello"
|
pluginname = "pytest_hello"
|
||||||
testdir.makepyfile(**{pluginname: ""})
|
testdir.makepyfile(**{pluginname: ""})
|
||||||
pytestpm.import_plugin("pytest_hello")
|
pytestpm.import_plugin("pytest_hello")
|
||||||
len1 = len(pytestpm.getplugins())
|
len1 = len(pytestpm.get_plugins())
|
||||||
pytestpm.import_plugin("pytest_hello")
|
pytestpm.import_plugin("pytest_hello")
|
||||||
len2 = len(pytestpm.getplugins())
|
len2 = len(pytestpm.get_plugins())
|
||||||
assert len1 == len2
|
assert len1 == len2
|
||||||
plugin1 = pytestpm.getplugin("pytest_hello")
|
plugin1 = pytestpm.get_plugin("pytest_hello")
|
||||||
assert plugin1.__name__.endswith('pytest_hello')
|
assert plugin1.__name__.endswith('pytest_hello')
|
||||||
plugin2 = pytestpm.getplugin("pytest_hello")
|
plugin2 = pytestpm.get_plugin("pytest_hello")
|
||||||
assert plugin2 is plugin1
|
assert plugin2 is plugin1
|
||||||
|
|
||||||
def test_import_plugin_dotted_name(self, testdir, pytestpm):
|
def test_import_plugin_dotted_name(self, testdir, pytestpm):
|
||||||
|
@ -847,7 +1019,7 @@ class TestPytestPluginManager:
|
||||||
testdir.mkpydir("pkg").join("plug.py").write("x=3")
|
testdir.mkpydir("pkg").join("plug.py").write("x=3")
|
||||||
pluginname = "pkg.plug"
|
pluginname = "pkg.plug"
|
||||||
pytestpm.import_plugin(pluginname)
|
pytestpm.import_plugin(pluginname)
|
||||||
mod = pytestpm.getplugin("pkg.plug")
|
mod = pytestpm.get_plugin("pkg.plug")
|
||||||
assert mod.x == 3
|
assert mod.x == 3
|
||||||
|
|
||||||
def test_consider_conftest_deps(self, testdir, pytestpm):
|
def test_consider_conftest_deps(self, testdir, pytestpm):
|
||||||
|
@ -863,15 +1035,16 @@ class TestPytestPluginManagerBootstrapming:
|
||||||
|
|
||||||
def test_plugin_prevent_register(self, pytestpm):
|
def test_plugin_prevent_register(self, pytestpm):
|
||||||
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
|
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
|
||||||
l1 = pytestpm.getplugins()
|
l1 = pytestpm.get_plugins()
|
||||||
pytestpm.register(42, name="abc")
|
pytestpm.register(42, name="abc")
|
||||||
l2 = pytestpm.getplugins()
|
l2 = pytestpm.get_plugins()
|
||||||
assert len(l2) == len(l1)
|
assert len(l2) == len(l1)
|
||||||
|
assert 42 not in l2
|
||||||
|
|
||||||
def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm):
|
def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm):
|
||||||
pytestpm.register(42, name="abc")
|
pytestpm.register(42, name="abc")
|
||||||
l1 = pytestpm.getplugins()
|
l1 = pytestpm.get_plugins()
|
||||||
assert 42 in l1
|
assert 42 in l1
|
||||||
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
|
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
|
||||||
l2 = pytestpm.getplugins()
|
l2 = pytestpm.get_plugins()
|
||||||
assert 42 not in l2
|
assert 42 not in l2
|
||||||
|
|
|
@ -38,7 +38,7 @@ def test_hookvalidation_unknown(testdir):
|
||||||
def test_hookvalidation_optional(testdir):
|
def test_hookvalidation_optional(testdir):
|
||||||
testdir.makeconftest("""
|
testdir.makeconftest("""
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.mark.optionalhook
|
@pytest.hookimpl_opts(optionalhook=True)
|
||||||
def pytest_hello(xyz):
|
def pytest_hello(xyz):
|
||||||
pass
|
pass
|
||||||
""")
|
""")
|
||||||
|
|
|
@ -510,7 +510,7 @@ class TestKeywordSelection:
|
||||||
""")
|
""")
|
||||||
testdir.makepyfile(conftest="""
|
testdir.makepyfile(conftest="""
|
||||||
import pytest
|
import pytest
|
||||||
@pytest.mark.hookwrapper
|
@pytest.hookimpl_opts(hookwrapper=True)
|
||||||
def pytest_pycollect_makeitem(name):
|
def pytest_pycollect_makeitem(name):
|
||||||
outcome = yield
|
outcome = yield
|
||||||
if name == "TestClass":
|
if name == "TestClass":
|
||||||
|
|
|
@ -457,7 +457,7 @@ class TestTerminalFunctional:
|
||||||
])
|
])
|
||||||
assert result.ret == 1
|
assert result.ret == 1
|
||||||
|
|
||||||
if not pytestconfig.pluginmanager.hasplugin("xdist"):
|
if not pytestconfig.pluginmanager.get_plugin("xdist"):
|
||||||
pytest.skip("xdist plugin not installed")
|
pytest.skip("xdist plugin not installed")
|
||||||
|
|
||||||
result = testdir.runpytest(p1, '-v', '-n 1')
|
result = testdir.runpytest(p1, '-v', '-n 1')
|
||||||
|
|
Loading…
Reference in New Issue