Skip to content

Commit 12ea858

Browse files
committed
manager: get rid of _plugin2hookcallers
We can do everything necessary with just the `_name2plugin` dict; while it makes some operations slower (registration and unregistration), they are not in any hot path (calling) so not a problem.
1 parent ddb9161 commit 12ea858

File tree

2 files changed

+84
-16
lines changed

2 files changed

+84
-16
lines changed

src/pluggy/_manager.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ class PluginManager:
115115
def __init__(self, project_name: str) -> None:
116116
self.project_name: "Final" = project_name
117117
self._name2plugin: "Final[Dict[str, _Plugin]]" = {}
118-
self._plugin2hookcallers: "Final[Dict[_Plugin, List[_HookCaller]]]" = {}
119118
self._plugin_distinfo: "Final[List[Tuple[_Plugin, DistFacade]]]" = []
120119
self.trace: "Final" = _tracing.TagTracer().get("pluginmanage")
121120
self.hook: "Final" = _HookRelay()
@@ -138,11 +137,17 @@ def register(self, plugin: _Plugin, name: Optional[str] = None) -> Optional[str]
138137
is already registered."""
139138
plugin_name = name or self.get_canonical_name(plugin)
140139

141-
if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
140+
if plugin_name in self._name2plugin:
142141
if self._name2plugin.get(plugin_name, -1) is None:
143142
return None # blocked plugin, return None to indicate no registration
144143
raise ValueError(
145-
"Plugin already registered: %s=%s\n%s"
144+
"Plugin name already registered: %s=%s\n%s"
145+
% (plugin_name, plugin, self._name2plugin)
146+
)
147+
148+
if plugin in self._name2plugin.values():
149+
raise ValueError(
150+
"Plugin already registered under a different name: %s=%s\n%s"
146151
% (plugin_name, plugin, self._name2plugin)
147152
)
148153

@@ -151,8 +156,6 @@ def register(self, plugin: _Plugin, name: Optional[str] = None) -> Optional[str]
151156
self._name2plugin[plugin_name] = plugin
152157

153158
# register matching hook implementations of the plugin
154-
hookcallers: List[_HookCaller] = []
155-
self._plugin2hookcallers[plugin] = hookcallers
156159
for name in dir(plugin):
157160
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
158161
if hookimpl_opts is not None:
@@ -168,7 +171,6 @@ def register(self, plugin: _Plugin, name: Optional[str] = None) -> Optional[str]
168171
self._verify_hook(hook, hookimpl)
169172
hook._maybe_apply_history(hookimpl)
170173
hook._add_hookimpl(hookimpl)
171-
hookcallers.append(hook)
172174
return plugin_name
173175

174176
def parse_hookimpl_opts(
@@ -201,14 +203,16 @@ def unregister(
201203
if plugin is None:
202204
plugin = self.get_plugin(name)
203205

206+
hookcallers = self.get_hookcallers(plugin)
207+
if hookcallers:
208+
for hookcaller in hookcallers:
209+
hookcaller._remove_plugin(plugin)
210+
204211
# if self._name2plugin[name] == None registration was blocked: ignore
205212
if self._name2plugin.get(name):
206213
assert name is not None
207214
del self._name2plugin[name]
208215

209-
for hookcaller in self._plugin2hookcallers.pop(plugin, []):
210-
hookcaller._remove_plugin(plugin)
211-
212216
return plugin
213217

214218
def set_blocked(self, name: str) -> None:
@@ -254,11 +258,11 @@ def parse_hookspec_opts(
254258

255259
def get_plugins(self) -> Set[Any]:
256260
"""return the set of registered plugins."""
257-
return set(self._plugin2hookcallers)
261+
return set(self._name2plugin.values())
258262

259263
def is_registered(self, plugin: _Plugin) -> bool:
260264
"""Return ``True`` if the plugin is already registered."""
261-
return plugin in self._plugin2hookcallers
265+
return any(plugin == val for val in self._name2plugin.values())
262266

263267
def get_canonical_name(self, plugin: _Plugin) -> str:
264268
"""Return canonical name for a plugin object. Note that a plugin
@@ -373,7 +377,14 @@ def list_name_plugin(self) -> List[Tuple[str, _Plugin]]:
373377

374378
def get_hookcallers(self, plugin: _Plugin) -> Optional[List[_HookCaller]]:
375379
"""get all hook callers for the specified plugin."""
376-
return self._plugin2hookcallers.get(plugin)
380+
if self.get_name(plugin) is None:
381+
return None
382+
hookcallers = []
383+
for hookcaller in self.hook.__dict__.values():
384+
for hookimpl in hookcaller.get_hookimpls():
385+
if hookimpl.plugin is plugin:
386+
hookcallers.append(hookcaller)
387+
return hookcallers
377388

378389
def add_hookcall_monitoring(
379390
self, before: _BeforeTrace, after: _AfterTrace

testing/test_pluginmanager.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,25 @@ def he_method1(self):
153153

154154
def test_register(pm: PluginManager) -> None:
155155
class MyPlugin:
156-
pass
156+
@hookimpl
157+
def he_method1(self):
158+
...
157159

158160
my = MyPlugin()
159161
pm.register(my)
160-
assert my in pm.get_plugins()
162+
assert pm.get_plugins() == {my}
161163
my2 = MyPlugin()
162164
pm.register(my2)
163-
assert {my, my2}.issubset(pm.get_plugins())
165+
assert pm.get_plugins() == {my, my2}
164166

165167
assert pm.is_registered(my)
166168
assert pm.is_registered(my2)
167169
pm.unregister(my)
168170
assert not pm.is_registered(my)
169-
assert my not in pm.get_plugins()
171+
assert pm.get_plugins() == {my2}
172+
173+
with pytest.raises(AssertionError, match=r"not registered"):
174+
pm.unregister(my)
170175

171176

172177
def test_register_unknown_hooks(pm: PluginManager) -> None:
@@ -468,6 +473,58 @@ class PluginNo:
468473
assert hook_plugins == [plugin1, plugin2]
469474

470475

476+
def test_get_hookcallers(pm: PluginManager) -> None:
477+
class Hooks:
478+
@hookspec
479+
def he_method1(self):
480+
...
481+
482+
@hookspec
483+
def he_method2(self):
484+
...
485+
486+
pm.add_hookspecs(Hooks)
487+
488+
class Plugin1:
489+
@hookimpl
490+
def he_method1(self):
491+
...
492+
493+
@hookimpl
494+
def he_method2(self):
495+
...
496+
497+
class Plugin2:
498+
@hookimpl
499+
def he_method1(self):
500+
...
501+
502+
class Plugin3:
503+
@hookimpl
504+
def he_method2(self):
505+
...
506+
507+
plugin1 = Plugin1()
508+
pm.register(plugin1)
509+
plugin2 = Plugin2()
510+
pm.register(plugin2)
511+
plugin3 = Plugin3()
512+
pm.register(plugin3)
513+
514+
hookcallers1 = pm.get_hookcallers(plugin1)
515+
assert hookcallers1 is not None
516+
assert len(hookcallers1) == 2
517+
hookcallers2 = pm.get_hookcallers(plugin2)
518+
assert hookcallers2 is not None
519+
assert len(hookcallers2) == 1
520+
hookcallers3 = pm.get_hookcallers(plugin3)
521+
assert hookcallers3 is not None
522+
assert len(hookcallers3) == 1
523+
assert hookcallers1 == hookcallers2 + hookcallers3
524+
525+
assert pm.get_hookcallers(object()) is None
526+
527+
471528
def test_add_hookspecs_nohooks(pm: PluginManager) -> None:
472529
class NoHooks:
473530
pass

0 commit comments

Comments
 (0)