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:
parent
ea50ef1588
commit
a63585dcab
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue