Change precedence of hook call value, hookspec default, hookimpl default - #738
Conversation
6555949 to
9de4800
Compare
RonnyPfannschmidt
left a comment
There was a problem hiding this comment.
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.
| If the hook caller passes a value for the parameter, it is used. Otherwise the | ||
| default declared by the hookimpl is used. |
There was a problem hiding this comment.
"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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
9de4800 to
51ec448
Compare
|
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>
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.