fix issue732: make sure removed plugins remove all hook callers.

--HG--
branch : more_plugin
This commit is contained in:
holger krekel 2015-04-25 18:14:39 +02:00
parent 4e116ed503
commit 7364647f2f
3 changed files with 51 additions and 38 deletions

View File

@ -36,6 +36,9 @@
- 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.
2.7.1.dev (compared to 2.7.0)
-----------------------------

View File

@ -233,7 +233,14 @@ class PluginManager(object):
def make_hook_caller(self, name, plugins):
caller = getattr(self.hook, name)
return caller.clone(plugins=plugins)
hc = HookCaller(caller.name, caller._specmodule_or_class)
for plugin in plugins:
if hasattr(plugin, name):
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
def register(self, plugin, name=None):
""" Register a plugin with the given name and ensure that all its
@ -247,9 +254,8 @@ class PluginManager(object):
if self.hasplugin(name):
raise ValueError("Plugin already registered: %s=%s\n%s" %(
name, plugin, self._name2plugin))
#self.trace("registering", name, plugin)
self._plugin2hookcallers[plugin] = self._scan_plugin(plugin)
self._name2plugin[name] = plugin
self._scan_plugin(plugin)
self._plugins.append(plugin)
return True
@ -260,9 +266,8 @@ class PluginManager(object):
for name, value in list(self._name2plugin.items()):
if value == plugin:
del self._name2plugin[name]
hookcallers = self._plugin2hookcallers.pop(plugin)
for hookcaller in hookcallers:
hookcaller.remove_plugin(plugin)
for hookcaller in self._plugin2hookcallers.pop(plugin):
hookcaller._remove_plugin(plugin)
def addhooks(self, module_or_class):
""" add new hook definitions from the given module_or_class using
@ -276,7 +281,7 @@ class PluginManager(object):
setattr(self.hook, name, hc)
else:
# plugins registered this hook without knowing the spec
hc.setspec(module_or_class)
hc.set_specification(module_or_class)
for plugin in hc._plugins:
self._verify_hook(hc, plugin)
names.append(name)
@ -305,7 +310,7 @@ class PluginManager(object):
return self._name2plugin.get(name)
def _scan_plugin(self, plugin):
hookcallers = []
self._plugin2hookcallers[plugin] = hookcallers = []
for name in dir(plugin):
if name[0] == "_" or not name.startswith(self._prefix):
continue
@ -319,9 +324,8 @@ class PluginManager(object):
elif hook.has_spec():
self._verify_hook(hook, plugin)
hook._apply_history(method)
hook.add_plugin(plugin)
hookcallers.append(hook)
return hookcallers
hook._add_plugin(plugin)
def _verify_hook(self, hook, plugin):
method = getattr(plugin, hook.name)
@ -434,29 +438,12 @@ class HookCaller(object):
self._wrappers = []
self._nonwrappers = []
if specmodule_or_class is not None:
self.setspec(specmodule_or_class)
self.set_specification(specmodule_or_class)
def has_spec(self):
return hasattr(self, "_specmodule_or_class")
def clone(self, plugins=None):
assert not self.is_historic()
hc = object.__new__(HookCaller)
hc.name = self.name
hc.argnames = self.argnames
hc.firstresult = self.firstresult
if plugins is None:
hc._plugins = self._plugins
hc._wrappers = list(self._wrappers)
hc._nonwrappers = list(self._nonwrappers)
else:
hc._plugins, hc._wrappers, hc._nonwrappers = [], [], []
for plugin in plugins:
if hasattr(plugin, hc.name):
hc.add_plugin(plugin)
return hc
def setspec(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)
@ -470,7 +457,7 @@ class HookCaller(object):
def is_historic(self):
return hasattr(self, "_call_history")
def remove_plugin(self, plugin):
def _remove_plugin(self, plugin):
self._plugins.remove(plugin)
meth = getattr(plugin, self.name)
try:
@ -478,11 +465,11 @@ class HookCaller(object):
except ValueError:
self._wrappers.remove(meth)
def add_plugin(self, plugin):
def _add_plugin(self, plugin):
self._plugins.append(plugin)
self.add_method(getattr(plugin, self.name))
self._add_method(getattr(plugin, self.name))
def add_method(self, meth):
def _add_method(self, meth):
if hasattr(meth, 'hookwrapper'):
assert not self.is_historic()
self._wrappers.append(meth)
@ -511,11 +498,15 @@ class HookCaller(object):
return self._docall(self._nonwrappers + self._wrappers, kwargs)
def call_extra(self, methods, kwargs):
assert not self.is_historic()
hc = self.clone()
""" Call the hook with some additional temporarily participating
methods using the specified kwargs as call parameters. """
old = list(self._nonwrappers), list(self._wrappers)
for method in methods:
hc.add_method(method)
return hc(**kwargs)
self._add_method(method)
try:
return self(**kwargs)
finally:
self._nonwrappers, self._wrappers = old
def call_historic(self, proc=None, kwargs=None):
self._call_history.append((kwargs or {}, proc))

View File

@ -143,6 +143,25 @@ class TestPluginManager:
l = pm.hook.he_method1.call_extra([he_method1], dict(arg=1))
assert l == [10]
def test_make_hook_caller_unregistered(self, pm):
class Hooks:
def he_method1(self, arg):
pass
pm.addhooks(Hooks)
l = []
class Plugin:
def he_method1(self, arg):
l.append(arg * 10)
plugin = Plugin()
pm.register(plugin)
hc = pm.make_hook_caller("he_method1", [plugin])
hc(arg=1)
assert l == [10]
pm.unregister(plugin)
hc(arg=2)
assert l == [10]
class TestAddMethodOrdering:
@pytest.fixture
@ -163,7 +182,7 @@ class TestAddMethodOrdering:
func.trylast = True
if hookwrapper:
func.hookwrapper = True
hc.add_method(func)
hc._add_method(func)
return func
return wrap
return addmeth