From 8f43fc55acebe3847a54152775a89f83e390b4d9 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 4 Nov 2026 11:19:35 +0530 Subject: [PATCH 1/2] Keep excluding plugins registered after a subset hook caller is made #695 replaced the hasattr() check in subset_hook_caller with an intersection against the hook's current hookimpls, to handle specname. That also dropped every plugin that isn't registered yet, so a plugin passed in remove_plugins and registered after the proxy was created was called through the proxy anyway. In 1.6.0 it stayed excluded, and _SubsetHookCaller is meant to be a live view (unregistering a plugin after creation is already covered by tests). Only discard plugins that are registered and don't implement the hook. Plugins that aren't registered yet are kept, so they're excluded once they register. The specname fix from #695 is unchanged. --- src/pluggy/_manager.py | 16 ++++++++++------ testing/test_pluginmanager.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/pluggy/_manager.py b/src/pluggy/_manager.py index f9cb47dc..acf5d49e 100644 --- a/src/pluggy/_manager.py +++ b/src/pluggy/_manager.py @@ -642,12 +642,16 @@ def subset_hook_caller( """Return a proxy :class:`~pluggy.HookCaller` for the named hook which calls all registered plugins except the ones from remove_plugins.""" orig: HookCaller = getattr(self.hook, name) - plugins_to_remove = set(remove_plugins) - # Optimization: discard plugins to remove which don't actually implement - # the hook, to make checks faster. - plugins_to_remove.intersection_update( - hookimpl.plugin for hookimpl in orig._hookimpls - ) + implementing = {hookimpl.plugin for hookimpl in orig._hookimpls} + # Optimization: discard plugins to remove which are registered but don't + # implement the hook, to make checks faster. A plugin that isn't + # registered yet is kept, since the proxy is live and must still exclude + # it if it gets registered later. + plugins_to_remove = { + plugin + for plugin in remove_plugins + if plugin in implementing or not self.is_registered(plugin) + } # Optimization: if none of the plugins to remove actually implement the # hook, avoid the subset hook caller overhead. if not plugins_to_remove: diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index f145a27c..3d261718 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -811,6 +811,39 @@ def he_method1(self): assert hc() == [2] +def test_subset_hook_caller_excludes_plugin_registered_later(pm: PluginManager) -> None: + """A plugin to remove that is registered after the proxy exists stays excluded.""" + + class Hooks: + @hookspec + def he_method1(self): + pass + + pm.add_hookspecs(Hooks) + + class Plugin: + def __init__(self, name: str) -> None: + self.name = name + + @hookimpl + def he_method1(self): + return self.name + + early, late, kept = Plugin("early"), Plugin("late"), Plugin("kept") + pm.register(early) + + # Only `early` is registered when the proxies are created. + hc_late_only = pm.subset_hook_caller("he_method1", remove_plugins=[late]) + hc_both = pm.subset_hook_caller("he_method1", remove_plugins=[early, late]) + + pm.register(late) + pm.register(kept) + + assert pm.hook.he_method1() == ["kept", "late", "early"] + assert hc_late_only() == ["kept", "early"] + assert hc_both() == ["kept"] + + def test_get_hookimpls(pm: PluginManager) -> None: class Hooks: @hookspec From f3974a104f4112ac14e98bd04f0430408f536d8e Mon Sep 17 00:00:00 2001 From: Abhishek Date: Wed, 4 Nov 2026 11:20:26 +0530 Subject: [PATCH 2/2] Add changelog entry for #827 --- changelog/827.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/827.bugfix.rst diff --git a/changelog/827.bugfix.rst b/changelog/827.bugfix.rst new file mode 100644 index 00000000..7452b18c --- /dev/null +++ b/changelog/827.bugfix.rst @@ -0,0 +1 @@ +Fix :meth:`~pluggy.PluginManager.subset_hook_caller` calling a plugin from ``remove_plugins`` that was registered after the subset hook caller was created, a regression in 1.7.0.