introduce historic hook spec which will memorize calls to a hook

in order to call them on later registered plugins

--HG--
branch : more_plugin
This commit is contained in:
holger krekel 2015-04-25 11:29:11 +02:00
parent ea50ef1588
commit a63585dcab
4 changed files with 138 additions and 50 deletions

View File

@ -122,8 +122,8 @@ class PytestPluginManager(PluginManager):
if ret:
if not conftest:
self._globalplugins.append(plugin)
if hasattr(self, "config"):
self.config._register_plugin(plugin, name)
self.hook.pytest_plugin_registered(plugin=plugin,
manager=self)
return ret
def unregister(self, plugin):
@ -704,19 +704,11 @@ class Config(object):
self._cleanup = []
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
def _register_plugin(self, plugin, name):
call_plugin = self.pluginmanager.call_plugin
call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self.pluginmanager})
self.hook.pytest_plugin_registered(plugin=plugin,
manager=self.pluginmanager)
dic = call_plugin(plugin, "pytest_namespace", {}) or {}
if dic:
def do_setns(dic):
import pytest
setns(pytest, dic)
call_plugin(plugin, "pytest_addoption", {'parser': self._parser})
if self._configured:
call_plugin(plugin, "pytest_configure", {'config': self})
self.hook.pytest_namespace.call_historic({}, proc=do_setns)
self.hook.pytest_addoption.call_historic(dict(parser=self._parser))
def add_cleanup(self, func):
""" Add a function to be called when the config object gets out of
@ -726,12 +718,13 @@ class Config(object):
def _do_configure(self):
assert not self._configured
self._configured = True
self.hook.pytest_configure(config=self)
self.hook.pytest_configure.call_historic(dict(config=self))
def _ensure_unconfigure(self):
if self._configured:
self._configured = False
self.hook.pytest_unconfigure(config=self)
self.hook.pytest_configure._call_history = []
while self._cleanup:
fin = self._cleanup.pop()
fin()
@ -847,6 +840,7 @@ class Config(object):
assert not hasattr(self, 'args'), (
"can only parse cmdline args at most once per Config object")
self._origargs = args
self.hook.pytest_addhooks.call_historic(dict(pluginmanager=self.pluginmanager))
self._preparse(args)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)

View File

@ -7,16 +7,23 @@ import py
py3 = sys.version_info > (3,0)
def hookspec_opts(firstresult=False):
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
@ -226,6 +233,7 @@ class PluginManager(object):
def make_hook_caller(self, name, plugins):
caller = getattr(self.hook, name)
assert not caller.historic
hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult,
argnames=caller.argnames)
for plugin in hc.plugins:
@ -272,15 +280,18 @@ class PluginManager(object):
if name.startswith(self._prefix):
specfunc = module_or_class.__dict__[name]
firstresult = getattr(specfunc, 'firstresult', False)
historic = getattr(specfunc, 'historic', False)
hc = getattr(self.hook, name, None)
argnames = varnames(specfunc, startindex=isclass)
if hc is None:
hc = HookCaller(name, [], firstresult=firstresult,
historic=historic,
argnames=argnames)
setattr(self.hook, name, hc)
else:
# plugins registered this hook without knowing the spec
hc.setspec(firstresult=firstresult, argnames=argnames)
hc.setspec(firstresult=firstresult, argnames=argnames,
historic=historic)
for plugin in hc.plugins:
self._verify_hook(hc, specfunc, plugin)
hc.add_method(getattr(plugin, name))
@ -309,11 +320,6 @@ class PluginManager(object):
""" Return a plugin or None for the given name. """
return self._name2plugin.get(name)
def call_plugin(self, plugin, methname, kwargs):
meth = getattr(plugin, methname, None)
if meth is not None:
return MultiCall(methods=[meth], kwargs=kwargs, firstresult=True).execute()
def _scan_plugin(self, plugin):
hookcallers = []
for name in dir(plugin):
@ -334,6 +340,7 @@ class PluginManager(object):
self._verify_hook(hook, method, plugin)
hook.plugins.append(plugin)
hook.add_method(method)
hook._apply_history(method)
hookcallers.append(hook)
return hookcallers
@ -441,25 +448,30 @@ class HookRelay:
class HookCaller:
def __init__(self, name, plugins, argnames=None, firstresult=None):
def __init__(self, name, plugins, argnames=None, firstresult=None,
historic=False):
self.name = name
self.plugins = plugins
if argnames is not None:
argnames = ["__multicall__"] + list(argnames)
self.historic = historic
self.argnames = argnames
self.firstresult = firstresult
self.wrappers = []
self.nonwrappers = []
if self.historic:
self._call_history = []
@property
def pre(self):
return self.argnames is None
def setspec(self, argnames, firstresult):
def setspec(self, argnames, firstresult, historic):
assert self.pre
assert "self" not in argnames # sanity check
self.argnames = ["__multicall__"] + list(argnames)
self.firstresult = firstresult
self.historic = historic
def remove_plugin(self, plugin):
self.plugins.remove(plugin)
@ -472,6 +484,7 @@ class HookCaller:
def add_method(self, meth):
assert not self.pre
if hasattr(meth, 'hookwrapper'):
assert not self.historic
self.wrappers.append(meth)
elif hasattr(meth, 'trylast'):
self.nonwrappers.insert(0, meth)
@ -493,16 +506,27 @@ class HookCaller:
return "<HookCaller %r>" %(self.name,)
def __call__(self, **kwargs):
assert not self.historic
return self._docall(self.nonwrappers + self.wrappers, kwargs)
def callextra(self, methods, **kwargs):
assert not self.historic
return self._docall(self.nonwrappers + methods + self.wrappers,
kwargs)
def _docall(self, methods, kwargs):
assert not self.pre, self.name
return MultiCall(methods, kwargs,
firstresult=self.firstresult).execute()
return MultiCall(methods, kwargs, firstresult=self.firstresult).execute()
def call_historic(self, kwargs, proc=None):
self._call_history.append((kwargs, proc))
self._docall(self.nonwrappers + self.wrappers, kwargs)
def _apply_history(self, meth):
if hasattr(self, "_call_history"):
for kwargs, proc in self._call_history:
res = MultiCall([meth], kwargs, firstresult=True).execute()
if proc is not None:
proc(res)
class PluginValidationError(Exception):

View File

@ -3,27 +3,23 @@
from _pytest.core import hookspec_opts
# -------------------------------------------------------------------------
# Initialization
# Initialization hooks called for every plugin
# -------------------------------------------------------------------------
@hookspec_opts(historic=True)
def pytest_addhooks(pluginmanager):
"""called at plugin load time to allow adding new hooks via a call to
"""called at plugin registration time to allow adding new hooks via a call to
pluginmanager.addhooks(module_or_class, prefix)."""
@hookspec_opts(historic=True)
def pytest_namespace():
"""return dict of name->object to be made globally available in
the pytest namespace. This hook is called before command line options
are parsed.
the pytest namespace. This hook is called at plugin registration
time.
"""
@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(historic=True)
def pytest_addoption(parser):
"""register argparse-style options and ini-style config values.
@ -49,6 +45,26 @@ def pytest_addoption(parser):
via (deprecated) ``pytest.config``.
"""
@hookspec_opts(historic=True)
def pytest_configure(config):
""" called after command line options have been parsed
and all plugins and initial conftest files been loaded.
This hook is called for every plugin.
"""
# -------------------------------------------------------------------------
# Bootstrapping hooks called for plugins registered early enough:
# internal and 3rd party plugins as well as directly
# discoverable conftest.py local plugins.
# -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args. """
def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """
@hookspec_opts(firstresult=True)
def pytest_cmdline_main(config):
""" called for performing the main command line action. The default
@ -58,18 +74,6 @@ def pytest_load_initial_conftests(args, early_config, parser):
""" implements the loading of initial conftest files ahead
of command line option parsing. """
def pytest_configure(config):
""" called after command line options have been parsed
and all plugins and initial conftest files been loaded.
"""
def pytest_unconfigure(config):
""" called before test process is exited. """
@hookspec_opts(firstresult=True)
def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished). """
# -------------------------------------------------------------------------
# collection hooks
@ -144,6 +148,12 @@ def pytest_generate_tests(metafunc):
# -------------------------------------------------------------------------
# generic runtest related hooks
# -------------------------------------------------------------------------
@hookspec_opts(firstresult=True)
def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished). """
def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """
@ -201,6 +211,9 @@ def pytest_sessionstart(session):
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
def pytest_unconfigure(config):
""" called before test process is exited. """
# -------------------------------------------------------------------------
# hooks for customising the assert methods

View File

@ -77,6 +77,61 @@ class TestPluginManager:
#assert not pm._unverified_hooks
assert pm.hook.he_method1(arg=1) == [2]
def test_register_unknown_hooks(self, pm):
class Plugin1:
def he_method1(self, arg):
return arg + 1
pm.register(Plugin1())
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
#assert not pm._unverified_hooks
assert pm.hook.he_method1(arg=1) == [2]
def test_register_historic(self, pm):
class Hooks:
@hookspec_opts(historic=True)
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
pm.hook.he_method1.call_historic(kwargs=dict(arg=1))
l = []
class Plugin:
def he_method1(self, arg):
l.append(arg)
pm.register(Plugin())
assert l == [1]
class Plugin2:
def he_method1(self, arg):
l.append(arg*10)
pm.register(Plugin2())
assert l == [1, 10]
pm.hook.he_method1.call_historic(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(proc=lambda res: l.append(res), kwargs=dict(arg=1))
l = []
class Plugin:
def he_method1(self, arg):
return arg * 10
pm.register(Plugin())
assert l == [10]
class TestAddMethodOrdering:
@pytest.fixture
@ -256,8 +311,10 @@ class TestPytestPluginInteractions:
return xyz + 1
""")
config = get_plugin_manager().config
pm = config.pluginmanager
pm.hook.pytest_addhooks.call_historic(dict(pluginmanager=config.pluginmanager))
config.pluginmanager._importconftest(conf)
print(config.pluginmanager.getplugins())
#print(config.pluginmanager.getplugins())
res = config.hook.pytest_myhook(xyz=10)
assert res == [11]