Merged in hpk42/pytest-patches/more_plugin (pull request #282)

another major pluginmanager refactor and docs
This commit is contained in:
Floris Bruynooghe 2015-04-27 13:17:40 +01:00
commit 2d8f115d8c
27 changed files with 1507 additions and 1019 deletions

View File

@ -26,6 +26,23 @@
change but it might still break 3rd party plugins which relied on
details like especially the pluginmanager.add_shutdown() API.
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)
-----------------------------

View File

@ -29,7 +29,7 @@ def pytest_addoption(parser):
help="shortcut for --capture=no.")
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_load_initial_conftests(early_config, parser, args):
ns = early_config.known_args_namespace
pluginmanager = early_config.pluginmanager
@ -101,7 +101,7 @@ class CaptureManager:
if capfuncarg is not None:
capfuncarg.close()
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_make_collect_report(self, collector):
if isinstance(collector, pytest.File):
self.resumecapture()
@ -115,13 +115,13 @@ class CaptureManager:
else:
yield
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_setup(self, item):
self.resumecapture()
yield
self.suspendcapture_item(item, "setup")
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_call(self, item):
self.resumecapture()
self.activate_funcargs(item)
@ -129,17 +129,17 @@ class CaptureManager:
#self.deactivate_funcargs() called from suspendcapture()
self.suspendcapture_item(item, "call")
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_teardown(self, item):
self.resumecapture()
yield
self.suspendcapture_item(item, "teardown")
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo):
self.reset_capturings()
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_internalerror(self, excinfo):
self.reset_capturings()

View File

@ -9,7 +9,7 @@ import py
# DON't import pytest here because it causes import cycle troubles
import sys, os
from _pytest import hookspec # the extension point definitions
from _pytest.core import PluginManager
from _pytest.core import PluginManager, hookimpl_opts, varnames
# pytest startup
#
@ -38,6 +38,7 @@ def main(args=None, plugins=None):
tw.line("ERROR: could not load %s\n" % (e.path), red=True)
return 4
else:
config.pluginmanager.check_pending()
return config.hook.pytest_cmdline_main(config=config)
class cmdline: # compatibility namespace
@ -59,17 +60,17 @@ builtin_plugins.add("pytester")
def _preloadplugins():
assert not _preinit
_preinit.append(get_plugin_manager())
_preinit.append(get_config())
def get_plugin_manager():
def get_config():
if _preinit:
return _preinit.pop(0)
# subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager()
pluginmanager.config = Config(pluginmanager) # XXX attr needed?
config = Config(pluginmanager)
for spec in default_plugins:
pluginmanager.import_plugin(spec)
return pluginmanager
return config
def _prepareconfig(args=None, plugins=None):
if args is None:
@ -80,7 +81,7 @@ def _prepareconfig(args=None, plugins=None):
if not isinstance(args, str):
raise ValueError("not a string or argument list: %r" % (args,))
args = shlex.split(args)
pluginmanager = get_plugin_manager()
pluginmanager = get_config().pluginmanager
if plugins:
for plugin in plugins:
pluginmanager.register(plugin)
@ -97,8 +98,7 @@ class PytestPluginManager(PluginManager):
super(PytestPluginManager, self).__init__(prefix="pytest_",
excludefunc=exclude_pytest_names)
self._warnings = []
self._plugin_distinfo = []
self._globalplugins = []
self._conftest_plugins = set()
# state related to local conftest plugins
self._path2confmods = {}
@ -114,28 +114,35 @@ class PytestPluginManager(PluginManager):
err = py.io.dupfile(err, encoding=encoding)
except Exception:
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)
if ret and not conftest:
self._globalplugins.append(plugin)
if ret:
self.hook.pytest_plugin_registered.call_historic(
kwargs=dict(plugin=plugin, manager=self))
return ret
def _do_register(self, plugin, name):
# called from core PluginManager class
if hasattr(self, "config"):
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 getplugin(self, name):
# support deprecated naming because plugins (xdist e.g.) use it
return self.get_plugin(name)
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",
"tryfirst: mark a hook implementation function such that the "
"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 "
"plugin machinery will try to call it last/as late as possible.")
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
@ -186,14 +196,21 @@ class PytestPluginManager(PluginManager):
try:
return self._path2confmods[path]
except KeyError:
clist = []
for parent in path.parts():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
if conftestpath.check(file=1):
mod = self._importconftest(conftestpath)
clist.append(mod)
if path.isfile():
clist = self._getconftestmodules(path.dirpath())
else:
# XXX these days we may rather want to use config.rootdir
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir
clist = []
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
return clist
@ -217,6 +234,8 @@ class PytestPluginManager(PluginManager):
mod = conftestpath.pyimport()
except Exception:
raise ConftestImportFailure(conftestpath, sys.exc_info())
self._conftest_plugins.add(mod)
self._conftestpath2mod[conftestpath] = mod
dirpath = conftestpath.dirpath()
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):
for opt1,opt2 in zip(args, args[1:]):
if opt1 == "-p":
@ -258,18 +259,12 @@ class PytestPluginManager(PluginManager):
def consider_pluginarg(self, arg):
if arg.startswith("no:"):
name = arg[3:]
plugin = self.getplugin(name)
if plugin is not None:
self.unregister(plugin)
self._name2plugin[name] = -1
self.set_blocked(arg[3:])
else:
if self.getplugin(arg) is None:
self.import_plugin(arg)
self.import_plugin(arg)
def consider_conftest(self, conftestmodule):
if self.register(conftestmodule, name=conftestmodule.__file__,
conftest=True):
if self.register(conftestmodule, name=conftestmodule.__file__):
self.consider_module(conftestmodule)
def consider_env(self):
@ -291,7 +286,7 @@ class PytestPluginManager(PluginManager):
# basename for historic purposes but must be imported with the
# _pytest prefix.
assert isinstance(modname, str)
if self.getplugin(modname) is not None:
if self.get_plugin(modname) is not None:
return
if modname in builtin_plugins:
importspec = "_pytest." + modname
@ -685,6 +680,7 @@ class Notset:
notset = Notset()
FILE_OR_DIR = 'file_or_dir'
class Config(object):
""" access to configuration values, pluginmanager and plugin hooks. """
@ -706,20 +702,11 @@ class Config(object):
self._cleanup = []
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
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:
def do_setns(dic):
import pytest
setns(pytest, dic)
call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
if self._configured:
call_plugin(plugin, "pytest_configure", {'config': self})
self.hook.pytest_namespace.call_historic(do_setns, {})
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
def add_cleanup(self, func):
""" Add a function to be called when the config object gets out of
@ -729,26 +716,27 @@ class Config(object):
def _do_configure(self):
assert not self._configured
self._configured = True
self.hook.pytest_configure(config=self)
self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
def _ensure_unconfigure(self):
if self._configured:
self._configured = False
self.hook.pytest_unconfigure(config=self)
self.hook.pytest_configure._call_history = []
while self._cleanup:
fin = self._cleanup.pop()
fin()
def warn(self, code, message):
def warn(self, code, message, fslocation=None):
""" generate a warning for this test session. """
self.hook.pytest_logwarning(code=code, message=message,
fslocation=None, nodeid=None)
fslocation=fslocation, nodeid=None)
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):
assert self == pluginmanager.config, (self, pluginmanager.config)
# REF1 assert self == pluginmanager.config, (self, pluginmanager.config)
self.parse(args)
return self
@ -778,8 +766,7 @@ class Config(object):
@classmethod
def fromdictargs(cls, option_dict, args):
""" constructor useable for subprocesses. """
pluginmanager = get_plugin_manager()
config = pluginmanager.config
config = get_config()
config._preparse(args, addopts=False)
config.option.__dict__.update(option_dict)
for x in config.option.plugins:
@ -794,13 +781,9 @@ class Config(object):
if not hasattr(self.option, opt.dest):
setattr(self.option, opt.dest, opt.default)
def _getmatchingplugins(self, fspath):
return self.pluginmanager._globalplugins + \
self.pluginmanager._getconftestmodules(fspath)
@hookimpl_opts(trylast=True)
def pytest_load_initial_conftests(self, early_config):
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
pytest_load_initial_conftests.trylast = True
def _initini(self, args):
parsed_args = self._parser.parse_known_args(args)
@ -817,7 +800,10 @@ class Config(object):
args[:] = self.getini("addopts") + args
self._checkversion()
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.known_args_namespace = ns = self._parser.parse_known_args(args)
try:
@ -850,6 +836,8 @@ class Config(object):
assert not hasattr(self, 'args'), (
"can only parse cmdline args at most once per Config object")
self._origargs = args
self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager))
self._preparse(args)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)

View File

@ -2,11 +2,65 @@
PluginManager, basic initialization and tracing.
"""
import sys
import inspect
from inspect import isfunction, ismethod, isclass, formatargspec, getargspec
import py
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:
def __init__(self):
self._tag2proc = {}
@ -53,42 +107,28 @@ class TagTracer:
assert isinstance(tags, tuple)
self._tag2proc[tags] = processor
class TagTracerSub:
def __init__(self, root, tags):
self.root = root
self.tags = tags
def __call__(self, *args):
self.root.processmessage(self.tags, args)
def setmyprocessor(self, processor):
self.root.setprocessor(self.tags, processor)
def get(self, 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):
co = wrap_controller.gi_code
raise RuntimeError("wrap_controller at %r %s:%d %s" %
(co.co_name, co.co_filename, co.co_firstlineno, msg))
def wrapped_call(wrap_controller, func):
""" Wrap calling to a function with a generator which needs to yield
exactly once. The yield point will trigger calling the wrapped function
@ -133,6 +173,25 @@ class CallOutcome:
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):
""" Core Pluginmanager class which manages registration
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
are not considered as hooks despite a matching prefix.
For debugging purposes you can call ``set_tracing(writer)``
which will subsequently send debug information to the specified
write function.
For debugging purposes you can call ``enable_tracing()``
which will subsequently send debug information to the trace helper.
"""
def __init__(self, prefix, excludefunc=None):
self._prefix = prefix
self._excludefunc = excludefunc
self._name2plugin = {}
self._plugins = []
self._plugin2hookcallers = {}
self._plugin_distinfo = []
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):
""" turn on tracing to the given writer method and
return an undo function. """
self.trace.root.setwriter(writer)
# reconfigure HookCalling to perform tracing
assert not hasattr(self, "_wrapping")
self._wrapping = True
def _hookexec(self, hook, methods, kwargs):
# called from all hookcaller instances.
# enable_tracing will set its own wrapping function at self._inner_hookexec
return self._inner_hookexec(hook, methods, kwargs)
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(self.name, kwargs)
box = yield
if box.excinfo is None:
hooktrace("finish", self.name, "-->", box.result)
hooktrace(hook.name, kwargs)
def after(outcome, hook, methods, kwargs):
if outcome.excinfo is None:
hooktrace("finish", hook.name, "-->", outcome.result)
hooktrace.root.indent -= 1
return add_method_wrapper(HookCaller, _docall)
return TracedHookExecution(self, before, after).undo
def make_hook_caller(self, name, plugins):
caller = getattr(self.hook, name)
methods = self.listattr(name, plugins=plugins)
return HookCaller(caller.name, caller.firstresult,
argnames=caller.argnames, methods=methods)
def subset_hook_caller(self, name, remove_plugins):
""" Return a new HookCaller instance for the named method
which manages calls to all registered plugins except the
ones from remove_plugins. """
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):
""" Register a plugin with the given name and ensure that all its
hook implementations are integrated. If the name is not specified
we use the ``__name__`` attribute of the plugin object or, if that
doesn't exist, the id of the plugin. This method will raise a
ValueError if the eventual name is already registered. """
name = name or self._get_canonical_name(plugin)
if self._name2plugin.get(name, None) == -1:
return
if self.hasplugin(name):
""" Register a plugin and return its canonical name or None if the name
is blocked from registering. Raise a ValueError if the plugin is already
registered. """
plugin_name = name or self.get_canonical_name(plugin)
if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
if self._name2plugin.get(plugin_name, -1) is None:
return # blocked plugin, return None to indicate no registration
raise ValueError("Plugin already registered: %s=%s\n%s" %(
name, plugin, self._name2plugin))
#self.trace("registering", name, plugin)
# allow subclasses to intercept here by calling a helper
return self._do_register(plugin, name)
plugin_name, plugin, self._name2plugin))
def _do_register(self, plugin, name):
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
self._name2plugin[plugin_name] = plugin
def unregister(self, plugin):
""" unregister the plugin object and all its contained hook implementations
# register prefix-matching hook specs of the plugin
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. """
self._plugins.remove(plugin)
for name, value in list(self._name2plugin.items()):
if value == plugin:
del self._name2plugin[name]
hookcallers = self._plugin2hookcallers.pop(plugin)
for hookcaller in hookcallers:
self._scan_methods(hookcaller)
if name is None:
assert plugin is not None, "one of name or plugin needs to be specified"
name = self.get_name(plugin)
if plugin is None:
plugin = self.get_plugin(name)
# 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):
""" add new hook definitions from the given module_or_class using
the prefix/excludefunc with which the PluginManager was initialized. """
isclass = int(inspect.isclass(module_or_class))
names = []
for name in dir(module_or_class):
if name.startswith(self._prefix):
method = module_or_class.__dict__[name]
firstresult = getattr(method, 'firstresult', False)
hc = HookCaller(name, firstresult=firstresult,
argnames=varnames(method, startindex=isclass))
setattr(self.hook, name, hc)
hc = getattr(self.hook, name, None)
if hc is None:
hc = HookCaller(name, self._hookexec, module_or_class)
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)
if not names:
raise ValueError("did not find new %r hooks in %r"
%(self._prefix, module_or_class))
def getplugins(self):
""" return the complete list of registered plugins. NOTE that
you will get the internal list and need to make a copy if you
modify the list."""
return self._plugins
def get_plugins(self):
""" return the set of registered plugins. """
return set(self._plugin2hookcallers)
def isregistered(self, plugin):
""" Return True if the plugin is already registered under its
canonical name. """
return self.hasplugin(self._get_canonical_name(plugin)) or \
plugin in self._plugins
def is_registered(self, plugin):
""" Return True if the plugin is already registered. """
return plugin in self._plugin2hookcallers
def hasplugin(self, name):
""" Return True if there is a registered with the given name. """
return name in self._name2plugin
def get_canonical_name(self, plugin):
""" Return canonical name for a plugin object. Note that a plugin
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 self._name2plugin.get(name)
def listattr(self, attrname, plugins=None):
if plugins is None:
plugins = self._plugins
l = []
last = []
wrappers = []
for plugin in plugins:
def get_name(self, plugin):
""" Return name for registered plugin or None if not registered. """
for name, val in self._name2plugin.items():
if plugin == val:
return name
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:
meth = getattr(plugin, attrname)
except AttributeError:
plugin = ep.load()
except DistributionNotFound:
continue
if hasattr(meth, 'hookwrapper'):
wrappers.append(meth)
elif hasattr(meth, 'tryfirst'):
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))
self.register(plugin, name=ep.name)
self._plugin_distinfo.append((ep.dist, plugin))
return len(self._plugin_distinfo)
class MultiCall:
""" 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):
self.methods = list(methods)
self.methods = methods
self.kwargs = kwargs
self.kwargs["__multicall__"] = self
self.results = []
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):
all_kwargs = self.kwargs
self.results = results = []
firstresult = self.firstresult
while self.methods:
method = self.methods.pop()
args = [all_kwargs[argname] for argname in varnames(method)]
@ -342,11 +432,19 @@ class MultiCall:
return wrapped_call(method(*args), self.execute)
res = method(*args)
if res is not None:
self.results.append(res)
if self.firstresult:
if firstresult:
return res
if not self.firstresult:
return self.results
results.append(res)
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):
@ -361,17 +459,17 @@ def varnames(func, startindex=None):
return cache["_varnames"]
except KeyError:
pass
if inspect.isclass(func):
if isclass(func):
try:
func = func.__init__
except AttributeError:
return ()
startindex = 1
else:
if not inspect.isfunction(func) and not inspect.ismethod(func):
if not isfunction(func) and not ismethod(func):
func = getattr(func, '__call__', func)
if startindex is None:
startindex = int(inspect.ismethod(func))
startindex = int(ismethod(func))
rawcode = py.code.getrawcode(func)
try:
@ -390,32 +488,95 @@ def varnames(func, startindex=None):
class HookRelay:
def __init__(self, pm):
self._pm = pm
self.trace = pm.trace.root.get("hook")
def __init__(self, trace):
self._trace = trace
class HookCaller:
def __init__(self, name, firstresult, argnames, methods=()):
class HookCaller(object):
def __init__(self, name, hook_execute, specmodule_or_class=None):
self.name = name
self.firstresult = firstresult
self.argnames = ["__multicall__"]
self.argnames.extend(argnames)
self._plugins = []
self._wrappers = []
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
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):
return "<HookCaller %r>" %(self.name,)
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):
return self._docall(self.methods + methods, kwargs)
def call_historic(self, proc=None, kwargs=None):
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):
return MultiCall(methods, kwargs,
firstresult=self.firstresult).execute()
def call_extra(self, methods, kwargs):
""" Call the hook with some additional temporarily participating
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):
@ -425,5 +586,5 @@ class PluginValidationError(Exception):
def formatdef(func):
return "%s%s" % (
func.__name__,
inspect.formatargspec(*inspect.getargspec(func))
formatargspec(*getargspec(func))
)

View File

@ -22,7 +22,7 @@ def pytest_addoption(parser):
help="store internal tracing debug information in 'pytestdebug.log'.")
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_cmdline_parse():
outcome = yield
config = outcome.get_result()
@ -34,13 +34,15 @@ def pytest_cmdline_parse():
pytest.__version__, py.__version__,
".".join(map(str, sys.version_info)),
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)
def unset_tracing():
debugfile.close()
sys.stderr.write("wrote pytestdebug information to %s\n" %
debugfile.name)
config.trace.root.setwriter(None)
undo_tracing()
config.add_cleanup(unset_tracing)
def pytest_cmdline_main(config):

View File

@ -1,27 +1,30 @@
""" 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):
"""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)."""
@hookspec_opts(historic=True)
def pytest_namespace():
"""return dict of name->object to be made globally available in
the pytest namespace. This hook is called before command line options
are parsed.
the pytest namespace. This hook is called at plugin registration
time.
"""
def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """
pytest_cmdline_parse.firstresult = True
@hookspec_opts(historic=True)
def pytest_plugin_registered(plugin, manager):
""" 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):
"""register argparse-style options and ini-style config values.
@ -47,35 +50,43 @@ def pytest_addoption(parser):
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):
""" called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. """
pytest_cmdline_main.firstresult = True
def pytest_load_initial_conftests(args, early_config, parser):
""" implements the loading of initial conftest files ahead
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
# -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_collection(session):
""" perform the collection protocol for the given session. """
pytest_collection.firstresult = True
def pytest_collection_modifyitems(session, config, items):
""" 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):
""" called after collection has been performed and modified. """
@hookspec_opts(firstresult=True)
def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling
more specific hooks.
"""
pytest_ignore_collect.firstresult = True
@hookspec_opts(firstresult=True)
def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """
pytest_collect_directory.firstresult = True
def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node
@ -112,29 +123,29 @@ def pytest_collectreport(report):
def pytest_deselected(items):
""" called for test items deselected by keyword. """
@hookspec_opts(firstresult=True)
def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """
pytest_make_collect_report.firstresult = True
# -------------------------------------------------------------------------
# Python test function related hooks
# -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_pycollect_makemodule(path, parent):
""" return a Module collector or None for the given 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
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):
""" 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):
""" call underlying test function. """
pytest_pyfunc_call.firstresult = True
def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function."""
@ -142,9 +153,16 @@ def pytest_generate_tests(metafunc):
# -------------------------------------------------------------------------
# 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):
""" (deprecated, use pytest_runtest_logstart). """
@hookspec_opts(firstresult=True)
def pytest_runtest_protocol(item, nextitem):
""" implements the runtest_setup/call/teardown protocol for
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.
"""
pytest_runtest_protocol.firstresult = True
def pytest_runtest_logstart(nodeid, location):
""" 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.
"""
@hookspec_opts(firstresult=True)
def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item` and
:py:class:`_pytest.runner.CallInfo`.
"""
pytest_runtest_makereport.firstresult = True
def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to
@ -199,6 +216,9 @@ def pytest_sessionstart(session):
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
def pytest_unconfigure(config):
""" called before test process is exited. """
# -------------------------------------------------------------------------
# hooks for customising the assert methods
@ -220,9 +240,9 @@ def pytest_assertrepr_compare(config, op, left, right):
def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal reporting."""
@hookspec_opts(firstresult=True)
def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting."""
pytest_report_teststatus.firstresult = True
def pytest_terminal_summary(terminalreporter):
""" add additional section in terminal summary reporting. """
@ -236,17 +256,14 @@ def pytest_logwarning(message, code, nodeid, fslocation):
# doctest hooks
# -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest"""
pytest_doctest_prepare_content.firstresult = True
# -------------------------------------------------------------------------
# error handling and internal debugging hooks
# -------------------------------------------------------------------------
def pytest_plugin_registered(plugin, manager):
""" a new pytest plugin got registered. """
def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """

View File

@ -151,18 +151,17 @@ def pytest_ignore_collect(path, config):
ignore_paths.extend([py.path.local(x) for x in excludeopt])
return path in ignore_paths
class FSHookProxy(object):
def __init__(self, fspath, config):
class FSHookProxy:
def __init__(self, fspath, pm, remove_mods):
self.fspath = fspath
self.config = config
self.pm = pm
self.remove_mods = remove_mods
def __getattr__(self, name):
plugins = self.config._getmatchingplugins(self.fspath)
x = self.config.pluginmanager.make_hook_caller(name, plugins)
x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
self.__dict__[name] = x
return x
def compatproperty(name):
def fget(self):
# deprecated - use pytest.name
@ -362,9 +361,6 @@ class Node(object):
def listnames(self):
return [x.name for x in self.listchain()]
def getplugins(self):
return self.config._getmatchingplugins(self.fspath)
def addfinalizer(self, fin):
""" register a function to be called when this node is finalized.
@ -519,12 +515,12 @@ class Session(FSCollector):
def _makeid(self):
return ""
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_collectstart(self):
if self.shouldstop:
raise self.Interrupted(self.shouldstop)
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'):
self._testsfailed += 1
@ -541,8 +537,20 @@ class Session(FSCollector):
try:
return self._fs2hookproxy[fspath]
except KeyError:
self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config)
return x
# check if we have the common case of running
# 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):
hook = self.config.hook

View File

@ -24,7 +24,7 @@ def pytest_runtest_makereport(item, call):
call.excinfo = call2.excinfo
@pytest.mark.trylast
@pytest.hookimpl_opts(trylast=True)
def pytest_runtest_setup(item):
if is_potential_nosetest(item):
if isinstance(item.parent, pytest.Generator):

View File

@ -11,7 +11,7 @@ def pytest_addoption(parser):
choices=['failed', 'all'],
help="send failed|all info to bpaste.net pastebin service.")
@pytest.mark.trylast
@pytest.hookimpl_opts(trylast=True)
def pytest_configure(config):
if config.option.pastebin == "all":
tr = config.pluginmanager.getplugin('terminalreporter')

View File

@ -11,7 +11,7 @@ import subprocess
import py
import pytest
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
@ -79,12 +79,12 @@ class HookRecorder:
self._pluginmanager = pluginmanager
self.calls = []
def _docall(hookcaller, methods, kwargs):
self.calls.append(ParsedCall(hookcaller.name, kwargs))
yield
self._undo_wrapping = add_method_wrapper(HookCaller, _docall)
#if hasattr(pluginmanager, "config"):
# pluginmanager.add_shutdown(self._undo_wrapping)
def before(hook, method, kwargs):
self.calls.append(ParsedCall(hook.name, kwargs))
def after(outcome, hook, method, kwargs):
pass
executor = TracedHookExecution(pluginmanager, before, after)
self._undo_wrapping = executor.undo
def finish_recording(self):
self._undo_wrapping()

View File

@ -172,7 +172,7 @@ def pytest_configure(config):
def pytest_sessionstart(session):
session._fixturemanager = FixtureManager(session)
@pytest.mark.trylast
@pytest.hookimpl_opts(trylast=True)
def pytest_namespace():
raises.Exception = pytest.fail.Exception
return {
@ -191,7 +191,7 @@ def pytestconfig(request):
return request.config
@pytest.mark.trylast
@pytest.hookimpl_opts(trylast=True)
def pytest_pyfunc_call(pyfuncitem):
testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction():
@ -219,7 +219,7 @@ def pytest_collect_file(path, parent):
def pytest_pycollect_makemodule(path, parent):
return Module(path, parent)
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield
res = outcome.get_result()
@ -375,13 +375,16 @@ class PyCollector(PyobjMixin, pytest.Collector):
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config,
cls=cls, module=module)
try:
methods = [module.pytest_generate_tests]
except AttributeError:
methods = []
methods = []
if hasattr(module, "pytest_generate_tests"):
methods.append(module.pytest_generate_tests)
if hasattr(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")
if not metafunc._calls:
@ -1621,7 +1624,6 @@ class FixtureManager:
self.session = session
self.config = session.config
self._arg2fixturedefs = {}
self._seenplugins = set()
self._holderobjseen = set()
self._arg2finish = {}
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
@ -1646,11 +1648,7 @@ class FixtureManager:
node)
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):
if plugin in self._seenplugins:
return
nodeid = None
try:
p = py.path.local(plugin.__file__)
@ -1665,13 +1663,6 @@ class FixtureManager:
if p.sep != "/":
nodeid = nodeid.replace(p.sep, "/")
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):
""" return a tuple of fixture names to be used. """

View File

@ -133,7 +133,7 @@ class MarkEvaluator:
return expl
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_setup(item):
evalskip = MarkEvaluator(item, 'skipif')
if evalskip.istrue():
@ -151,7 +151,7 @@ def check_xfail_no_run(item):
if not evalxfail.get('run', True):
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()

View File

@ -164,6 +164,8 @@ class TerminalReporter:
def pytest_logwarning(self, code, fslocation, message, nodeid):
warnings = self.stats.setdefault("warnings", [])
if isinstance(fslocation, tuple):
fslocation = "%s:%d" % fslocation
warning = WarningReport(code=code, fslocation=fslocation,
message=message, nodeid=nodeid)
warnings.append(warning)
@ -265,7 +267,7 @@ class TerminalReporter:
def pytest_collection_modifyitems(self):
self.report_collect(True)
@pytest.mark.trylast
@pytest.hookimpl_opts(trylast=True)
def pytest_sessionstart(self, session):
self._sessionstarttime = time.time()
if not self.showheader:
@ -350,7 +352,7 @@ class TerminalReporter:
indent = (len(stack) - 1) * " "
self._tw.line("%s%s" % (indent, col))
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus):
outcome = yield
outcome.get_result()

View File

@ -140,7 +140,7 @@ class TestCaseFunction(pytest.Function):
if traceback:
excinfo.traceback = traceback
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_makereport(item, call):
if isinstance(item, TestCaseFunction):
if item._excinfo:
@ -152,7 +152,7 @@ def pytest_runtest_makereport(item, call):
# twisted trial support
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_runtest_protocol(item):
if isinstance(item, TestCaseFunction) and \
'twisted.trial.unittest' in sys.modules:

View File

@ -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.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
@ -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.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

View File

@ -534,7 +534,7 @@ case we just write some informations out to a ``failures`` file::
import pytest
import os.path
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_makereport(item, call, __multicall__):
# execute all other hooks to obtain the report object
rep = __multicall__.execute()
@ -607,7 +607,7 @@ here is a little example implemented via a local plugin::
import pytest
@pytest.mark.tryfirst
@pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_makereport(item, call, __multicall__):
# execute all other hooks to obtain the report object
rep = __multicall__.execute()

View File

@ -56,6 +56,7 @@ pytest: helps you write better programs
- all collection, reporting, running aspects are delegated to hook functions
- customizations can be per-directory, per-project or per PyPI released plugin
- 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

View File

@ -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`:
.. _`extplugins`:
.. _`using plugins`:
Installing External Plugins / Searching
---------------------------------------
Installing and Using plugins
============================
Installing a plugin happens through any usual Python installation
tool, for example::
This section talks about installing and using third party plugins.
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 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
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
-----------------------------------------------------------
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
will be loaded as well. You can also use dotted path like this::
will be loaded as well.
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.
.. _`findpluginname`:
Finding out which plugins are active
@ -293,223 +145,3 @@ in the `pytest repository <http://bitbucket.org/pytest-dev/pytest/>`_.
_pytest.tmpdir
_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:

495
doc/en/writing_plugins.txt Normal file
View File

@ -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:

View File

@ -12,6 +12,7 @@ if __name__ == '__main__': # if run as a script or by 'python -m pytest'
# else we are imported
from _pytest.config import main, UsageError, _preloadplugins, cmdline
from _pytest.core import hookspec_opts, hookimpl_opts
from _pytest import __version__
_preloadplugins() # to populate pytest.* namespace so help(pytest) works

View File

@ -66,13 +66,12 @@ def check_open_files(config):
error.append(error[0])
raise AssertionError("\n".join(error))
@pytest.mark.trylast
def pytest_runtest_teardown(item, __multicall__):
@pytest.hookimpl_opts(hookwrapper=True, trylast=True)
def pytest_runtest_teardown(item):
yield
item.config._basedir.chdir()
if hasattr(item.config, '_openfiles'):
x = __multicall__.execute()
check_open_files(item.config)
return x
# XXX copied from execnet's conftest.py - needs to be merged
winpymap = {

View File

@ -563,7 +563,7 @@ class TestConftestCustomization:
b = testdir.mkdir("a").mkdir("b")
b.join("conftest.py").write(py.code.Source("""
import pytest
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_pycollect_makeitem():
outcome = yield
if outcome.excinfo is None:

View File

@ -313,7 +313,7 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch):
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
config = testdir.parseconfig("-p", "no:mytestplugin")
plugin = config.pluginmanager.getplugin("mytestplugin")
assert plugin == -1
assert plugin is None
def test_cmdline_processargs_simple(testdir):
testdir.makeconftest("""
@ -348,14 +348,15 @@ def test_notify_exception(testdir, capfd):
def test_load_initial_conftest_last_ordering(testdir):
from _pytest.config import get_plugin_manager
pm = get_plugin_manager()
from _pytest.config import get_config
pm = get_config().pluginmanager
class My:
def pytest_load_initial_conftests(self):
pass
m = My()
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[-2] == m.pytest_load_initial_conftests
assert l[-3].__module__ == "_pytest.config"

View File

@ -1,6 +1,6 @@
import pytest, py, os
from _pytest.core import * # noqa
from _pytest.config import get_plugin_manager
from _pytest.config import get_config
@pytest.fixture
@ -17,65 +17,410 @@ class TestPluginManager:
pm.register(42, name="abc")
with pytest.raises(ValueError):
pm.register(42, name="abc")
with pytest.raises(ValueError):
pm.register(42, name="def")
def test_pm(self, pm):
class A: pass
a1, a2 = A(), A()
pm.register(a1)
assert pm.isregistered(a1)
assert pm.is_registered(a1)
pm.register(a2, "hello")
assert pm.isregistered(a2)
l = pm.getplugins()
assert pm.is_registered(a2)
l = pm.get_plugins()
assert a1 in l
assert a2 in l
assert pm.getplugin('hello') == a2
pm.unregister(a1)
assert not pm.isregistered(a1)
assert pm.get_plugin('hello') == a2
assert pm.unregister(a1) == a1
assert not pm.is_registered(a1)
def test_register_mismatch_method(self):
pm = get_plugin_manager()
def test_pm_name(self, pm):
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:
def pytest_gurgel(self):
pass
pytest.raises(Exception, lambda: pm.register(hello()))
pytestpm.register(hello())
with pytest.raises(PluginValidationError):
pytestpm.check_pending()
def test_register_mismatch_arg(self):
pm = get_plugin_manager()
pm = get_config().pluginmanager
class hello:
def pytest_configure(self, asd):
pass
pytest.raises(Exception, lambda: pm.register(hello()))
def test_register(self):
pm = get_plugin_manager()
pm = get_config().pluginmanager
class MyPlugin:
pass
my = MyPlugin()
pm.register(my)
assert pm.getplugins()
assert pm.get_plugins()
my2 = MyPlugin()
pm.register(my2)
assert pm.getplugins()[-2:] == [my, my2]
assert set([my,my2]).issubset(pm.get_plugins())
assert pm.isregistered(my)
assert pm.isregistered(my2)
assert pm.is_registered(my)
assert pm.is_registered(my2)
pm.unregister(my)
assert not pm.isregistered(my)
assert pm.getplugins()[-1:] == [my2]
assert not pm.is_registered(my)
assert my not in pm.get_plugins()
def test_listattr(self):
plugins = PluginManager("xyz")
class api1:
x = 41
class api2:
x = 42
class api3:
x = 43
plugins.register(api1())
plugins.register(api2())
plugins.register(api3())
l = list(plugins.listattr('x'))
assert l == [41, 42, 43]
def test_register_unknown_hooks(self, pm):
class Plugin1:
def he_method1(self, arg):
return arg + 1
pm.register(Plugin1())
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
#assert not pm._unverified_hooks
assert pm.hook.he_method1(arg=1) == [2]
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:
@ -93,9 +438,12 @@ class TestPytestPluginInteractions:
def pytest_myhook(xyz):
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)
print(config.pluginmanager.getplugins())
#print(config.pluginmanager.get_plugins())
res = config.hook.pytest_myhook(xyz=10)
assert res == [11]
@ -166,64 +514,44 @@ class TestPytestPluginInteractions:
assert len(l) == 2
def test_hook_tracing(self):
pytestpm = get_plugin_manager() # fully initialized with plugins
pytestpm = get_config().pluginmanager # fully initialized with plugins
saveindent = []
class api1:
x = 41
def pytest_plugin_registered(self, plugin):
def pytest_plugin_registered(self):
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 = []
pytestpm.set_tracing(l.append)
indent = pytestpm.trace.root.indent
p = api1()
pytestpm.register(p)
pytestpm.trace.root.setwriter(l.append)
undo = pytestpm.enable_tracing()
try:
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
assert len(l) == 2
assert 'pytest_plugin_registered' in l[0]
assert 'finish' in l[1]
with pytest.raises(ValueError):
pytestpm.register(api1())
assert pytestpm.trace.root.indent == indent
assert saveindent[0] > indent
l[:] = []
with pytest.raises(ValueError):
pytestpm.register(api2())
assert pytestpm.trace.root.indent == indent
assert saveindent[0] > indent
finally:
undo()
# lower level API
def test_warn_on_deprecated_multicall(self, pytestpm):
class Plugin:
def pytest_configure(self, __multicall__):
pass
def test_listattr(self):
pluginmanager = PluginManager("xyz")
class My2:
x = 42
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]
before = list(pytestpm._warnings)
pytestpm.register(Plugin())
assert len(pytestpm._warnings) == len(before) + 1
assert "deprecated" in pytestpm._warnings[-1]["message"]
def test_namespace_has_default_and_env_plugins(testdir):
@ -373,35 +701,6 @@ class TestMultiCall:
assert res == []
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 m1():
pass
@ -598,109 +897,6 @@ def test_importplugin_issue375(testdir, pytestpm):
assert "qwe" not 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
from _pytest.config import PytestPluginManager
@ -710,21 +906,21 @@ class TestPytestPluginManager:
pm = PytestPluginManager()
mod = py.std.types.ModuleType("x.y.pytest_hello")
pm.register(mod)
assert pm.isregistered(mod)
l = pm.getplugins()
assert pm.is_registered(mod)
l = pm.get_plugins()
assert mod in l
pytest.raises(ValueError, "pm.register(mod)")
pytest.raises(ValueError, lambda: pm.register(mod))
#assert not pm.isregistered(mod2)
assert pm.getplugins() == l
#assert not pm.is_registered(mod2)
assert pm.get_plugins() == l
def test_canonical_import(self, monkeypatch):
mod = py.std.types.ModuleType("pytest_xyz")
monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod)
pm = PytestPluginManager()
pm.import_plugin('pytest_xyz')
assert pm.getplugin('pytest_xyz') == mod
assert pm.isregistered(mod)
assert pm.get_plugin('pytest_xyz') == mod
assert pm.is_registered(mod)
def test_consider_module(self, testdir, pytestpm):
testdir.syspathinsert()
@ -733,11 +929,11 @@ class TestPytestPluginManager:
mod = py.std.types.ModuleType("temp")
mod.pytest_plugins = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod)
assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1"
assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2"
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
def test_consider_module_import_module(self, testdir):
pytestpm = get_plugin_manager()
pytestpm = get_config().pluginmanager
mod = py.std.types.ModuleType("x")
mod.pytest_plugins = "pytest_a"
aplugin = testdir.makepyfile(pytest_a="#")
@ -776,51 +972,27 @@ class TestPytestPluginManager:
testdir.syspathinsert()
testdir.makepyfile(xy123="#")
monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123')
l1 = len(pytestpm.getplugins())
l1 = len(pytestpm.get_plugins())
pytestpm.consider_env()
l2 = len(pytestpm.getplugins())
l2 = len(pytestpm.get_plugins())
assert l2 == l1 + 1
assert pytestpm.getplugin('xy123')
assert pytestpm.get_plugin('xy123')
pytestpm.consider_env()
l3 = len(pytestpm.getplugins())
l3 = len(pytestpm.get_plugins())
assert l2 == l3
def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm):
pkg_resources = pytest.importorskip("pkg_resources")
def my_iter(name):
assert name == "pytest11"
class EntryPoint:
name = "pytest_mytestplugin"
dist = None
def load(self):
class PseudoPlugin:
x = 42
return PseudoPlugin()
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
pytestpm.consider_setuptools_entrypoints()
plugin = pytestpm.getplugin("mytestplugin")
assert plugin.x == 42
def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm):
monkeypatch.setitem(py.std.sys.modules, 'pkg_resources',
py.std.types.ModuleType("pkg_resources"))
pytestpm.consider_setuptools_entrypoints()
# ok, we did not explode
def test_pluginmanager_ENV_startup(self, testdir, monkeypatch):
testdir.makepyfile(pytest_x500="#")
p = testdir.makepyfile("""
import pytest
def test_hello(pytestconfig):
plugin = pytestconfig.pluginmanager.getplugin('pytest_x500')
plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
assert plugin is not None
""")
monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",")
result = testdir.runpytest(p)
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed in*"])
result.stdout.fnmatch_lines(["*1 passed*"])
def test_import_plugin_importname(self, testdir, pytestpm):
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
@ -830,13 +1002,13 @@ class TestPytestPluginManager:
pluginname = "pytest_hello"
testdir.makepyfile(**{pluginname: ""})
pytestpm.import_plugin("pytest_hello")
len1 = len(pytestpm.getplugins())
len1 = len(pytestpm.get_plugins())
pytestpm.import_plugin("pytest_hello")
len2 = len(pytestpm.getplugins())
len2 = len(pytestpm.get_plugins())
assert len1 == len2
plugin1 = pytestpm.getplugin("pytest_hello")
plugin1 = pytestpm.get_plugin("pytest_hello")
assert plugin1.__name__.endswith('pytest_hello')
plugin2 = pytestpm.getplugin("pytest_hello")
plugin2 = pytestpm.get_plugin("pytest_hello")
assert plugin2 is plugin1
def test_import_plugin_dotted_name(self, testdir, pytestpm):
@ -847,7 +1019,7 @@ class TestPytestPluginManager:
testdir.mkpydir("pkg").join("plug.py").write("x=3")
pluginname = "pkg.plug"
pytestpm.import_plugin(pluginname)
mod = pytestpm.getplugin("pkg.plug")
mod = pytestpm.get_plugin("pkg.plug")
assert mod.x == 3
def test_consider_conftest_deps(self, testdir, pytestpm):
@ -863,15 +1035,16 @@ class TestPytestPluginManagerBootstrapming:
def test_plugin_prevent_register(self, pytestpm):
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
l1 = pytestpm.getplugins()
l1 = pytestpm.get_plugins()
pytestpm.register(42, name="abc")
l2 = pytestpm.getplugins()
l2 = pytestpm.get_plugins()
assert len(l2) == len(l1)
assert 42 not in l2
def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm):
pytestpm.register(42, name="abc")
l1 = pytestpm.getplugins()
l1 = pytestpm.get_plugins()
assert 42 in l1
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
l2 = pytestpm.getplugins()
l2 = pytestpm.get_plugins()
assert 42 not in l2

View File

@ -38,7 +38,7 @@ def test_hookvalidation_unknown(testdir):
def test_hookvalidation_optional(testdir):
testdir.makeconftest("""
import pytest
@pytest.mark.optionalhook
@pytest.hookimpl_opts(optionalhook=True)
def pytest_hello(xyz):
pass
""")

View File

@ -510,7 +510,7 @@ class TestKeywordSelection:
""")
testdir.makepyfile(conftest="""
import pytest
@pytest.mark.hookwrapper
@pytest.hookimpl_opts(hookwrapper=True)
def pytest_pycollect_makeitem(name):
outcome = yield
if name == "TestClass":

View File

@ -457,7 +457,7 @@ class TestTerminalFunctional:
])
assert result.ret == 1
if not pytestconfig.pluginmanager.hasplugin("xdist"):
if not pytestconfig.pluginmanager.get_plugin("xdist"):
pytest.skip("xdist plugin not installed")
result = testdir.runpytest(p1, '-v', '-n 1')