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 change but it might still break 3rd party plugins which relied on
details like especially the pluginmanager.add_shutdown() API. details like especially the pluginmanager.add_shutdown() API.
Thanks Holger Krekel. Thanks Holger Krekel.
- pluginmanagement: introduce ``pytest.hookimpl_opts`` and
``pytest.hookspec_opts`` decorators for setting impl/spec
specific parameters. This substitutes the previous
now deprecated use of ``pytest.mark`` which is meant to
contain markers for test functions only.
- write/refine docs for "writing plugins" which now have their
own page and are separate from the "using/installing plugins`` page.
- fix issue732: properly unregister plugins from any hook calling
sites allowing to have temporary plugins during test execution.
- deprecate and warn about ``__multicall__`` argument in hook
implementations. Use the ``hookwrapper`` mechanism instead already
introduced with pytest-2.7.
2.7.1.dev (compared to 2.7.0) 2.7.1.dev (compared to 2.7.0)
----------------------------- -----------------------------

View File

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

View File

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

View File

@ -2,11 +2,65 @@
PluginManager, basic initialization and tracing. PluginManager, basic initialization and tracing.
""" """
import sys import sys
import inspect from inspect import isfunction, ismethod, isclass, formatargspec, getargspec
import py import py
py3 = sys.version_info > (3,0) py3 = sys.version_info > (3,0)
def hookspec_opts(firstresult=False, historic=False):
""" returns a decorator which will define a function as a hook specfication.
If firstresult is True the 1:N hook call (N being the number of registered
hook implementation functions) will stop at I<=N when the I'th function
returns a non-None result.
If historic is True calls to a hook will be memorized and replayed
on later registered plugins.
"""
def setattr_hookspec_opts(func):
if historic and firstresult:
raise ValueError("cannot have a historic firstresult hook")
if firstresult:
func.firstresult = firstresult
if historic:
func.historic = historic
return func
return setattr_hookspec_opts
def hookimpl_opts(hookwrapper=False, optionalhook=False,
tryfirst=False, trylast=False):
""" Return a decorator which marks a function as a hook implementation.
If optionalhook is True a missing matching hook specification will not result
in an error (by default it is an error if no matching spec is found).
If tryfirst is True this hook implementation will run as early as possible
in the chain of N hook implementations for a specfication.
If trylast is True this hook implementation will run as late as possible
in the chain of N hook implementations.
If hookwrapper is True the hook implementations needs to execute exactly
one "yield". The code before the yield is run early before any non-hookwrapper
function is run. The code after the yield is run after all non-hookwrapper
function have run. The yield receives an ``CallOutcome`` object representing
the exception or result outcome of the inner calls (including other hookwrapper
calls).
"""
def setattr_hookimpl_opts(func):
if hookwrapper:
func.hookwrapper = True
if optionalhook:
func.optionalhook = True
if tryfirst:
func.tryfirst = True
if trylast:
func.trylast = True
return func
return setattr_hookimpl_opts
class TagTracer: class TagTracer:
def __init__(self): def __init__(self):
self._tag2proc = {} self._tag2proc = {}
@ -53,42 +107,28 @@ class TagTracer:
assert isinstance(tags, tuple) assert isinstance(tags, tuple)
self._tag2proc[tags] = processor self._tag2proc[tags] = processor
class TagTracerSub: class TagTracerSub:
def __init__(self, root, tags): def __init__(self, root, tags):
self.root = root self.root = root
self.tags = tags self.tags = tags
def __call__(self, *args): def __call__(self, *args):
self.root.processmessage(self.tags, args) self.root.processmessage(self.tags, args)
def setmyprocessor(self, processor): def setmyprocessor(self, processor):
self.root.setprocessor(self.tags, processor) self.root.setprocessor(self.tags, processor)
def get(self, name): def get(self, name):
return self.__class__(self.root, self.tags + (name,)) return self.__class__(self.root, self.tags + (name,))
def add_method_wrapper(cls, wrapper_func):
""" Substitute the function named "wrapperfunc.__name__" at class
"cls" with a function that wraps the call to the original function.
Return an undo function which can be called to reset the class to use
the old method again.
wrapper_func is called with the same arguments as the method
it wraps and its result is used as a wrap_controller for
calling the original function.
"""
name = wrapper_func.__name__
oldcall = getattr(cls, name)
def wrap_exec(*args, **kwargs):
gen = wrapper_func(*args, **kwargs)
return wrapped_call(gen, lambda: oldcall(*args, **kwargs))
setattr(cls, name, wrap_exec)
return lambda: setattr(cls, name, oldcall)
def raise_wrapfail(wrap_controller, msg): def raise_wrapfail(wrap_controller, msg):
co = wrap_controller.gi_code co = wrap_controller.gi_code
raise RuntimeError("wrap_controller at %r %s:%d %s" % raise RuntimeError("wrap_controller at %r %s:%d %s" %
(co.co_name, co.co_filename, co.co_firstlineno, msg)) (co.co_name, co.co_filename, co.co_firstlineno, msg))
def wrapped_call(wrap_controller, func): def wrapped_call(wrap_controller, func):
""" Wrap calling to a function with a generator which needs to yield """ Wrap calling to a function with a generator which needs to yield
exactly once. The yield point will trigger calling the wrapped function exactly once. The yield point will trigger calling the wrapped function
@ -133,6 +173,25 @@ class CallOutcome:
py.builtin._reraise(*ex) py.builtin._reraise(*ex)
class TracedHookExecution:
def __init__(self, pluginmanager, before, after):
self.pluginmanager = pluginmanager
self.before = before
self.after = after
self.oldcall = pluginmanager._inner_hookexec
assert not isinstance(self.oldcall, TracedHookExecution)
self.pluginmanager._inner_hookexec = self
def __call__(self, hook, methods, kwargs):
self.before(hook, methods, kwargs)
outcome = CallOutcome(lambda: self.oldcall(hook, methods, kwargs))
self.after(outcome, hook, methods, kwargs)
return outcome.get_result()
def undo(self):
self.pluginmanager._inner_hookexec = self.oldcall
class PluginManager(object): class PluginManager(object):
""" Core Pluginmanager class which manages registration """ Core Pluginmanager class which manages registration
of plugin objects and 1:N hook calling. of plugin objects and 1:N hook calling.
@ -144,197 +203,228 @@ class PluginManager(object):
plugin objects. An optional excludefunc allows to blacklist names which plugin objects. An optional excludefunc allows to blacklist names which
are not considered as hooks despite a matching prefix. are not considered as hooks despite a matching prefix.
For debugging purposes you can call ``set_tracing(writer)`` For debugging purposes you can call ``enable_tracing()``
which will subsequently send debug information to the specified which will subsequently send debug information to the trace helper.
write function.
""" """
def __init__(self, prefix, excludefunc=None): def __init__(self, prefix, excludefunc=None):
self._prefix = prefix self._prefix = prefix
self._excludefunc = excludefunc self._excludefunc = excludefunc
self._name2plugin = {} self._name2plugin = {}
self._plugins = []
self._plugin2hookcallers = {} self._plugin2hookcallers = {}
self._plugin_distinfo = []
self.trace = TagTracer().get("pluginmanage") self.trace = TagTracer().get("pluginmanage")
self.hook = HookRelay(pm=self) self.hook = HookRelay(self.trace.root.get("hook"))
self._inner_hookexec = lambda hook, methods, kwargs: \
MultiCall(methods, kwargs, hook.firstresult).execute()
def set_tracing(self, writer): def _hookexec(self, hook, methods, kwargs):
""" turn on tracing to the given writer method and # called from all hookcaller instances.
return an undo function. """ # enable_tracing will set its own wrapping function at self._inner_hookexec
self.trace.root.setwriter(writer) return self._inner_hookexec(hook, methods, kwargs)
# reconfigure HookCalling to perform tracing
assert not hasattr(self, "_wrapping")
self._wrapping = True
hooktrace = self.hook.trace def enable_tracing(self):
""" enable tracing of hook calls and return an undo function. """
hooktrace = self.hook._trace
def _docall(self, methods, kwargs): def before(hook, methods, kwargs):
hooktrace.root.indent += 1 hooktrace.root.indent += 1
hooktrace(self.name, kwargs) hooktrace(hook.name, kwargs)
box = yield
if box.excinfo is None: def after(outcome, hook, methods, kwargs):
hooktrace("finish", self.name, "-->", box.result) if outcome.excinfo is None:
hooktrace("finish", hook.name, "-->", outcome.result)
hooktrace.root.indent -= 1 hooktrace.root.indent -= 1
return add_method_wrapper(HookCaller, _docall) return TracedHookExecution(self, before, after).undo
def make_hook_caller(self, name, plugins): def subset_hook_caller(self, name, remove_plugins):
caller = getattr(self.hook, name) """ Return a new HookCaller instance for the named method
methods = self.listattr(name, plugins=plugins) which manages calls to all registered plugins except the
return HookCaller(caller.name, caller.firstresult, ones from remove_plugins. """
argnames=caller.argnames, methods=methods) orig = getattr(self.hook, name)
plugins_to_remove = [plugin for plugin in remove_plugins
if hasattr(plugin, name)]
if plugins_to_remove:
hc = HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class)
for plugin in orig._plugins:
if plugin not in plugins_to_remove:
hc._add_plugin(plugin)
# we also keep track of this hook caller so it
# gets properly removed on plugin unregistration
self._plugin2hookcallers.setdefault(plugin, []).append(hc)
return hc
return orig
def register(self, plugin, name=None): def register(self, plugin, name=None):
""" Register a plugin with the given name and ensure that all its """ Register a plugin and return its canonical name or None if the name
hook implementations are integrated. If the name is not specified is blocked from registering. Raise a ValueError if the plugin is already
we use the ``__name__`` attribute of the plugin object or, if that registered. """
doesn't exist, the id of the plugin. This method will raise a plugin_name = name or self.get_canonical_name(plugin)
ValueError if the eventual name is already registered. """
name = name or self._get_canonical_name(plugin) if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
if self._name2plugin.get(name, None) == -1: if self._name2plugin.get(plugin_name, -1) is None:
return return # blocked plugin, return None to indicate no registration
if self.hasplugin(name):
raise ValueError("Plugin already registered: %s=%s\n%s" %( raise ValueError("Plugin already registered: %s=%s\n%s" %(
name, plugin, self._name2plugin)) plugin_name, plugin, self._name2plugin))
#self.trace("registering", name, plugin)
# allow subclasses to intercept here by calling a helper
return self._do_register(plugin, name)
def _do_register(self, plugin, name): self._name2plugin[plugin_name] = plugin
hookcallers = list(self._scan_plugin(plugin))
self._plugin2hookcallers[plugin] = hookcallers
self._name2plugin[name] = plugin
self._plugins.append(plugin)
# rescan all methods for the hookcallers we found
for hookcaller in hookcallers:
self._scan_methods(hookcaller)
return True
def unregister(self, plugin): # register prefix-matching hook specs of the plugin
""" unregister the plugin object and all its contained hook implementations self._plugin2hookcallers[plugin] = hookcallers = []
for name in dir(plugin):
if name.startswith(self._prefix):
hook = getattr(self.hook, name, None)
if hook is None:
if self._excludefunc is not None and self._excludefunc(name):
continue
hook = HookCaller(name, self._hookexec)
setattr(self.hook, name, hook)
elif hook.has_spec():
self._verify_hook(hook, plugin)
hook._maybe_apply_history(getattr(plugin, name))
hookcallers.append(hook)
hook._add_plugin(plugin)
return plugin_name
def unregister(self, plugin=None, name=None):
""" unregister a plugin object and all its contained hook implementations
from internal data structures. """ from internal data structures. """
self._plugins.remove(plugin) if name is None:
for name, value in list(self._name2plugin.items()): assert plugin is not None, "one of name or plugin needs to be specified"
if value == plugin: name = self.get_name(plugin)
del self._name2plugin[name]
hookcallers = self._plugin2hookcallers.pop(plugin) if plugin is None:
for hookcaller in hookcallers: plugin = self.get_plugin(name)
self._scan_methods(hookcaller)
# if self._name2plugin[name] == None registration was blocked: ignore
if self._name2plugin.get(name):
del self._name2plugin[name]
for hookcaller in self._plugin2hookcallers.pop(plugin, []):
hookcaller._remove_plugin(plugin)
return plugin
def set_blocked(self, name):
""" block registrations of the given name, unregister if already registered. """
self.unregister(name=name)
self._name2plugin[name] = None
def addhooks(self, module_or_class): def addhooks(self, module_or_class):
""" add new hook definitions from the given module_or_class using """ add new hook definitions from the given module_or_class using
the prefix/excludefunc with which the PluginManager was initialized. """ the prefix/excludefunc with which the PluginManager was initialized. """
isclass = int(inspect.isclass(module_or_class))
names = [] names = []
for name in dir(module_or_class): for name in dir(module_or_class):
if name.startswith(self._prefix): if name.startswith(self._prefix):
method = module_or_class.__dict__[name] hc = getattr(self.hook, name, None)
firstresult = getattr(method, 'firstresult', False) if hc is None:
hc = HookCaller(name, firstresult=firstresult, hc = HookCaller(name, self._hookexec, module_or_class)
argnames=varnames(method, startindex=isclass)) setattr(self.hook, name, hc)
setattr(self.hook, name, hc) else:
# plugins registered this hook without knowing the spec
hc.set_specification(module_or_class)
for plugin in hc._plugins:
self._verify_hook(hc, plugin)
names.append(name) names.append(name)
if not names: if not names:
raise ValueError("did not find new %r hooks in %r" raise ValueError("did not find new %r hooks in %r"
%(self._prefix, module_or_class)) %(self._prefix, module_or_class))
def getplugins(self): def get_plugins(self):
""" return the complete list of registered plugins. NOTE that """ return the set of registered plugins. """
you will get the internal list and need to make a copy if you return set(self._plugin2hookcallers)
modify the list."""
return self._plugins
def isregistered(self, plugin): def is_registered(self, plugin):
""" Return True if the plugin is already registered under its """ Return True if the plugin is already registered. """
canonical name. """ return plugin in self._plugin2hookcallers
return self.hasplugin(self._get_canonical_name(plugin)) or \
plugin in self._plugins
def hasplugin(self, name): def get_canonical_name(self, plugin):
""" Return True if there is a registered with the given name. """ """ Return canonical name for a plugin object. Note that a plugin
return name in self._name2plugin may be registered under a different name which was specified
by the caller of register(plugin, name). To obtain the name
of an registered plugin use ``get_name(plugin)`` instead."""
return getattr(plugin, "__name__", None) or str(id(plugin))
def getplugin(self, name): def get_plugin(self, name):
""" Return a plugin or None for the given name. """ """ Return a plugin or None for the given name. """
return self._name2plugin.get(name) return self._name2plugin.get(name)
def listattr(self, attrname, plugins=None): def get_name(self, plugin):
if plugins is None: """ Return name for registered plugin or None if not registered. """
plugins = self._plugins for name, val in self._name2plugin.items():
l = [] if plugin == val:
last = [] return name
wrappers = []
for plugin in plugins: def _verify_hook(self, hook, plugin):
method = getattr(plugin, hook.name)
pluginname = self.get_name(plugin)
if hook.is_historic() and hasattr(method, "hookwrapper"):
raise PluginValidationError(
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %(
pluginname, hook.name))
for arg in varnames(method):
if arg not in hook.argnames:
raise PluginValidationError(
"Plugin %r\nhook %r\nargument %r not available\n"
"plugin definition: %s\n"
"available hookargs: %s" %(
pluginname, hook.name, arg, formatdef(method),
", ".join(hook.argnames)))
def check_pending(self):
""" Verify that all hooks which have not been verified against
a hook specification are optional, otherwise raise PluginValidationError"""
for name in self.hook.__dict__:
if name.startswith(self._prefix):
hook = getattr(self.hook, name)
if not hook.has_spec():
for plugin in hook._plugins:
method = getattr(plugin, hook.name)
if not getattr(method, "optionalhook", False):
raise PluginValidationError(
"unknown hook %r in plugin %r" %(name, plugin))
def load_setuptools_entrypoints(self, entrypoint_name):
""" Load modules from querying the specified setuptools entrypoint name.
Return the number of loaded plugins. """
from pkg_resources import iter_entry_points, DistributionNotFound
for ep in iter_entry_points(entrypoint_name):
# is the plugin registered or blocked?
if self.get_plugin(ep.name) or ep.name in self._name2plugin:
continue
try: try:
meth = getattr(plugin, attrname) plugin = ep.load()
except AttributeError: except DistributionNotFound:
continue continue
if hasattr(meth, 'hookwrapper'): self.register(plugin, name=ep.name)
wrappers.append(meth) self._plugin_distinfo.append((ep.dist, plugin))
elif hasattr(meth, 'tryfirst'): return len(self._plugin_distinfo)
last.append(meth)
elif hasattr(meth, 'trylast'):
l.insert(0, meth)
else:
l.append(meth)
l.extend(last)
l.extend(wrappers)
return l
def _scan_methods(self, hookcaller):
hookcaller.methods = self.listattr(hookcaller.name)
def call_plugin(self, plugin, methname, kwargs):
return MultiCall(methods=self.listattr(methname, plugins=[plugin]),
kwargs=kwargs, firstresult=True).execute()
def _scan_plugin(self, plugin):
def fail(msg, *args):
name = getattr(plugin, '__name__', plugin)
raise PluginValidationError("plugin %r\n%s" %(name, msg % args))
for name in dir(plugin):
if name[0] == "_" or not name.startswith(self._prefix):
continue
hook = getattr(self.hook, name, None)
method = getattr(plugin, name)
if hook is None:
if self._excludefunc is not None and self._excludefunc(name):
continue
if getattr(method, 'optionalhook', False):
continue
fail("found unknown hook: %r", name)
for arg in varnames(method):
if arg not in hook.argnames:
fail("argument %r not available\n"
"actual definition: %s\n"
"available hookargs: %s",
arg, formatdef(method),
", ".join(hook.argnames))
yield hook
def _get_canonical_name(self, plugin):
return getattr(plugin, "__name__", None) or str(id(plugin))
class MultiCall: class MultiCall:
""" execute a call into multiple python functions/methods. """ """ execute a call into multiple python functions/methods. """
# XXX note that the __multicall__ argument is supported only
# for pytest compatibility reasons. It was never officially
# supported there and is explicitely deprecated since 2.8
# so we can remove it soon, allowing to avoid the below recursion
# in execute() and simplify/speed up the execute loop.
def __init__(self, methods, kwargs, firstresult=False): def __init__(self, methods, kwargs, firstresult=False):
self.methods = list(methods) self.methods = methods
self.kwargs = kwargs self.kwargs = kwargs
self.kwargs["__multicall__"] = self self.kwargs["__multicall__"] = self
self.results = []
self.firstresult = firstresult self.firstresult = firstresult
def __repr__(self):
status = "%d results, %d meths" % (len(self.results), len(self.methods))
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
def execute(self): def execute(self):
all_kwargs = self.kwargs all_kwargs = self.kwargs
self.results = results = []
firstresult = self.firstresult
while self.methods: while self.methods:
method = self.methods.pop() method = self.methods.pop()
args = [all_kwargs[argname] for argname in varnames(method)] args = [all_kwargs[argname] for argname in varnames(method)]
@ -342,11 +432,19 @@ class MultiCall:
return wrapped_call(method(*args), self.execute) return wrapped_call(method(*args), self.execute)
res = method(*args) res = method(*args)
if res is not None: if res is not None:
self.results.append(res) if firstresult:
if self.firstresult:
return res return res
if not self.firstresult: results.append(res)
return self.results
if not firstresult:
return results
def __repr__(self):
status = "%d meths" % (len(self.methods),)
if hasattr(self, "results"):
status = ("%d results, " % len(self.results)) + status
return "<MultiCall %s, kwargs=%r>" %(status, self.kwargs)
def varnames(func, startindex=None): def varnames(func, startindex=None):
@ -361,17 +459,17 @@ def varnames(func, startindex=None):
return cache["_varnames"] return cache["_varnames"]
except KeyError: except KeyError:
pass pass
if inspect.isclass(func): if isclass(func):
try: try:
func = func.__init__ func = func.__init__
except AttributeError: except AttributeError:
return () return ()
startindex = 1 startindex = 1
else: else:
if not inspect.isfunction(func) and not inspect.ismethod(func): if not isfunction(func) and not ismethod(func):
func = getattr(func, '__call__', func) func = getattr(func, '__call__', func)
if startindex is None: if startindex is None:
startindex = int(inspect.ismethod(func)) startindex = int(ismethod(func))
rawcode = py.code.getrawcode(func) rawcode = py.code.getrawcode(func)
try: try:
@ -390,32 +488,95 @@ def varnames(func, startindex=None):
class HookRelay: class HookRelay:
def __init__(self, pm): def __init__(self, trace):
self._pm = pm self._trace = trace
self.trace = pm.trace.root.get("hook")
class HookCaller: class HookCaller(object):
def __init__(self, name, firstresult, argnames, methods=()): def __init__(self, name, hook_execute, specmodule_or_class=None):
self.name = name self.name = name
self.firstresult = firstresult self._plugins = []
self.argnames = ["__multicall__"] self._wrappers = []
self.argnames.extend(argnames) self._nonwrappers = []
self._hookexec = hook_execute
if specmodule_or_class is not None:
self.set_specification(specmodule_or_class)
def has_spec(self):
return hasattr(self, "_specmodule_or_class")
def set_specification(self, specmodule_or_class):
assert not self.has_spec()
self._specmodule_or_class = specmodule_or_class
specfunc = getattr(specmodule_or_class, self.name)
argnames = varnames(specfunc, startindex=isclass(specmodule_or_class))
assert "self" not in argnames # sanity check assert "self" not in argnames # sanity check
self.methods = methods self.argnames = ["__multicall__"] + list(argnames)
self.firstresult = getattr(specfunc, 'firstresult', False)
if hasattr(specfunc, "historic"):
self._call_history = []
def is_historic(self):
return hasattr(self, "_call_history")
def _remove_plugin(self, plugin):
self._plugins.remove(plugin)
meth = getattr(plugin, self.name)
try:
self._nonwrappers.remove(meth)
except ValueError:
self._wrappers.remove(meth)
def _add_plugin(self, plugin):
self._plugins.append(plugin)
self._add_method(getattr(plugin, self.name))
def _add_method(self, meth):
if hasattr(meth, 'hookwrapper'):
methods = self._wrappers
else:
methods = self._nonwrappers
if hasattr(meth, 'trylast'):
methods.insert(0, meth)
elif hasattr(meth, 'tryfirst'):
methods.append(meth)
else:
# find last non-tryfirst method
i = len(methods) - 1
while i >= 0 and hasattr(methods[i], "tryfirst"):
i -= 1
methods.insert(i + 1, meth)
def __repr__(self): def __repr__(self):
return "<HookCaller %r>" %(self.name,) return "<HookCaller %r>" %(self.name,)
def __call__(self, **kwargs): def __call__(self, **kwargs):
return self._docall(self.methods, kwargs) assert not self.is_historic()
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
def callextra(self, methods, **kwargs): def call_historic(self, proc=None, kwargs=None):
return self._docall(self.methods + methods, kwargs) self._call_history.append((kwargs or {}, proc))
# historizing hooks don't return results
self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
def _docall(self, methods, kwargs): def call_extra(self, methods, kwargs):
return MultiCall(methods, kwargs, """ Call the hook with some additional temporarily participating
firstresult=self.firstresult).execute() methods using the specified kwargs as call parameters. """
old = list(self._nonwrappers), list(self._wrappers)
for method in methods:
self._add_method(method)
try:
return self(**kwargs)
finally:
self._nonwrappers, self._wrappers = old
def _maybe_apply_history(self, method):
if self.is_historic():
for kwargs, proc in self._call_history:
res = self._hookexec(self, [method], kwargs)
if res and proc is not None:
proc(res[0])
class PluginValidationError(Exception): class PluginValidationError(Exception):
@ -425,5 +586,5 @@ class PluginValidationError(Exception):
def formatdef(func): def formatdef(func):
return "%s%s" % ( return "%s%s" % (
func.__name__, func.__name__,
inspect.formatargspec(*inspect.getargspec(func)) formatargspec(*getargspec(func))
) )

View File

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

View File

@ -1,27 +1,30 @@
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
from _pytest.core import hookspec_opts
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Initialization # Initialization hooks called for every plugin
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec_opts(historic=True)
def pytest_addhooks(pluginmanager): def pytest_addhooks(pluginmanager):
"""called at plugin load time to allow adding new hooks via a call to """called at plugin registration time to allow adding new hooks via a call to
pluginmanager.addhooks(module_or_class, prefix).""" pluginmanager.addhooks(module_or_class, prefix)."""
@hookspec_opts(historic=True)
def pytest_namespace(): def pytest_namespace():
"""return dict of name->object to be made globally available in """return dict of name->object to be made globally available in
the pytest namespace. This hook is called before command line options the pytest namespace. This hook is called at plugin registration
are parsed. time.
""" """
def pytest_cmdline_parse(pluginmanager, args): @hookspec_opts(historic=True)
"""return initialized config object, parsing the specified args. """ def pytest_plugin_registered(plugin, manager):
pytest_cmdline_parse.firstresult = True """ a new pytest plugin got registered. """
def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """
@hookspec_opts(historic=True)
def pytest_addoption(parser): def pytest_addoption(parser):
"""register argparse-style options and ini-style config values. """register argparse-style options and ini-style config values.
@ -47,35 +50,43 @@ def pytest_addoption(parser):
via (deprecated) ``pytest.config``. via (deprecated) ``pytest.config``.
""" """
@hookspec_opts(historic=True)
def pytest_configure(config):
""" called after command line options have been parsed
and all plugins and initial conftest files been loaded.
This hook is called for every plugin.
"""
# -------------------------------------------------------------------------
# Bootstrapping hooks called for plugins registered early enough:
# internal and 3rd party plugins as well as directly
# discoverable conftest.py local plugins.
# -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """
def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """
@hookspec_opts(firstresult=True)
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
""" called for performing the main command line action. The default """ called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop. """ implementation will invoke the configure hooks and runtest_mainloop. """
pytest_cmdline_main.firstresult = True
def pytest_load_initial_conftests(args, early_config, parser): def pytest_load_initial_conftests(args, early_config, parser):
""" implements the loading of initial conftest files ahead """ implements the loading of initial conftest files ahead
of command line option parsing. """ of command line option parsing. """
def pytest_configure(config):
""" called after command line options have been parsed
and all plugins and initial conftest files been loaded.
"""
def pytest_unconfigure(config):
""" called before test process is exited. """
def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished). """
pytest_runtestloop.firstresult = True
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# collection hooks # collection hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_collection(session): def pytest_collection(session):
""" perform the collection protocol for the given session. """ """ perform the collection protocol for the given session. """
pytest_collection.firstresult = True
def pytest_collection_modifyitems(session, config, items): def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order """ called after collection has been performed, may filter or re-order
@ -84,16 +95,16 @@ def pytest_collection_modifyitems(session, config, items):
def pytest_collection_finish(session): def pytest_collection_finish(session):
""" called after collection has been performed and modified. """ """ called after collection has been performed and modified. """
@hookspec_opts(firstresult=True)
def pytest_ignore_collect(path, config): def pytest_ignore_collect(path, config):
""" return True to prevent considering this path for collection. """ return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling This hook is consulted for all files and directories prior to calling
more specific hooks. more specific hooks.
""" """
pytest_ignore_collect.firstresult = True
@hookspec_opts(firstresult=True)
def pytest_collect_directory(path, parent): def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files. """ """ called before traversing a directory for collection files. """
pytest_collect_directory.firstresult = True
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node """ return collection Node or None for the given path. Any new node
@ -112,29 +123,29 @@ def pytest_collectreport(report):
def pytest_deselected(items): def pytest_deselected(items):
""" called for test items deselected by keyword. """ """ called for test items deselected by keyword. """
@hookspec_opts(firstresult=True)
def pytest_make_collect_report(collector): def pytest_make_collect_report(collector):
""" perform ``collector.collect()`` and return a CollectReport. """ """ perform ``collector.collect()`` and return a CollectReport. """
pytest_make_collect_report.firstresult = True
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Python test function related hooks # Python test function related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_pycollect_makemodule(path, parent): def pytest_pycollect_makemodule(path, parent):
""" return a Module collector or None for the given path. """ return a Module collector or None for the given path.
This hook will be called for each matching test module path. This hook will be called for each matching test module path.
The pytest_collect_file hook needs to be used if you want to The pytest_collect_file hook needs to be used if you want to
create test modules for files that do not match as a test module. create test modules for files that do not match as a test module.
""" """
pytest_pycollect_makemodule.firstresult = True
@hookspec_opts(firstresult=True)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
""" return custom item/collector for a python object in a module, or None. """ """ return custom item/collector for a python object in a module, or None. """
pytest_pycollect_makeitem.firstresult = True
@hookspec_opts(firstresult=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
""" call underlying test function. """ """ call underlying test function. """
pytest_pyfunc_call.firstresult = True
def pytest_generate_tests(metafunc): def pytest_generate_tests(metafunc):
""" generate (multiple) parametrized calls to a test function.""" """ generate (multiple) parametrized calls to a test function."""
@ -142,9 +153,16 @@ def pytest_generate_tests(metafunc):
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# generic runtest related hooks # generic runtest related hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished). """
def pytest_itemstart(item, node): def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """ """ (deprecated, use pytest_runtest_logstart). """
@hookspec_opts(firstresult=True)
def pytest_runtest_protocol(item, nextitem): def pytest_runtest_protocol(item, nextitem):
""" implements the runtest_setup/call/teardown protocol for """ implements the runtest_setup/call/teardown protocol for
the given test item, including capturing exceptions and calling the given test item, including capturing exceptions and calling
@ -158,7 +176,6 @@ def pytest_runtest_protocol(item, nextitem):
:return boolean: True if no further hook implementations should be invoked. :return boolean: True if no further hook implementations should be invoked.
""" """
pytest_runtest_protocol.firstresult = True
def pytest_runtest_logstart(nodeid, location): def pytest_runtest_logstart(nodeid, location):
""" signal the start of running a single test item. """ """ signal the start of running a single test item. """
@ -178,12 +195,12 @@ def pytest_runtest_teardown(item, nextitem):
so that nextitem only needs to call setup-functions. so that nextitem only needs to call setup-functions.
""" """
@hookspec_opts(firstresult=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
""" return a :py:class:`_pytest.runner.TestReport` object """ return a :py:class:`_pytest.runner.TestReport` object
for the given :py:class:`pytest.Item` and for the given :py:class:`pytest.Item` and
:py:class:`_pytest.runner.CallInfo`. :py:class:`_pytest.runner.CallInfo`.
""" """
pytest_runtest_makereport.firstresult = True
def pytest_runtest_logreport(report): def pytest_runtest_logreport(report):
""" process a test setup/call/teardown report relating to """ process a test setup/call/teardown report relating to
@ -199,6 +216,9 @@ def pytest_sessionstart(session):
def pytest_sessionfinish(session, exitstatus): def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """ """ whole test run finishes. """
def pytest_unconfigure(config):
""" called before test process is exited. """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# hooks for customising the assert methods # hooks for customising the assert methods
@ -220,9 +240,9 @@ def pytest_assertrepr_compare(config, op, left, right):
def pytest_report_header(config, startdir): def pytest_report_header(config, startdir):
""" return a string to be displayed as header info for terminal reporting.""" """ return a string to be displayed as header info for terminal reporting."""
@hookspec_opts(firstresult=True)
def pytest_report_teststatus(report): def pytest_report_teststatus(report):
""" return result-category, shortletter and verbose word for reporting.""" """ return result-category, shortletter and verbose word for reporting."""
pytest_report_teststatus.firstresult = True
def pytest_terminal_summary(terminalreporter): def pytest_terminal_summary(terminalreporter):
""" add additional section in terminal summary reporting. """ """ add additional section in terminal summary reporting. """
@ -236,17 +256,14 @@ def pytest_logwarning(message, code, nodeid, fslocation):
# doctest hooks # doctest hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_doctest_prepare_content(content): def pytest_doctest_prepare_content(content):
""" return processed content for a given doctest""" """ return processed content for a given doctest"""
pytest_doctest_prepare_content.firstresult = True
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# error handling and internal debugging hooks # error handling and internal debugging hooks
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
def pytest_plugin_registered(plugin, manager):
""" a new pytest plugin got registered. """
def pytest_internalerror(excrepr, excinfo): def pytest_internalerror(excrepr, excinfo):
""" called for internal errors. """ """ called for internal errors. """

View File

@ -151,18 +151,17 @@ def pytest_ignore_collect(path, config):
ignore_paths.extend([py.path.local(x) for x in excludeopt]) ignore_paths.extend([py.path.local(x) for x in excludeopt])
return path in ignore_paths return path in ignore_paths
class FSHookProxy(object): class FSHookProxy:
def __init__(self, fspath, config): def __init__(self, fspath, pm, remove_mods):
self.fspath = fspath self.fspath = fspath
self.config = config self.pm = pm
self.remove_mods = remove_mods
def __getattr__(self, name): def __getattr__(self, name):
plugins = self.config._getmatchingplugins(self.fspath) x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
x = self.config.pluginmanager.make_hook_caller(name, plugins)
self.__dict__[name] = x self.__dict__[name] = x
return x return x
def compatproperty(name): def compatproperty(name):
def fget(self): def fget(self):
# deprecated - use pytest.name # deprecated - use pytest.name
@ -362,9 +361,6 @@ class Node(object):
def listnames(self): def listnames(self):
return [x.name for x in self.listchain()] return [x.name for x in self.listchain()]
def getplugins(self):
return self.config._getmatchingplugins(self.fspath)
def addfinalizer(self, fin): def addfinalizer(self, fin):
""" register a function to be called when this node is finalized. """ register a function to be called when this node is finalized.
@ -519,12 +515,12 @@ class Session(FSCollector):
def _makeid(self): def _makeid(self):
return "" return ""
@pytest.mark.tryfirst @pytest.hookimpl_opts(tryfirst=True)
def pytest_collectstart(self): def pytest_collectstart(self):
if self.shouldstop: if self.shouldstop:
raise self.Interrupted(self.shouldstop) raise self.Interrupted(self.shouldstop)
@pytest.mark.tryfirst @pytest.hookimpl_opts(tryfirst=True)
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
if report.failed and not hasattr(report, 'wasxfail'): if report.failed and not hasattr(report, 'wasxfail'):
self._testsfailed += 1 self._testsfailed += 1
@ -541,8 +537,20 @@ class Session(FSCollector):
try: try:
return self._fs2hookproxy[fspath] return self._fs2hookproxy[fspath]
except KeyError: except KeyError:
self._fs2hookproxy[fspath] = x = FSHookProxy(fspath, self.config) # check if we have the common case of running
return x # hooks with all conftest.py filesall conftest.py
pm = self.config.pluginmanager
my_conftestmodules = pm._getconftestmodules(fspath)
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods:
# one or more conftests are not in use at this fspath
proxy = FSHookProxy(fspath, pm, remove_mods)
else:
# all plugis are active for this fspath
proxy = self.config.hook
self._fs2hookproxy[fspath] = proxy
return proxy
def perform_collect(self, args=None, genitems=True): def perform_collect(self, args=None, genitems=True):
hook = self.config.hook hook = self.config.hook

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import subprocess
import py import py
import pytest import pytest
from py.builtin import print_ from py.builtin import print_
from _pytest.core import HookCaller, add_method_wrapper from _pytest.core import TracedHookExecution
from _pytest.main import Session, EXIT_OK from _pytest.main import Session, EXIT_OK
@ -79,12 +79,12 @@ class HookRecorder:
self._pluginmanager = pluginmanager self._pluginmanager = pluginmanager
self.calls = [] self.calls = []
def _docall(hookcaller, methods, kwargs): def before(hook, method, kwargs):
self.calls.append(ParsedCall(hookcaller.name, kwargs)) self.calls.append(ParsedCall(hook.name, kwargs))
yield def after(outcome, hook, method, kwargs):
self._undo_wrapping = add_method_wrapper(HookCaller, _docall) pass
#if hasattr(pluginmanager, "config"): executor = TracedHookExecution(pluginmanager, before, after)
# pluginmanager.add_shutdown(self._undo_wrapping) self._undo_wrapping = executor.undo
def finish_recording(self): def finish_recording(self):
self._undo_wrapping() self._undo_wrapping()

View File

@ -172,7 +172,7 @@ def pytest_configure(config):
def pytest_sessionstart(session): def pytest_sessionstart(session):
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
@pytest.mark.trylast @pytest.hookimpl_opts(trylast=True)
def pytest_namespace(): def pytest_namespace():
raises.Exception = pytest.fail.Exception raises.Exception = pytest.fail.Exception
return { return {
@ -191,7 +191,7 @@ def pytestconfig(request):
return request.config return request.config
@pytest.mark.trylast @pytest.hookimpl_opts(trylast=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
testfunction = pyfuncitem.obj testfunction = pyfuncitem.obj
if pyfuncitem._isyieldedfunction(): if pyfuncitem._isyieldedfunction():
@ -219,7 +219,7 @@ def pytest_collect_file(path, parent):
def pytest_pycollect_makemodule(path, parent): def pytest_pycollect_makemodule(path, parent):
return Module(path, parent) return Module(path, parent)
@pytest.mark.hookwrapper @pytest.hookimpl_opts(hookwrapper=True)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
outcome = yield outcome = yield
res = outcome.get_result() res = outcome.get_result()
@ -375,13 +375,16 @@ class PyCollector(PyobjMixin, pytest.Collector):
fixtureinfo = fm.getfixtureinfo(self, funcobj, cls) fixtureinfo = fm.getfixtureinfo(self, funcobj, cls)
metafunc = Metafunc(funcobj, fixtureinfo, self.config, metafunc = Metafunc(funcobj, fixtureinfo, self.config,
cls=cls, module=module) cls=cls, module=module)
try: methods = []
methods = [module.pytest_generate_tests] if hasattr(module, "pytest_generate_tests"):
except AttributeError: methods.append(module.pytest_generate_tests)
methods = []
if hasattr(cls, "pytest_generate_tests"): if hasattr(cls, "pytest_generate_tests"):
methods.append(cls().pytest_generate_tests) methods.append(cls().pytest_generate_tests)
self.ihook.pytest_generate_tests.callextra(methods, metafunc=metafunc) if methods:
self.ihook.pytest_generate_tests.call_extra(methods,
dict(metafunc=metafunc))
else:
self.ihook.pytest_generate_tests(metafunc=metafunc)
Function = self._getcustomclass("Function") Function = self._getcustomclass("Function")
if not metafunc._calls: if not metafunc._calls:
@ -1621,7 +1624,6 @@ class FixtureManager:
self.session = session self.session = session
self.config = session.config self.config = session.config
self._arg2fixturedefs = {} self._arg2fixturedefs = {}
self._seenplugins = set()
self._holderobjseen = set() self._holderobjseen = set()
self._arg2finish = {} self._arg2finish = {}
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
@ -1646,11 +1648,7 @@ class FixtureManager:
node) node)
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs) return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
### XXX this hook should be called for historic events like pytest_configure
### so that we don't have to do the below pytest_configure hook
def pytest_plugin_registered(self, plugin): def pytest_plugin_registered(self, plugin):
if plugin in self._seenplugins:
return
nodeid = None nodeid = None
try: try:
p = py.path.local(plugin.__file__) p = py.path.local(plugin.__file__)
@ -1665,13 +1663,6 @@ class FixtureManager:
if p.sep != "/": if p.sep != "/":
nodeid = nodeid.replace(p.sep, "/") nodeid = nodeid.replace(p.sep, "/")
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
self._seenplugins.add(plugin)
@pytest.mark.tryfirst
def pytest_configure(self, config):
plugins = config.pluginmanager.getplugins()
for plugin in plugins:
self.pytest_plugin_registered(plugin)
def _getautousenames(self, nodeid): def _getautousenames(self, nodeid):
""" return a tuple of fixture names to be used. """ """ return a tuple of fixture names to be used. """

View File

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

View File

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

View File

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

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.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
For an example on how to add and work with markers from a plugin, see For an example on how to add and work with markers from a plugin, see
@ -375,9 +375,9 @@ The ``--markers`` option always gives you a list of available markers::
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. @pytest.hookimpl_opts(tryfirst=True): mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. @pytest.hookimpl_opts(trylast=True): mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
Reading markers which were set from multiple places Reading markers which were set from multiple places

View File

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

View File

@ -56,6 +56,7 @@ pytest: helps you write better programs
- all collection, reporting, running aspects are delegated to hook functions - all collection, reporting, running aspects are delegated to hook functions
- customizations can be per-directory, per-project or per PyPI released plugin - customizations can be per-directory, per-project or per PyPI released plugin
- it is easy to add command line options or customize existing behaviour - it is easy to add command line options or customize existing behaviour
- :ref:`easy to write your own plugins <writing-plugins>`
.. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html .. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html

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`: .. _`external plugins`:
.. _`extplugins`: .. _`extplugins`:
.. _`using plugins`:
Installing External Plugins / Searching Installing and Using plugins
--------------------------------------- ============================
Installing a plugin happens through any usual Python installation This section talks about installing and using third party plugins.
tool, for example:: For writing your own plugins, please refer to :ref:`writing-plugins`.
Installing a third party plugin can be easily done with ``pip``::
pip install pytest-NAME pip install pytest-NAME
pip uninstall pytest-NAME pip uninstall pytest-NAME
@ -120,118 +70,20 @@ You may also discover more plugins through a `pytest- pypi.python.org search`_.
.. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search .. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search
Writing a plugin by looking at examples
---------------------------------------
.. _`setuptools`: http://pypi.python.org/pypi/setuptools
If you want to write a plugin, there are many real-life examples
you can copy from:
* a custom collection example plugin: :ref:`yaml plugin`
* around 20 `builtin plugins`_ which provide pytest's own functionality
* many `external plugins`_ providing additional features
All of these plugins implement the documented `well specified hooks`_
to extend and add functionality.
You can also :ref:`contribute your plugin to pytest-dev<submitplugin>`
once it has some happy users other than yourself.
.. _`setuptools entry points`:
Making your plugin installable by others
----------------------------------------
If you want to make your plugin externally available, you
may define a so-called entry point for your distribution so
that ``pytest`` finds your plugin module. Entry points are
a feature that is provided by `setuptools`_. pytest looks up
the ``pytest11`` entrypoint to discover its
plugins and you can thus make your plugin available by defining
it in your setuptools-invocation:
.. sourcecode:: python
# sample ./setup.py file
from setuptools import setup
setup(
name="myproject",
packages = ['myproject']
# the following makes a plugin available to pytest
entry_points = {
'pytest11': [
'name_of_plugin = myproject.pluginmodule',
]
},
)
If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define
`well specified hooks`_.
.. _`pluginorder`:
Plugin discovery order at tool startup
--------------------------------------
``pytest`` loads plugin modules at tool startup in the following way:
* by loading all builtin plugins
* by loading all plugins registered through `setuptools entry points`_.
* by pre-scanning the command line for the ``-p name`` option
and loading the specified plugin before actual command line parsing.
* by loading all :file:`conftest.py` files as inferred by the command line
invocation:
- if no test paths are specified use current dir as a test path
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
to the directory part of the first test path.
Note that pytest does not find ``conftest.py`` files in deeper nested
sub directories at tool startup. It is usually a good idea to keep
your conftest.py file in the top level test or project root directory.
* by recursively loading all plugins specified by the
``pytest_plugins`` variable in ``conftest.py`` files
Requiring/Loading plugins in a test module or conftest file Requiring/Loading plugins in a test module or conftest file
----------------------------------------------------------- -----------------------------------------------------------
You can require plugins in a test module or a conftest file like this:: You can require plugins in a test module or a conftest file like this::
pytest_plugins = "name1", "name2", pytest_plugins = "myapp.testsupport.myplugin",
When the test module or conftest plugin is loaded the specified plugins When the test module or conftest plugin is loaded the specified plugins
will be loaded as well. You can also use dotted path like this:: will be loaded as well.
pytest_plugins = "myapp.testsupport.myplugin" pytest_plugins = "myapp.testsupport.myplugin"
which will import the specified module as a ``pytest`` plugin. which will import the specified module as a ``pytest`` plugin.
Accessing another plugin by name
--------------------------------
If a plugin wants to collaborate with code from
another plugin it can obtain a reference through
the plugin manager like this:
.. sourcecode:: python
plugin = config.pluginmanager.getplugin("name_of_plugin")
If you want to look at the names of existing plugins, use
the ``--traceconfig`` option.
.. _`findpluginname`: .. _`findpluginname`:
Finding out which plugins are active Finding out which plugins are active
@ -293,223 +145,3 @@ in the `pytest repository <http://bitbucket.org/pytest-dev/pytest/>`_.
_pytest.tmpdir _pytest.tmpdir
_pytest.unittest _pytest.unittest
.. _`well specified hooks`:
pytest hook reference
=====================
Hook specification and validation
---------------------------------
``pytest`` calls hook functions to implement initialization, running,
test execution and reporting. When ``pytest`` loads a plugin it validates
that each hook function conforms to its respective hook specification.
Each hook function name and its argument names need to match a hook
specification. However, a hook function may accept *fewer* parameters
by simply not specifying them. If you mistype argument names or the
hook name itself you get an error showing the available arguments.
Initialization, command line and configuration hooks
----------------------------------------------------
.. currentmodule:: _pytest.hookspec
.. autofunction:: pytest_load_initial_conftests
.. autofunction:: pytest_cmdline_preparse
.. autofunction:: pytest_cmdline_parse
.. autofunction:: pytest_namespace
.. autofunction:: pytest_addoption
.. autofunction:: pytest_cmdline_main
.. autofunction:: pytest_configure
.. autofunction:: pytest_unconfigure
Generic "runtest" hooks
-----------------------
All runtest related hooks receive a :py:class:`pytest.Item` object.
.. autofunction:: pytest_runtest_protocol
.. autofunction:: pytest_runtest_setup
.. autofunction:: pytest_runtest_call
.. autofunction:: pytest_runtest_teardown
.. autofunction:: pytest_runtest_makereport
For deeper understanding you may look at the default implementation of
these hooks in :py:mod:`_pytest.runner` and maybe also
in :py:mod:`_pytest.pdb` which interacts with :py:mod:`_pytest.capture`
and its input/output capturing in order to immediately drop
into interactive debugging when a test failure occurs.
The :py:mod:`_pytest.terminal` reported specifically uses
the reporting hook to print information about a test run.
Collection hooks
----------------
``pytest`` calls the following hooks for collecting files and directories:
.. autofunction:: pytest_ignore_collect
.. autofunction:: pytest_collect_directory
.. autofunction:: pytest_collect_file
For influencing the collection of objects in Python modules
you can use the following hook:
.. autofunction:: pytest_pycollect_makeitem
.. autofunction:: pytest_generate_tests
After collection is complete, you can modify the order of
items, delete or otherwise amend the test items:
.. autofunction:: pytest_collection_modifyitems
Reporting hooks
---------------
Session related reporting hooks:
.. autofunction:: pytest_collectstart
.. autofunction:: pytest_itemcollected
.. autofunction:: pytest_collectreport
.. autofunction:: pytest_deselected
And here is the central hook for reporting about
test execution:
.. autofunction:: pytest_runtest_logreport
Debugging/Interaction hooks
---------------------------
There are few hooks which can be used for special
reporting or interaction with exceptions:
.. autofunction:: pytest_internalerror
.. autofunction:: pytest_keyboard_interrupt
.. autofunction:: pytest_exception_interact
Declaring new hooks
------------------------
Plugins and ``conftest.py`` files may declare new hooks that can then be
implemented by other plugins in order to alter behaviour or interact with
the new plugin:
.. autofunction:: pytest_addhooks
Hooks are usually declared as do-nothing functions that contain only
documentation describing when the hook will be called and what return values
are expected.
For an example, see `newhooks.py`_ from :ref:`xdist`.
.. _`newhooks.py`: https://bitbucket.org/pytest-dev/pytest-xdist/src/52082f70e7dd04b00361091b8af906c60fd6700f/xdist/newhooks.py?at=default
Using hooks from 3rd party plugins
-------------------------------------
Using new hooks from plugins as explained above might be a little tricky
because the standard `Hook specification and validation`_ mechanism:
if you depend on a plugin that is not installed,
validation will fail and the error message will not make much sense to your users.
One approach is to defer the hook implementation to a new plugin instead of
declaring the hook functions directly in your plugin module, for example::
# contents of myplugin.py
class DeferPlugin(object):
"""Simple plugin to defer pytest-xdist hook functions."""
def pytest_testnodedown(self, node, error):
"""standard xdist hook function.
"""
def pytest_configure(config):
if config.pluginmanager.hasplugin('xdist'):
config.pluginmanager.register(DeferPlugin())
This has the added benefit of allowing you to conditionally install hooks
depending on which plugins are installed.
hookwrapper: executing around other hooks
-------------------------------------------------
.. currentmodule:: _pytest.core
.. versionadded:: 2.7 (experimental)
pytest plugins can implement hook wrappers which which wrap the execution
of other hook implementations. A hook wrapper is a generator function
which yields exactly once. When pytest invokes hooks it first executes
hook wrappers and passes the same arguments as to the regular hooks.
At the yield point of the hook wrapper pytest will execute the next hook
implementations and return their result to the yield point in the form of
a :py:class:`CallOutcome` instance which encapsulates a result or
exception info. The yield point itself will thus typically not raise
exceptions (unless there are bugs).
Here is an example definition of a hook wrapper::
import pytest
@pytest.mark.hookwrapper
def pytest_pyfunc_call(pyfuncitem):
# do whatever you want before the next hook executes
outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple
res = outcome.get_result() # will raise if outcome was exception
# postprocess result
Note that hook wrappers don't return results themselves, they merely
perform tracing or other side effects around the actual hook implementations.
If the result of the underlying hook is a mutable object, they may modify
that result, however.
Reference of objects involved in hooks
======================================
.. autoclass:: _pytest.config.Config()
:members:
.. autoclass:: _pytest.config.Parser()
:members:
.. autoclass:: _pytest.main.Node()
:members:
.. autoclass:: _pytest.main.Collector()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Item()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Module()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Class()
:members:
:show-inheritance:
.. autoclass:: _pytest.python.Function()
:members:
:show-inheritance:
.. autoclass:: _pytest.runner.CallInfo()
:members:
.. autoclass:: _pytest.runner.TestReport()
:members:
.. autoclass:: _pytest.core.CallOutcome()
:members:

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 # else we are imported
from _pytest.config import main, UsageError, _preloadplugins, cmdline from _pytest.config import main, UsageError, _preloadplugins, cmdline
from _pytest.core import hookspec_opts, hookimpl_opts
from _pytest import __version__ from _pytest import __version__
_preloadplugins() # to populate pytest.* namespace so help(pytest) works _preloadplugins() # to populate pytest.* namespace so help(pytest) works

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import pytest, py, os import pytest, py, os
from _pytest.core import * # noqa from _pytest.core import * # noqa
from _pytest.config import get_plugin_manager from _pytest.config import get_config
@pytest.fixture @pytest.fixture
@ -17,65 +17,410 @@ class TestPluginManager:
pm.register(42, name="abc") pm.register(42, name="abc")
with pytest.raises(ValueError): with pytest.raises(ValueError):
pm.register(42, name="abc") pm.register(42, name="abc")
with pytest.raises(ValueError):
pm.register(42, name="def")
def test_pm(self, pm): def test_pm(self, pm):
class A: pass class A: pass
a1, a2 = A(), A() a1, a2 = A(), A()
pm.register(a1) pm.register(a1)
assert pm.isregistered(a1) assert pm.is_registered(a1)
pm.register(a2, "hello") pm.register(a2, "hello")
assert pm.isregistered(a2) assert pm.is_registered(a2)
l = pm.getplugins() l = pm.get_plugins()
assert a1 in l assert a1 in l
assert a2 in l assert a2 in l
assert pm.getplugin('hello') == a2 assert pm.get_plugin('hello') == a2
pm.unregister(a1) assert pm.unregister(a1) == a1
assert not pm.isregistered(a1) assert not pm.is_registered(a1)
def test_register_mismatch_method(self): def test_pm_name(self, pm):
pm = get_plugin_manager() class A: pass
a1 = A()
name = pm.register(a1, name="hello")
assert name == "hello"
pm.unregister(a1)
assert pm.get_plugin(a1) is None
assert not pm.is_registered(a1)
assert not pm.get_plugins()
name2 = pm.register(a1, name="hello")
assert name2 == name
pm.unregister(name="hello")
assert pm.get_plugin(a1) is None
assert not pm.is_registered(a1)
assert not pm.get_plugins()
def test_set_blocked(self, pm):
class A: pass
a1 = A()
name = pm.register(a1)
assert pm.is_registered(a1)
pm.set_blocked(name)
assert not pm.is_registered(a1)
pm.set_blocked("somename")
assert not pm.register(A(), "somename")
pm.unregister(name="somename")
def test_register_mismatch_method(self, pytestpm):
class hello: class hello:
def pytest_gurgel(self): def pytest_gurgel(self):
pass pass
pytest.raises(Exception, lambda: pm.register(hello())) pytestpm.register(hello())
with pytest.raises(PluginValidationError):
pytestpm.check_pending()
def test_register_mismatch_arg(self): def test_register_mismatch_arg(self):
pm = get_plugin_manager() pm = get_config().pluginmanager
class hello: class hello:
def pytest_configure(self, asd): def pytest_configure(self, asd):
pass pass
pytest.raises(Exception, lambda: pm.register(hello())) pytest.raises(Exception, lambda: pm.register(hello()))
def test_register(self): def test_register(self):
pm = get_plugin_manager() pm = get_config().pluginmanager
class MyPlugin: class MyPlugin:
pass pass
my = MyPlugin() my = MyPlugin()
pm.register(my) pm.register(my)
assert pm.getplugins() assert pm.get_plugins()
my2 = MyPlugin() my2 = MyPlugin()
pm.register(my2) pm.register(my2)
assert pm.getplugins()[-2:] == [my, my2] assert set([my,my2]).issubset(pm.get_plugins())
assert pm.isregistered(my) assert pm.is_registered(my)
assert pm.isregistered(my2) assert pm.is_registered(my2)
pm.unregister(my) pm.unregister(my)
assert not pm.isregistered(my) assert not pm.is_registered(my)
assert pm.getplugins()[-1:] == [my2] assert my not in pm.get_plugins()
def test_listattr(self): def test_register_unknown_hooks(self, pm):
plugins = PluginManager("xyz") class Plugin1:
class api1: def he_method1(self, arg):
x = 41 return arg + 1
class api2:
x = 42 pm.register(Plugin1())
class api3: class Hooks:
x = 43 def he_method1(self, arg):
plugins.register(api1()) pass
plugins.register(api2()) pm.addhooks(Hooks)
plugins.register(api3()) #assert not pm._unverified_hooks
l = list(plugins.listattr('x')) assert pm.hook.he_method1(arg=1) == [2]
assert l == [41, 42, 43]
def test_register_historic(self, pm):
class Hooks:
@hookspec_opts(historic=True)
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
pm.hook.he_method1.call_historic(kwargs=dict(arg=1))
l = []
class Plugin:
def he_method1(self, arg):
l.append(arg)
pm.register(Plugin())
assert l == [1]
class Plugin2:
def he_method1(self, arg):
l.append(arg*10)
pm.register(Plugin2())
assert l == [1, 10]
pm.hook.he_method1.call_historic(kwargs=dict(arg=12))
assert l == [1, 10, 120, 12]
def test_with_result_memorized(self, pm):
class Hooks:
@hookspec_opts(historic=True)
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
he_method1 = pm.hook.he_method1
he_method1.call_historic(lambda res: l.append(res), dict(arg=1))
l = []
class Plugin:
def he_method1(self, arg):
return arg * 10
pm.register(Plugin())
assert l == [10]
def test_register_historic_incompat_hookwrapper(self, pm):
class Hooks:
@hookspec_opts(historic=True)
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
l = []
class Plugin:
@hookimpl_opts(hookwrapper=True)
def he_method1(self, arg):
l.append(arg)
with pytest.raises(PluginValidationError):
pm.register(Plugin())
def test_call_extra(self, pm):
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
def he_method1(arg):
return arg * 10
l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1))
assert l == [10]
def test_subset_hook_caller(self, pm):
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
l = []
class Plugin1:
def he_method1(self, arg):
l.append(arg)
class Plugin2:
def he_method1(self, arg):
l.append(arg*10)
class PluginNo:
pass
plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
pm.register(plugin1)
pm.register(plugin2)
pm.register(plugin3)
pm.hook.he_method1(arg=1)
assert l == [10, 1]
l[:] = []
hc = pm.subset_hook_caller("he_method1", [plugin1])
hc(arg=2)
assert l == [20]
l[:] = []
hc = pm.subset_hook_caller("he_method1", [plugin2])
hc(arg=2)
assert l == [2]
l[:] = []
pm.unregister(plugin1)
hc(arg=2)
assert l == []
l[:] = []
pm.hook.he_method1(arg=1)
assert l == [10]
class TestAddMethodOrdering:
@pytest.fixture
def hc(self, pm):
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
return pm.hook.he_method1
@pytest.fixture
def addmeth(self, hc):
def addmeth(tryfirst=False, trylast=False, hookwrapper=False):
def wrap(func):
if tryfirst:
func.tryfirst = True
if trylast:
func.trylast = True
if hookwrapper:
func.hookwrapper = True
hc._add_method(func)
return func
return wrap
return addmeth
def test_adding_nonwrappers(self, hc, addmeth):
@addmeth()
def he_method1():
pass
@addmeth()
def he_method2():
pass
@addmeth()
def he_method3():
pass
assert hc._nonwrappers == [he_method1, he_method2, he_method3]
def test_adding_nonwrappers_trylast(self, hc, addmeth):
@addmeth()
def he_method1_middle():
pass
@addmeth(trylast=True)
def he_method1():
pass
@addmeth()
def he_method1_b():
pass
assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b]
def test_adding_nonwrappers_trylast3(self, hc, addmeth):
@addmeth()
def he_method1_a():
pass
@addmeth(trylast=True)
def he_method1_b():
pass
@addmeth()
def he_method1_c():
pass
@addmeth(trylast=True)
def he_method1_d():
pass
assert hc._nonwrappers == [he_method1_d, he_method1_b,
he_method1_a, he_method1_c]
def test_adding_nonwrappers_trylast2(self, hc, addmeth):
@addmeth()
def he_method1_middle():
pass
@addmeth()
def he_method1_b():
pass
@addmeth(trylast=True)
def he_method1():
pass
assert hc._nonwrappers == [he_method1, he_method1_middle, he_method1_b]
def test_adding_nonwrappers_tryfirst(self, hc, addmeth):
@addmeth(tryfirst=True)
def he_method1():
pass
@addmeth()
def he_method1_middle():
pass
@addmeth()
def he_method1_b():
pass
assert hc._nonwrappers == [he_method1_middle, he_method1_b, he_method1]
def test_adding_wrappers_ordering(self, hc, addmeth):
@addmeth(hookwrapper=True)
def he_method1():
pass
@addmeth()
def he_method1_middle():
pass
@addmeth(hookwrapper=True)
def he_method3():
pass
assert hc._nonwrappers == [he_method1_middle]
assert hc._wrappers == [he_method1, he_method3]
def test_adding_wrappers_ordering_tryfirst(self, hc, addmeth):
@addmeth(hookwrapper=True, tryfirst=True)
def he_method1():
pass
@addmeth(hookwrapper=True)
def he_method2():
pass
assert hc._nonwrappers == []
assert hc._wrappers == [he_method2, he_method1]
def test_hookspec_opts(self, pm):
class HookSpec:
@hookspec_opts()
def he_myhook1(self, arg1):
pass
@hookspec_opts(firstresult=True)
def he_myhook2(self, arg1):
pass
@hookspec_opts(firstresult=False)
def he_myhook3(self, arg1):
pass
pm.addhooks(HookSpec)
assert not pm.hook.he_myhook1.firstresult
assert pm.hook.he_myhook2.firstresult
assert not pm.hook.he_myhook3.firstresult
def test_hookimpl_opts(self):
for name in ["hookwrapper", "optionalhook", "tryfirst", "trylast"]:
for val in [True, False]:
@hookimpl_opts(**{name: val})
def he_myhook1(self, arg1):
pass
if val:
assert getattr(he_myhook1, name)
else:
assert not hasattr(he_myhook1, name)
def test_decorator_functional(self, pm):
class HookSpec:
@hookspec_opts(firstresult=True)
def he_myhook(self, arg1):
""" add to arg1 """
pm.addhooks(HookSpec)
class Plugin:
@hookimpl_opts()
def he_myhook(self, arg1):
return arg1 + 1
pm.register(Plugin())
results = pm.hook.he_myhook(arg1=17)
assert results == 18
def test_load_setuptools_instantiation(self, monkeypatch, pm):
pkg_resources = pytest.importorskip("pkg_resources")
def my_iter(name):
assert name == "hello"
class EntryPoint:
name = "myname"
dist = None
def load(self):
class PseudoPlugin:
x = 42
return PseudoPlugin()
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
num = pm.load_setuptools_entrypoints("hello")
assert num == 1
plugin = pm.get_plugin("myname")
assert plugin.x == 42
assert pm._plugin_distinfo == [(None, plugin)]
def test_load_setuptools_not_installed(self, monkeypatch, pm):
monkeypatch.setitem(py.std.sys.modules, 'pkg_resources',
py.std.types.ModuleType("pkg_resources"))
with pytest.raises(ImportError):
pm.load_setuptools_entrypoints("qwe")
class TestPytestPluginInteractions: class TestPytestPluginInteractions:
@ -93,9 +438,12 @@ class TestPytestPluginInteractions:
def pytest_myhook(xyz): def pytest_myhook(xyz):
return xyz + 1 return xyz + 1
""") """)
config = get_plugin_manager().config config = get_config()
pm = config.pluginmanager
pm.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=config.pluginmanager))
config.pluginmanager._importconftest(conf) config.pluginmanager._importconftest(conf)
print(config.pluginmanager.getplugins()) #print(config.pluginmanager.get_plugins())
res = config.hook.pytest_myhook(xyz=10) res = config.hook.pytest_myhook(xyz=10)
assert res == [11] assert res == [11]
@ -166,64 +514,44 @@ class TestPytestPluginInteractions:
assert len(l) == 2 assert len(l) == 2
def test_hook_tracing(self): def test_hook_tracing(self):
pytestpm = get_plugin_manager() # fully initialized with plugins pytestpm = get_config().pluginmanager # fully initialized with plugins
saveindent = [] saveindent = []
class api1: class api1:
x = 41 def pytest_plugin_registered(self):
def pytest_plugin_registered(self, plugin):
saveindent.append(pytestpm.trace.root.indent) saveindent.append(pytestpm.trace.root.indent)
raise ValueError(42) class api2:
def pytest_plugin_registered(self):
saveindent.append(pytestpm.trace.root.indent)
raise ValueError()
l = [] l = []
pytestpm.set_tracing(l.append) pytestpm.trace.root.setwriter(l.append)
indent = pytestpm.trace.root.indent undo = pytestpm.enable_tracing()
p = api1() try:
pytestpm.register(p) indent = pytestpm.trace.root.indent
p = api1()
pytestpm.register(p)
assert pytestpm.trace.root.indent == indent
assert len(l) >= 2
assert 'pytest_plugin_registered' in l[0]
assert 'finish' in l[1]
assert pytestpm.trace.root.indent == indent l[:] = []
assert len(l) == 2 with pytest.raises(ValueError):
assert 'pytest_plugin_registered' in l[0] pytestpm.register(api2())
assert 'finish' in l[1] assert pytestpm.trace.root.indent == indent
with pytest.raises(ValueError): assert saveindent[0] > indent
pytestpm.register(api1()) finally:
assert pytestpm.trace.root.indent == indent undo()
assert saveindent[0] > indent
# lower level API def test_warn_on_deprecated_multicall(self, pytestpm):
class Plugin:
def pytest_configure(self, __multicall__):
pass
def test_listattr(self): before = list(pytestpm._warnings)
pluginmanager = PluginManager("xyz") pytestpm.register(Plugin())
class My2: assert len(pytestpm._warnings) == len(before) + 1
x = 42 assert "deprecated" in pytestpm._warnings[-1]["message"]
pluginmanager.register(My2())
assert not pluginmanager.listattr("hello")
assert pluginmanager.listattr("x") == [42]
def test_listattr_tryfirst(self):
class P1:
@pytest.mark.tryfirst
def m(self):
return 17
class P2:
def m(self):
return 23
class P3:
def m(self):
return 19
pluginmanager = PluginManager("xyz")
p1 = P1()
p2 = P2()
p3 = P3()
pluginmanager.register(p1)
pluginmanager.register(p2)
pluginmanager.register(p3)
methods = pluginmanager.listattr('m')
assert methods == [p2.m, p3.m, p1.m]
del P1.m.__dict__['tryfirst']
pytest.mark.trylast(getattr(P2.m, 'im_func', P2.m))
methods = pluginmanager.listattr('m')
assert methods == [p2.m, p1.m, p3.m]
def test_namespace_has_default_and_env_plugins(testdir): def test_namespace_has_default_and_env_plugins(testdir):
@ -373,35 +701,6 @@ class TestMultiCall:
assert res == [] assert res == []
assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"] assert l == ["m1 init", "m2 init", "m2 finish", "m1 finish"]
def test_listattr_hookwrapper_ordering(self):
class P1:
@pytest.mark.hookwrapper
def m(self):
return 17
class P2:
def m(self):
return 23
class P3:
@pytest.mark.tryfirst
def m(self):
return 19
pluginmanager = PluginManager("xyz")
p1 = P1()
p2 = P2()
p3 = P3()
pluginmanager.register(p1)
pluginmanager.register(p2)
pluginmanager.register(p3)
methods = pluginmanager.listattr('m')
assert methods == [p2.m, p3.m, p1.m]
## listattr keeps a cache and deleting
## a function attribute requires clearing it
#pluginmanager._listattrcache.clear()
#del P1.m.__dict__['tryfirst']
def test_hookwrapper_not_yield(self): def test_hookwrapper_not_yield(self):
def m1(): def m1():
pass pass
@ -598,109 +897,6 @@ def test_importplugin_issue375(testdir, pytestpm):
assert "qwe" not in str(excinfo.value) assert "qwe" not in str(excinfo.value)
assert "aaaa" in str(excinfo.value) assert "aaaa" in str(excinfo.value)
class TestWrapMethod:
def test_basic_hapmypath(self):
class A:
def f(self):
return "A.f"
l = []
def f(self):
l.append(1)
box = yield
assert box.result == "A.f"
l.append(2)
undo = add_method_wrapper(A, f)
assert A().f() == "A.f"
assert l == [1,2]
undo()
l[:] = []
assert A().f() == "A.f"
assert l == []
def test_no_yield(self):
class A:
def method(self):
return
def method(self):
if 0:
yield
add_method_wrapper(A, method)
with pytest.raises(RuntimeError) as excinfo:
A().method()
assert "method" in str(excinfo.value)
assert "did not yield" in str(excinfo.value)
def test_method_raises(self):
class A:
def error(self, val):
raise ValueError(val)
l = []
def error(self, val):
l.append(val)
yield
l.append(None)
undo = add_method_wrapper(A, error)
with pytest.raises(ValueError):
A().error(42)
assert l == [42, None]
undo()
l[:] = []
with pytest.raises(ValueError):
A().error(42)
assert l == []
def test_controller_swallows_method_raises(self):
class A:
def error(self, val):
raise ValueError(val)
def error(self, val):
box = yield
box.force_result(2)
add_method_wrapper(A, error)
assert A().error(42) == 2
def test_reraise_on_controller_StopIteration(self):
class A:
def error(self, val):
raise ValueError(val)
def error(self, val):
try:
yield
except ValueError:
pass
add_method_wrapper(A, error)
with pytest.raises(ValueError):
A().error(42)
@pytest.mark.xfail(reason="if needed later")
def test_modify_call_args(self):
class A:
def error(self, val1, val2):
raise ValueError(val1+val2)
l = []
def error(self):
box = yield (1,), {'val2': 2}
assert box.excinfo[1].args == (3,)
l.append(1)
add_method_wrapper(A, error)
with pytest.raises(ValueError):
A().error()
assert l == [1]
### to be shifted to own test file ### to be shifted to own test file
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
@ -710,21 +906,21 @@ class TestPytestPluginManager:
pm = PytestPluginManager() pm = PytestPluginManager()
mod = py.std.types.ModuleType("x.y.pytest_hello") mod = py.std.types.ModuleType("x.y.pytest_hello")
pm.register(mod) pm.register(mod)
assert pm.isregistered(mod) assert pm.is_registered(mod)
l = pm.getplugins() l = pm.get_plugins()
assert mod in l assert mod in l
pytest.raises(ValueError, "pm.register(mod)") pytest.raises(ValueError, "pm.register(mod)")
pytest.raises(ValueError, lambda: pm.register(mod)) pytest.raises(ValueError, lambda: pm.register(mod))
#assert not pm.isregistered(mod2) #assert not pm.is_registered(mod2)
assert pm.getplugins() == l assert pm.get_plugins() == l
def test_canonical_import(self, monkeypatch): def test_canonical_import(self, monkeypatch):
mod = py.std.types.ModuleType("pytest_xyz") mod = py.std.types.ModuleType("pytest_xyz")
monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod)
pm = PytestPluginManager() pm = PytestPluginManager()
pm.import_plugin('pytest_xyz') pm.import_plugin('pytest_xyz')
assert pm.getplugin('pytest_xyz') == mod assert pm.get_plugin('pytest_xyz') == mod
assert pm.isregistered(mod) assert pm.is_registered(mod)
def test_consider_module(self, testdir, pytestpm): def test_consider_module(self, testdir, pytestpm):
testdir.syspathinsert() testdir.syspathinsert()
@ -733,11 +929,11 @@ class TestPytestPluginManager:
mod = py.std.types.ModuleType("temp") mod = py.std.types.ModuleType("temp")
mod.pytest_plugins = ["pytest_p1", "pytest_p2"] mod.pytest_plugins = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod) pytestpm.consider_module(mod)
assert pytestpm.getplugin("pytest_p1").__name__ == "pytest_p1" assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
assert pytestpm.getplugin("pytest_p2").__name__ == "pytest_p2" assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
def test_consider_module_import_module(self, testdir): def test_consider_module_import_module(self, testdir):
pytestpm = get_plugin_manager() pytestpm = get_config().pluginmanager
mod = py.std.types.ModuleType("x") mod = py.std.types.ModuleType("x")
mod.pytest_plugins = "pytest_a" mod.pytest_plugins = "pytest_a"
aplugin = testdir.makepyfile(pytest_a="#") aplugin = testdir.makepyfile(pytest_a="#")
@ -776,51 +972,27 @@ class TestPytestPluginManager:
testdir.syspathinsert() testdir.syspathinsert()
testdir.makepyfile(xy123="#") testdir.makepyfile(xy123="#")
monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123')
l1 = len(pytestpm.getplugins()) l1 = len(pytestpm.get_plugins())
pytestpm.consider_env() pytestpm.consider_env()
l2 = len(pytestpm.getplugins()) l2 = len(pytestpm.get_plugins())
assert l2 == l1 + 1 assert l2 == l1 + 1
assert pytestpm.getplugin('xy123') assert pytestpm.get_plugin('xy123')
pytestpm.consider_env() pytestpm.consider_env()
l3 = len(pytestpm.getplugins()) l3 = len(pytestpm.get_plugins())
assert l2 == l3 assert l2 == l3
def test_consider_setuptools_instantiation(self, monkeypatch, pytestpm):
pkg_resources = pytest.importorskip("pkg_resources")
def my_iter(name):
assert name == "pytest11"
class EntryPoint:
name = "pytest_mytestplugin"
dist = None
def load(self):
class PseudoPlugin:
x = 42
return PseudoPlugin()
return iter([EntryPoint()])
monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter)
pytestpm.consider_setuptools_entrypoints()
plugin = pytestpm.getplugin("mytestplugin")
assert plugin.x == 42
def test_consider_setuptools_not_installed(self, monkeypatch, pytestpm):
monkeypatch.setitem(py.std.sys.modules, 'pkg_resources',
py.std.types.ModuleType("pkg_resources"))
pytestpm.consider_setuptools_entrypoints()
# ok, we did not explode
def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): def test_pluginmanager_ENV_startup(self, testdir, monkeypatch):
testdir.makepyfile(pytest_x500="#") testdir.makepyfile(pytest_x500="#")
p = testdir.makepyfile(""" p = testdir.makepyfile("""
import pytest import pytest
def test_hello(pytestconfig): def test_hello(pytestconfig):
plugin = pytestconfig.pluginmanager.getplugin('pytest_x500') plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
assert plugin is not None assert plugin is not None
""") """)
monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",")
result = testdir.runpytest(p) result = testdir.runpytest(p)
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed in*"]) result.stdout.fnmatch_lines(["*1 passed*"])
def test_import_plugin_importname(self, testdir, pytestpm): def test_import_plugin_importname(self, testdir, pytestpm):
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
@ -830,13 +1002,13 @@ class TestPytestPluginManager:
pluginname = "pytest_hello" pluginname = "pytest_hello"
testdir.makepyfile(**{pluginname: ""}) testdir.makepyfile(**{pluginname: ""})
pytestpm.import_plugin("pytest_hello") pytestpm.import_plugin("pytest_hello")
len1 = len(pytestpm.getplugins()) len1 = len(pytestpm.get_plugins())
pytestpm.import_plugin("pytest_hello") pytestpm.import_plugin("pytest_hello")
len2 = len(pytestpm.getplugins()) len2 = len(pytestpm.get_plugins())
assert len1 == len2 assert len1 == len2
plugin1 = pytestpm.getplugin("pytest_hello") plugin1 = pytestpm.get_plugin("pytest_hello")
assert plugin1.__name__.endswith('pytest_hello') assert plugin1.__name__.endswith('pytest_hello')
plugin2 = pytestpm.getplugin("pytest_hello") plugin2 = pytestpm.get_plugin("pytest_hello")
assert plugin2 is plugin1 assert plugin2 is plugin1
def test_import_plugin_dotted_name(self, testdir, pytestpm): def test_import_plugin_dotted_name(self, testdir, pytestpm):
@ -847,7 +1019,7 @@ class TestPytestPluginManager:
testdir.mkpydir("pkg").join("plug.py").write("x=3") testdir.mkpydir("pkg").join("plug.py").write("x=3")
pluginname = "pkg.plug" pluginname = "pkg.plug"
pytestpm.import_plugin(pluginname) pytestpm.import_plugin(pluginname)
mod = pytestpm.getplugin("pkg.plug") mod = pytestpm.get_plugin("pkg.plug")
assert mod.x == 3 assert mod.x == 3
def test_consider_conftest_deps(self, testdir, pytestpm): def test_consider_conftest_deps(self, testdir, pytestpm):
@ -863,15 +1035,16 @@ class TestPytestPluginManagerBootstrapming:
def test_plugin_prevent_register(self, pytestpm): def test_plugin_prevent_register(self, pytestpm):
pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
l1 = pytestpm.getplugins() l1 = pytestpm.get_plugins()
pytestpm.register(42, name="abc") pytestpm.register(42, name="abc")
l2 = pytestpm.getplugins() l2 = pytestpm.get_plugins()
assert len(l2) == len(l1) assert len(l2) == len(l1)
assert 42 not in l2
def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm): def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm):
pytestpm.register(42, name="abc") pytestpm.register(42, name="abc")
l1 = pytestpm.getplugins() l1 = pytestpm.get_plugins()
assert 42 in l1 assert 42 in l1
pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
l2 = pytestpm.getplugins() l2 = pytestpm.get_plugins()
assert 42 not in l2 assert 42 not in l2

View File

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

View File

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

View File

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