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 ret:
if not conftest: if not conftest:
self._globalplugins.append(plugin) self._globalplugins.append(plugin)
if hasattr(self, "config"): self.hook.pytest_plugin_registered(plugin=plugin,
self.config._register_plugin(plugin, name) manager=self)
return ret return ret
def unregister(self, plugin): def unregister(self, plugin):
@ -704,19 +704,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({}, proc=do_setns)
if self._configured: self.hook.pytest_addoption.call_historic(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
@ -726,12 +718,13 @@ 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(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()
@ -847,6 +840,7 @@ 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(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

@ -7,16 +7,23 @@ import py
py3 = sys.version_info > (3,0) 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. """ 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 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 hook implementation functions) will stop at I<=N when the I'th function
returns a non-None result. 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): def setattr_hookspec_opts(func):
if historic and firstresult:
raise ValueError("cannot have a historic firstresult hook")
if firstresult: if firstresult:
func.firstresult = firstresult func.firstresult = firstresult
if historic:
func.historic = historic
return func return func
return setattr_hookspec_opts return setattr_hookspec_opts
@ -226,6 +233,7 @@ class PluginManager(object):
def make_hook_caller(self, name, plugins): def make_hook_caller(self, name, plugins):
caller = getattr(self.hook, name) caller = getattr(self.hook, name)
assert not caller.historic
hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult, hc = HookCaller(caller.name, plugins, firstresult=caller.firstresult,
argnames=caller.argnames) argnames=caller.argnames)
for plugin in hc.plugins: for plugin in hc.plugins:
@ -272,15 +280,18 @@ class PluginManager(object):
if name.startswith(self._prefix): if name.startswith(self._prefix):
specfunc = module_or_class.__dict__[name] specfunc = module_or_class.__dict__[name]
firstresult = getattr(specfunc, 'firstresult', False) firstresult = getattr(specfunc, 'firstresult', False)
historic = getattr(specfunc, 'historic', False)
hc = getattr(self.hook, name, None) hc = getattr(self.hook, name, None)
argnames = varnames(specfunc, startindex=isclass) argnames = varnames(specfunc, startindex=isclass)
if hc is None: if hc is None:
hc = HookCaller(name, [], firstresult=firstresult, hc = HookCaller(name, [], firstresult=firstresult,
historic=historic,
argnames=argnames) argnames=argnames)
setattr(self.hook, name, hc) setattr(self.hook, name, hc)
else: else:
# plugins registered this hook without knowing the spec # 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: for plugin in hc.plugins:
self._verify_hook(hc, specfunc, plugin) self._verify_hook(hc, specfunc, plugin)
hc.add_method(getattr(plugin, name)) hc.add_method(getattr(plugin, name))
@ -309,11 +320,6 @@ class PluginManager(object):
""" 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 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): def _scan_plugin(self, plugin):
hookcallers = [] hookcallers = []
for name in dir(plugin): for name in dir(plugin):
@ -334,6 +340,7 @@ class PluginManager(object):
self._verify_hook(hook, method, plugin) self._verify_hook(hook, method, plugin)
hook.plugins.append(plugin) hook.plugins.append(plugin)
hook.add_method(method) hook.add_method(method)
hook._apply_history(method)
hookcallers.append(hook) hookcallers.append(hook)
return hookcallers return hookcallers
@ -441,25 +448,30 @@ class HookRelay:
class HookCaller: 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.name = name
self.plugins = plugins self.plugins = plugins
if argnames is not None: if argnames is not None:
argnames = ["__multicall__"] + list(argnames) argnames = ["__multicall__"] + list(argnames)
self.historic = historic
self.argnames = argnames self.argnames = argnames
self.firstresult = firstresult self.firstresult = firstresult
self.wrappers = [] self.wrappers = []
self.nonwrappers = [] self.nonwrappers = []
if self.historic:
self._call_history = []
@property @property
def pre(self): def pre(self):
return self.argnames is None return self.argnames is None
def setspec(self, argnames, firstresult): def setspec(self, argnames, firstresult, historic):
assert self.pre assert self.pre
assert "self" not in argnames # sanity check assert "self" not in argnames # sanity check
self.argnames = ["__multicall__"] + list(argnames) self.argnames = ["__multicall__"] + list(argnames)
self.firstresult = firstresult self.firstresult = firstresult
self.historic = historic
def remove_plugin(self, plugin): def remove_plugin(self, plugin):
self.plugins.remove(plugin) self.plugins.remove(plugin)
@ -472,6 +484,7 @@ class HookCaller:
def add_method(self, meth): def add_method(self, meth):
assert not self.pre assert not self.pre
if hasattr(meth, 'hookwrapper'): if hasattr(meth, 'hookwrapper'):
assert not self.historic
self.wrappers.append(meth) self.wrappers.append(meth)
elif hasattr(meth, 'trylast'): elif hasattr(meth, 'trylast'):
self.nonwrappers.insert(0, meth) self.nonwrappers.insert(0, meth)
@ -493,16 +506,27 @@ class HookCaller:
return "<HookCaller %r>" %(self.name,) return "<HookCaller %r>" %(self.name,)
def __call__(self, **kwargs): def __call__(self, **kwargs):
assert not self.historic
return self._docall(self.nonwrappers + self.wrappers, kwargs) return self._docall(self.nonwrappers + self.wrappers, kwargs)
def callextra(self, methods, **kwargs): def callextra(self, methods, **kwargs):
assert not self.historic
return self._docall(self.nonwrappers + methods + self.wrappers, return self._docall(self.nonwrappers + methods + self.wrappers,
kwargs) kwargs)
def _docall(self, methods, 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): class PluginValidationError(Exception):

View File

@ -3,27 +3,23 @@
from _pytest.core import hookspec_opts 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.
""" """
@hookspec_opts(firstresult=True) @hookspec_opts(historic=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. """
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.
@ -49,6 +45,26 @@ 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) @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
@ -58,18 +74,6 @@ 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. """
@hookspec_opts(firstresult=True)
def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished). """
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# collection hooks # collection hooks
@ -144,6 +148,12 @@ 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). """
@ -201,6 +211,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

View File

@ -77,6 +77,61 @@ class TestPluginManager:
#assert not pm._unverified_hooks #assert not pm._unverified_hooks
assert pm.hook.he_method1(arg=1) == [2] 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: class TestAddMethodOrdering:
@pytest.fixture @pytest.fixture
@ -256,8 +311,10 @@ class TestPytestPluginInteractions:
return xyz + 1 return xyz + 1
""") """)
config = get_plugin_manager().config config = get_plugin_manager().config
pm = config.pluginmanager
pm.hook.pytest_addhooks.call_historic(dict(pluginmanager=config.pluginmanager))
config.pluginmanager._importconftest(conf) config.pluginmanager._importconftest(conf)
print(config.pluginmanager.getplugins()) #print(config.pluginmanager.getplugins())
res = config.hook.pytest_myhook(xyz=10) res = config.hook.pytest_myhook(xyz=10)
assert res == [11] assert res == [11]