Skip to content

Fix callable-keyword collisions in capture and acapture - #50

Draft
AryanRG wants to merge 2 commits into
python-trio:mainfrom
AryanRG:fix/callable-keyword-forwarding
Draft

AryanRG wants to merge 2 commits into
python-trio:mainfrom
AryanRG:fix/callable-keyword-forwarding

Conversation

@AryanRG

@AryanRG AryanRG commented Oct 16, 2026 •

Copy link
Copy Markdown

Passing a callable positionally to capture or acapture currently prevents forwarding a target keyword named sync_fn or async_fn: Python raises a duplicate-binding TypeError before the target runs. This change lets the positional callable receive every keyword, while retaining the legacy callable-by-keyword form when there are no positional arguments.

def f(*, sync_fn):
    return sync_fn

async def af(*, async_fn):
    return async_fn

capture(f, sync_fn=42)                 # Value(42)
await acapture(af, async_fn=42)        # Value(42)

def legacy(*, value):
    return value

capture(sync_fn=legacy, value=42)     # Value(42), legacy form

The dispatcher selects the positional callable first; otherwise it consumes the original callable keyword. Paired ParamSpec overloads preserve static argument checking and result/NoReturn inference. Regression tests cover both conventions, return and exception identity, async suspension, and traceback removal. The tutorial explains the dispatch rule. The release note is newsfragments/50.bugfix.rst, added in a separate follow-up commit after obtaining this PR number.

Compatibility is scoped to previously valid calls, not identical behavior for every observation:

  • inspect.signature and runtime annotations now expose (*args: Any, **kwargs: Any) -> Value[Any] | Error instead of a named required callable and ParamSpec. Reflective binding, autospec, and injection consumers may behave differently; no downstream reflective integration was tested. Static overloads retain precise typing.
  • A malformed acapture() call now returns a coroutine and raises the missing-callable TypeError when awaited, instead of raising during coroutine construction. That error includes an acapture frame, and malformed-call diagnostic precedence can differ. acapture remains an async function, and valid target execution remains deferred until await.
  • With no positional callable, sync_fn/async_fn selects the legacy callable and cannot also be forwarded under that name; use a positional callable for the collision case. Positional-callable-plus-colliding-keyword forms were previously invalid and now gain the forwarding meaning.

Local validation on macOS arm64, against base 03ed6218b08001877745bb1a9e180c8c5cf7c903:

  • CPython 3.8.20 (minimum attrs 19.2.0), 3.10.22, and 3.13.12: 24 tests passed on each, with 100% statement/branch coverage and source selected explicitly.
  • Strict Mypy 1.14.1 (Python 3.8 target), an additional ParamSpec/NoReturn probe, YAPF 0.20.1, isort, Pyright type completeness (100%), and git diff --check passed. Installed package bytes match source for the completeness check.
  • Local red/green validation reproduced four runtime collision failures and four Mypy errors on the exact base with the final regression tests; the patch passes. The capture/traceback-removal blocks are unchanged.
  • Broader Pyright analysis and strict Sphinx are not green: validation found the same eight existing Pyright diagnostics and two existing Sphinx configuration warnings on base and patch, with no added diagnostic. These were not repaired in this change.

AI assistance was used to prepare implementation and regression tests. Hosted CI results are separate from this local evidence. Linux, Windows, PyPy, other Python versions, reflective integrations, and performance benchmarks were not run locally.

@AryanRG
AryanRG marked this pull request as ready for review October 16, 2026 23:47
@AryanRG
AryanRG marked this pull request as draft October 16, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant