Skip to content

config: add plugin hookspecs at registration, not at Config.parse() - #15015

Draft
RonnyPfannschmidt wants to merge 1 commit into
pytest-dev:mainfrom
RonnyPfannschmidt:addhooks-at-registration
Draft

config: add plugin hookspecs at registration, not at Config.parse()#15015
RonnyPfannschmidt wants to merge 1 commit into
pytest-dev:mainfrom
RonnyPfannschmidt:addhooks-at-registration

Conversation

@RonnyPfannschmidt

Copy link
Copy Markdown
Member

AI-authored, posted by me. I prompted an agent (Claude Opus 5 via Claude Code) to investigate #2720; the analysis, the patch, the tests and this description are its work. I have read the change and I am putting it up — as a draft, for discussion of whether this is the right place for the call.

Closes #2720.

The bug

pytest_addhooks.call_historic(...) was issued from exactly one place: the top of Config.parse(). Plugin registration did not drive it, and historic replay only works once a call has been recorded — so on a PytestPluginManager that never belonged to a parsed Config, no registered plugin ever got pytest_addhooks called at all.

That is reachable through get_plugin_manager(), which is non-underscored and whose docstring says it "can be used by integration with other tools, like hooking into pytest to run tests into an IDE":

from _pytest.config import get_plugin_manager


class NewHooks:
    def pytest_my_hook(self):
        """spec"""


class Plugin:
    def pytest_addhooks(self, pluginmanager):
        pluginmanager.add_hookspecs(NewHooks)

    def pytest_my_hook(self):
        pass


pm = get_plugin_manager()
pm.register(Plugin())
pm.check_pending()
pluggy._manager.PluginValidationError: unknown hook 'pytest_my_hook' in plugin <__main__.Plugin object at 0x...>

The plugin adds the hookspec and implements it, so it is self-consistent; it is rejected over its own hook. This has been the behaviour since 3.1.0 (8bcf88e), and the original report in #2720 hit the same defect from the setuptools-entry-point / pytest_plugins side — see #2720 (comment) for the full archaeology.

The change

Move the historic call from Config.parse() into PytestPluginManager.__init__, so that contributing hookspecs belongs to plugin registration rather than to command line parsing.

It has to move rather than be duplicated: two call_historic calls replay the history twice and add_hookspecs() rejects the second pass.

No builtin plugin implements pytest_addhooks, so nothing in-tree changes timing. pytest_addhooks(pluginmanager) only ever receives the pluginmanager, never a Config, so firing it earlier cannot starve an implementation of anything it is entitled to.

Compatibility

Every path that already worked still works, verified individually:

path before after
normal pytest run
pytest.main(plugins=[P()]) — registered before parse()
register() from pytest_configureafter parse()
Config.fromdictargs
xdist workers (_prepareconfigparse())
get_plugin_manager() + register()
get_config() + load_setuptools_entrypoints() without parse()

testing/: 4555 passed, 48 skipped, 13 xfailed, 7 xpassed, against 4554, 48, 13, 7 on main — exactly the one added test, same pre-existing xpasses. pre-commit run -a is clean.

Notes for review

  • test_addhooks_conftestplugin loses its explicit pm.hook.pytest_addhooks.call_historic(...) line. That line was hand-simulating what Config.parse() used to do, and now double-replays the history.
  • Draft because the placement is the part worth arguing about. PytestPluginManager.__init__ makes the invariant unconditional, but it does mean pytest_addhooks fires during get_config()'s default-plugin import rather than at a point where a Config exists. If you would rather keep it bound to Config and instead make the unparsed-manager case fail loudly, say so and this can be reshaped.

🤖 Generated with Claude Code

pytest_addhooks was only ever driven from Config.parse(), so a
PytestPluginManager used outside a full pytest run - for instance one
obtained from get_plugin_manager(), which is documented for integration
with other tools - never called it at all. A plugin registered on such a
manager never got to contribute its own hookspecs, and its matching hook
implementations were then rejected by check_pending() as unknown hooks.

Move the historic call into PytestPluginManager.__init__, so that hookspec
contribution belongs to plugin registration rather than to command line
parsing. It has to move rather than be duplicated: calling it twice replays
the history twice, and add_hookspecs() rejects the second pass.

Fixes pytest-dev#2720.

Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pluginmanager validation kicks in too early when setuptools plugins use pytest_plugins

1 participant