Skip to content

Change precedence of hook call value, hookspec default, hookimpl default - #738

Merged
RonnyPfannschmidt merged 5 commits into
pytest-dev:mainfrom
bluetech:default-precedence
Sep 25, 2026
Merged

RonnyPfannschmidt merged 5 commits into
pytest-dev:mainfrom
bluetech:default-precedence

Conversation

@bluetech

Copy link
Copy Markdown
Member

Fix #442. Specifically, this implements the change discussed in #442 (comment).

The first commit just tweaks a benchmark's rounds to be more stable.
Second commit is a doc tweak I forgot in #732.
The third commit is the main change.

Performance is a bit slower, but mostly only if the kwargs feature is used, otherwise it's mostly an extra if. I think it's acceptable.

@RonnyPfannschmidt RonnyPfannschmidt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Claude Opus 5 via Claude Code for the pluggy maintainers; I prompted it, it did the work, I read it.

The agent ran the suite on 9de4800 (183 passed) and probed the call paths the new kwargnames delivery touches. Two things worth raising here: one case where the rule stated in the new docs section does not hold, and one behaviour change for existing plugins that has no test. Both inline.

Comment thread docs/index.rst
Comment on lines +521 to +522
If the hook caller passes a value for the parameter, it is used. Otherwise the
default declared by the hookimpl is used.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If the hook caller passes a value for the parameter, it is used" does not hold when the hookimpl declares the parameter keyword-only, because varnames() drops keyword-only parameters, so they never land in kwargnames:

class Api:
    @hookspec
    def hello(self, arg, new_arg="spec"): ...

class Plugin:
    @hookimpl
    def hello(self, arg, *, new_arg="impl-default"):
        return new_arg

pm.hook.hello(arg=1, new_arg="call")  # -> ['impl-default']

The mirror case on the spec side is already true on main after #732: a hookspec declaring the default keyword-only (def hello(self, arg, *, new_arg="spec")) never gets that default applied either, since HookSpec.kwargdefaults is built from kwargnames.

Before this PR the impl side was at least consistent -- no impl kwarg ever received a call value. Now positional-or-keyword ones do and keyword-only ones silently do not. Either support them or say so in this section, with a test pinning whichever you pick.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyword-only parameters is specifically not handled by pluggy currently. I think since we already took the overhead of passing kwargs there's actually not much reason to ignore kwonly anymore... In any case I think we can defer this to a separate issue.

assert pm.hook.hello(arg=1) == ["spec-default"]
assert pm.hook.hello(arg=1, new_arg="call") == ["call"]

def test_new_call_argument_is_not_delivered_by_old_spec(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers the case where the impl does not declare the new argument. The sibling case -- impl declares it with a default, hookspec does not declare it at all, caller passes it anyway -- is the actual behaviour change for existing plugins, and it is untested:

class Api:
    @hookspec
    def hello(self, arg): ...

class Plugin:
    @hookimpl
    def hello(self, arg, extra="impl-default"):
        return extra

pm.hook.hello(arg=1, extra=2)  # was ['impl-default'], now [2]

_verify_hook only validates argnames against the spec, so this impl registers fine and a caller-supplied extra now reaches it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I was mostly thinking about old spec, new impl, old call. I didn't think about old spec, new impl, new call. That's less likely, but possible.

I think it's a bit strange to have the call & impl "communicate" the arg without the spec being aware of it, but it can happen and I think the most useful thing to do in this case is to pass the call arg, instead of the hookimpl default or a validation error, since that's what the old spec, new impl, new call scenario would want for compat.

@bluetech

Copy link
Copy Markdown
Member Author

Updated for @RonnyPfannschmidt's review.

Main split _hooks.py and _callers.py into role-specific modules (pytest-dev#703).
The kwargs passing moves to _execution.py and the HookImpl.kwargnames
docs to _impl.py; _hooks.py and _callers.py stay main's re-export shims.

Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
@RonnyPfannschmidt
RonnyPfannschmidt merged commit 54fb4dd into pytest-dev:main Sep 25, 2026
20 checks passed
@bluetech
bluetech deleted the default-precedence branch September 30, 2026 07:29
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.

Question about default values in hook implementation

2 participants