Skip to content

Deliver defaulted positional-only hook arguments positionally - #775

Open
adenzhou1350 wants to merge 2 commits into
pytest-dev:mainfrom
adenzhou1350:codex/fix-posonly-hook-defaults
Open

adenzhou1350 wants to merge 2 commits into
pytest-dev:mainfrom
adenzhou1350:codex/fix-posonly-hook-defaults

Conversation

@adenzhou1350

@adenzhou1350 adenzhou1350 commented Oct 11, 2026 •

Copy link
Copy Markdown

Fixes #774.

Hookimpl arguments with defaults are forwarded as keywords, including positional-only arguments. An explicit call value therefore raises TypeError in an otherwise valid hook or wrapper.

Remember optional positional-only names/defaults during registration and deliver them positionally. Fill gaps before a supplied positional-only value, but leave trailing omitted defaults implicit. Keep required-argument validation and hookspec/implementation default precedence unchanged; no keyword-only support change.

Regression coverage includes plain hooks, both wrapper styles, bound methods, callable/decorated call_extra implementations, falsy values, mixed signatures and default gaps.

Validation:

  • Unmodified main: 219 existing tests pass on Windows/Linux; the expanded 27-case matrix has 16 failures and 11 passing controls on each.
  • After the fix: full suite 246 passed on Windows CPython 3.12/3.14 and Linux CPython 3.12.
  • Complete repository pre-commit hooks, including mypy, pass on Linux.

Environment note: the committed uv lock selects pytest 3.2.5, which cannot start on Python 3.12. Linux tests used a task-private editable install and pytest 9.1.1 via uv run --no-sync --no-default-groups python -m pytest; Windows used the native interpreter and isolated package targets because local venv executables are blocked. No supported-Python matrix or performance claim.

AI-assisted with OpenAI Codex; the implementation and tests were independently checked against the unmodified source.

Coverage follow-up: add native class-constructor call_extra cases and express the required default prefix without an unreachable loop-exhaustion branch. Both changed production modules now have 100% statement/branch coverage locally; no coverage threshold or exclusion changed. Upstream matrix/package/benchmark checks passed on the initial revision; reruns for the updated head are pending.

Fixes pytest-dev#774

Assisted-by: OpenAI Codex
Signed-off-by: Xucheng Zhou <aden1350@outlook.com>
Assisted-by: OpenAI Codex
Signed-off-by: Xucheng Zhou <aden1350@outlook.com>

@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.5 via Claude Code for the pluggy maintainers; I prompted it, it did the work, I read it.

Approving. I measured the runtime cost against main before approving. The setup was CPython 3.13, a micro-benchmark, 2 rounds, and the best of 15 runs for each case:

Case main this PR Δ
Hook call, 10 impls, no defaulted args ~4.3 µs ~4.35 µs noise
Hook call, 10 impls with b=None, c=None ~7.2 µs ~7.9 µs +8–12% (~60–80 ns per impl)
Hook call, 10 impls with positional-only defaults TypeError ~15.2 µs new path, ~1.2 µs per impl
HookImpl() construction ~2.55 µs ~4.35 µs +70% (~1.8 µs per impl)
  • Impls without defaulted args are unaffected, because all the new code is inside if hook_impl.kwargnames:.
  • Impls with defaulted args now pay on every call for the _posonlydefaults truthiness check. They also pay for a not in _posonlydefaults lookup for each kwarg inside the kwargs comprehension.
  • The registration cost is one-time and negligible in absolute terms.

Follow-up (not blocking, will be folded into a refactoring PR stack): move the split to HookImpl.__init__. That means precomputing a kwargnames tuple without the positional-only names for the kwargs comprehension, plus a precomputed (name, default) sequence for the positional-only fill. The call path then only touches the positional-only branch when it is non-empty. With that, impls with ordinary defaults should cost the same as on main, and only positional-only impls pay extra.


Generated by Claude Code

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.

Defaulted positional-only hook parameters are forwarded as keywords

2 participants