Skip to content

Keep excluding plugins registered after a subset hook caller is made - #827

Open
abhishekcpp007 wants to merge 2 commits into
pytest-dev:mainfrom
abhishekcpp007:fix/subset-hook-caller-late-registration
Open

abhishekcpp007 wants to merge 2 commits into
pytest-dev:mainfrom
abhishekcpp007:fix/subset-hook-caller-late-registration

Conversation

@abhishekcpp007

@abhishekcpp007 abhishekcpp007 commented Nov 4, 2026 •

Copy link
Copy Markdown

subset_hook_caller stopped excluding a plugin that gets registered after the proxy is created. This is a regression in 1.7.0 from #695.

p = P()                                            # implements he_method1
hc = pm.subset_hook_caller("he_method1", [p])
pm.register(p)
hc(arg=0)
pluggy 1.6.0 pluggy 1.7.0
p registered after the proxy [] ['P']
remove_plugins=[a, p], only a registered at creation [] ['P']
p registered, proxy made, then unregister + re-register [] []

Cause

#695 fixed the specname case by replacing

plugins_to_remove = {plug for plug in remove_plugins if hasattr(plug, name)}

with an intersection against the hook's current _hookimpls. That's the right check for specname, but a plugin that isn't registered yet has no hookimpls, so it's dropped from the set, and with nothing left the function returns the plain orig caller. Once that plugin registers, nothing excludes it.

_SubsetHookCaller is a live view: test_subset_hook_caller unregisters a plugin after creating it, and test_historic_with_subset_hook_caller registers plugins afterwards. So whether a plugin is excluded shouldn't depend on whether it was registered at the moment the proxy was built. The last row above shows the inconsistency: re-registering an excluded plugin keeps it excluded, registering it for the first time doesn't.

Fix

Keep #695's intersection for registered plugins, and keep any plugin that isn't registered yet:

implementing = {hookimpl.plugin for hookimpl in orig._hookimpls}
plugins_to_remove = {
    plugin
    for plugin in remove_plugins
    if plugin in implementing or not self.is_registered(plugin)
}
  • Registered plugins that don't implement the hook are still discarded, so the optimisation and the orig shortcut work as before.
  • The specname behaviour from Handle specname hooks in subset callers #695 is unchanged, and test_subset_hook_caller_with_specname still passes.
  • The cost is one is_registered lookup per plugin passed in, only when the caller is created.

Tests

test_subset_hook_caller_excludes_plugin_registered_later covers both the single-plugin case and the mixed case where one of the plugins to remove is already registered.

I ran testing/ (excluding the benchmark) against src/pluggy from main and from this branch:

passed failed
main 226 1 (the new test)
this branch 227 0

ruff check and ruff format (0.17.3) and mypy are clean on the changed files. Changelog entry: changelog/827.bugfix.rst.

pytest-dev#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 pytest-dev#695 is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant