Repository navigation
Change precedence of hook call value, hookspec default, hookimpl default #738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09868be
8df74a1
59bab5b
51ec448
f2c8133
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| With the addition of default values for hookspec arguments, we also changed the precedence of default values for *hookimpl* arguments. | ||
| Previously, if a hookimpl function declared a default for an argument, it was always used, even if the *hook call* passed an explicit value. | ||
| Now the precedence is changed to: hook call passed value > hookspec default value > hookimpl default value. | ||
| See :ref:`hookimpl_arg_defaults`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -446,15 +446,72 @@ def hello(self, arg, new_arg="impl-default"): | |
| pm.register(PluginWithoutImplDefault()) | ||
| pm.register(PluginWithImplDefault()) | ||
|
|
||
| # Old spec, new impl, old call. | ||
| # The normal case for impl default. | ||
| assert pm.hook.hello(arg=1) == [(1, "impl-default")] | ||
| # Old spec, new impl, new call. | ||
| # Less common, but the call arg is still passed. | ||
| assert pm.hook.hello(arg=1, new_arg="call") == [(1, "call")] | ||
|
|
||
| def test_does_not_override_hookimpl_default(self, pm: PluginManager) -> None: | ||
| """If an impl provides its own default, it takes precedence over both | ||
| the spec default and call value. | ||
| def test_call_value_overrides_hookimpl_default(self, pm: PluginManager) -> None: | ||
| """A call value takes precedence over an hookimpl default.""" | ||
|
|
||
| NOTE: This is verifying existing behavior, but it's not necessarily what | ||
| we want (#442). | ||
| """ | ||
| class Api: | ||
| @hookspec | ||
| def hello(self, arg, new_arg): | ||
| pass | ||
|
|
||
| class Plugin: | ||
| @hookimpl | ||
| def hello(self, arg, new_arg="impl-default"): | ||
| return new_arg | ||
|
|
||
| pm.add_hookspecs(Api) | ||
| pm.register(Plugin()) | ||
|
|
||
| assert pm.hook.hello(arg=1, new_arg="call") == ["call"] | ||
|
|
||
| def test_call_value_overrides_hookimpl_default_in_wrappers( | ||
| self, pm: PluginManager | ||
| ) -> None: | ||
| class Api: | ||
| @hookspec | ||
| def hello(self, arg, new_arg): | ||
| pass | ||
|
|
||
| seen = [] | ||
|
|
||
| class Plugin: | ||
| @hookimpl | ||
| def hello(self, arg, new_arg="impl-default"): | ||
| seen.append(new_arg) | ||
| return new_arg | ||
|
|
||
| class WrapperPlugin: | ||
| @hookimpl(wrapper=True) | ||
| def hello(self, arg, new_arg="impl-default"): | ||
| seen.append(new_arg) | ||
| result = yield | ||
| return result | ||
|
|
||
| class HookwrapperPlugin: | ||
| @hookimpl(hookwrapper=True) | ||
| def hello(self, arg, new_arg="impl-default"): | ||
| seen.append(new_arg) | ||
| yield | ||
|
|
||
| pm.add_hookspecs(Api) | ||
| pm.register(Plugin()) | ||
| pm.register(WrapperPlugin()) | ||
| pm.register(HookwrapperPlugin()) | ||
|
|
||
| assert pm.hook.hello(arg=1, new_arg="call") == ["call"] | ||
| assert seen == ["call", "call", "call"] | ||
|
|
||
| def test_hookspec_default_and_call_override_hookimpl_default( | ||
| self, pm: PluginManager | ||
| ) -> None: | ||
| """Hookspec defaults take precedence over hookimpl defaults.""" | ||
|
|
||
| class Api: | ||
| @hookspec | ||
|
|
@@ -469,8 +526,8 @@ def hello(self, arg, new_arg="impl-default"): | |
| pm.add_hookspecs(Api) | ||
| pm.register(Plugin()) | ||
|
|
||
| assert pm.hook.hello(arg=1) == ["impl-default"] | ||
| assert pm.hook.hello(arg=1, new_arg="call") == ["impl-default"] | ||
| 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( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| self, pm: PluginManager | ||
|
|
||
There was a problem hiding this comment.
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 inkwargnames: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, sinceHookSpec.kwargdefaultsis built fromkwargnames.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.
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.