Skip to content

Fix ParamSpec forwarding between generic helpers (#823) - minimal fix (#2801)#2801

Open
grievejia wants to merge 1 commit intofacebook:mainfrom
grievejia:export-D96510930
Open

Fix ParamSpec forwarding between generic helpers (#823) - minimal fix (#2801)#2801
grievejia wants to merge 1 commit intofacebook:mainfrom
grievejia:export-D96510930

Conversation

@grievejia
Copy link
Contributor

@grievejia grievejia commented Mar 14, 2026

Summary:

When one generic helper forwards *args: P.args, **kwargs: P.kwargs to another generic helper that also takes Callable[P, R], the solver binds the callee's ParamSpec Var to the caller's still-quantified P. The var_to_rparams closure didn't handle Type::Quantified and fell through to the error branch, producing a bogus "Expected P to be a ParamSpec value" error.

Fix: recognize Type::Quantified(q) if q.is_param_spec() and treat it permissively (like ...), since a forwarded ParamSpec contributes no concrete parameters to expand.

Differential Revision: D96510930

@meta-codesync
Copy link

meta-codesync bot commented Mar 14, 2026

@grievejia has exported this pull request. If you are a Meta employee, you can view the originating Diff in D96510930.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

grievejia added a commit to grievejia/pyrefly that referenced this pull request Mar 20, 2026
…imal fix (facebook#2801)

Summary:

When one generic helper forwards `*args: P.args, **kwargs: P.kwargs` to another generic helper that also takes `Callable[P, R]`, the solver binds the callee's ParamSpec Var to the caller's still-quantified `P`. The `var_to_rparams` closure didn't handle `Type::Quantified` and fell through to the error branch, producing a bogus "Expected `P` to be a ParamSpec value" error.

Fix: recognize `Type::Quantified(q) if q.is_param_spec()` and treat it permissively (like `...`), since a forwarded ParamSpec contributes no concrete parameters to expand.

Differential Revision: D96510930
@meta-codesync meta-codesync bot changed the title Fix ParamSpec forwarding between generic helpers (#823) - minimal fix Fix ParamSpec forwarding between generic helpers (#823) - minimal fix (#2801) Mar 20, 2026
grievejia added a commit to grievejia/pyrefly that referenced this pull request Mar 20, 2026
…imal fix (facebook#2801)

Summary:

When one generic helper forwards `*args: P.args, **kwargs: P.kwargs` to another generic helper that also takes `Callable[P, R]`, the solver binds the callee's ParamSpec Var to the caller's still-quantified `P`. The `var_to_rparams` closure didn't handle `Type::Quantified` and fell through to the error branch, producing a bogus "Expected `P` to be a ParamSpec value" error.

Fix: recognize `Type::Quantified(q) if q.is_param_spec()` and treat it permissively (like `...`), since a forwarded ParamSpec contributes no concrete parameters to expand.

Differential Revision: D96510930
…imal fix (facebook#2801)

Summary:
Pull Request resolved: facebook#2801

When one generic helper forwards `*args: P.args, **kwargs: P.kwargs` to another generic helper that also takes `Callable[P, R]`, the solver binds the callee's ParamSpec Var to the caller's still-quantified `P`. The `var_to_rparams` closure didn't handle `Type::Quantified` and fell through to the error branch, producing a bogus "Expected `P` to be a ParamSpec value" error.

Fix: recognize `Type::Quantified(q) if q.is_param_spec()` and treat it permissively (like `...`), since a forwarded ParamSpec contributes no concrete parameters to expand.

Differential Revision: D96510930
@github-actions
Copy link

Diff from mypy_primer, showing the effect of this PR on open source code:

async-utils (https://github.com/mikeshardmind/async-utils)
- ERROR src/async_utils/bg_tasks.py:192:48-80: Expected `P` to be a ParamSpec value in function `_sem_fut` [bad-argument-type]
- ERROR src/async_utils/gen_transform.py:206:36-56: Expected `P` to be a ParamSpec value in function `_sync_to_async_gen` [bad-argument-type]
- ERROR src/async_utils/gen_transform.py:243:35-55: Expected `P` to be a ParamSpec value in function `_sync_to_async_gen` [bad-argument-type]

pwndbg (https://github.com/pwndbg/pwndbg)
- ERROR pwndbg/commands/__init__.py:948:41-61: Expected `P` to be a ParamSpec value in function `_try2run_heap_command` [bad-argument-type]
- ERROR pwndbg/commands/__init__.py:963:45-65: Expected `P` to be a ParamSpec value in function `_try2run_heap_command` [bad-argument-type]

starlette (https://github.com/encode/starlette)
- ERROR starlette/applications.py:101:50-85: Expected `P` to be a ParamSpec value in function `starlette.middleware.Middleware.__init__` [bad-argument-type]
- ERROR starlette/background.py:31:30-53: Expected `P` to be a ParamSpec value in function `BackgroundTask.__init__` [bad-argument-type]

zulip (https://github.com/zulip/zulip)
- ERROR zerver/lib/profile.py:34:30-53: Expected `ParamT` to be a ParamSpec value in function `cProfile.Profile.runcall` [bad-argument-type]

pandas (https://github.com/pandas-dev/pandas)
- ERROR pandas/core/generic.py:6134:27-73: No matching overload found for function `pandas.core.common.pipe` called with arguments: (Self@NDFrame, ((Self@NDFrame, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]
- ERROR pandas/core/groupby/groupby.py:540:24-53: No matching overload found for function `pandas.core.common.pipe` called with arguments: (Self@BaseGroupBy, ((Self@BaseGroupBy, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]
- ERROR pandas/core/resample.py:342:28-51: No matching overload found for function `pandas.core.groupby.groupby.BaseGroupBy.pipe` called with arguments: (((Self@Resampler, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]
- ERROR pandas/core/window/expanding.py:435:28-51: No matching overload found for function `pandas.core.window.rolling.RollingAndExpandingMixin.pipe` called with arguments: (((Self@Expanding, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]
- ERROR pandas/core/window/rolling.py:1634:24-53: No matching overload found for function `pandas.core.common.pipe` called with arguments: (Self@RollingAndExpandingMixin, ((Self@RollingAndExpandingMixin, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]
- ERROR pandas/core/window/rolling.py:2376:28-51: No matching overload found for function `RollingAndExpandingMixin.pipe` called with arguments: (((Self@Rolling, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]
- ERROR pandas/io/formats/style.py:4200:24-53: No matching overload found for function `pandas.core.common.pipe` called with arguments: (Self@Styler, ((Self@Styler, ParamSpec(P)) -> T) | tuple[(...) -> T, str], *tuple[Any, ...], **dict[str, Any]) [no-matching-overload]

pytest-robotframework (https://github.com/detachhead/pytest-robotframework)
- ERROR pytest_robotframework/__init__.py:303:30-68: Expected `P` to be a ParamSpec value in function `_KeywordDecorator.inner` [bad-argument-type]

trio (https://github.com/python-trio/trio)
- ERROR src/trio/_socket.py:440:46-76: Expected `P` to be a ParamSpec value in function `_SocketType._nonblocking_helper` [bad-argument-type]

paasta (https://github.com/yelp/paasta)
- ERROR paasta_tools/async_utils.py:184:24-51: Expected `P` to be a ParamSpec value in function `run_sync` [bad-argument-type]

scrapy (https://github.com/scrapy/scrapy)
- ERROR scrapy/utils/asyncio.py:222:34-57: Expected `_P` to be a ParamSpec value in function `AsyncioLoopingCall.__init__` [bad-argument-type]
- ERROR scrapy/utils/defer.py:288:60-290:6: Expected `_P` to be a ParamSpec value in function `_AsyncCooperatorAdapter.__init__` [bad-argument-type]
- ERROR scrapy/utils/defer.py:430:31-54: Expected `_P` to be a ParamSpec value in function `_maybeDeferred_coro` [bad-argument-type]

prefect (https://github.com/PrefectHQ/prefect)
- ERROR src/prefect/_internal/concurrency/api.py:32:23-46: Expected `P` to be a ParamSpec value in function `prefect._internal.concurrency.calls.Call.new` [bad-argument-type]
- ERROR src/prefect/utilities/asyncutils.py:400:44-73: Expected `P` to be a ParamSpec value in function `run_async_from_worker_thread` [bad-argument-type]
- ERROR src/prefect/utilities/asyncutils.py:404:37-66: Expected `P` to be a ParamSpec value in function `run_async_in_new_loop` [bad-argument-type]

@github-actions
Copy link

Primer Diff Classification

✅ 10 improvement(s) | 10 project(s) total | -24 errors

10 improvement(s) across async-utils, pwndbg, starlette, zulip, pandas, pytest-robotframework, trio, paasta, scrapy, prefect.

Project Verdict Changes Error Kinds Root Cause
async-utils ✅ Improvement -3 bad-argument-type is_param_spec()
pwndbg ✅ Improvement -2 bad-argument-type is_param_spec()
starlette ✅ Improvement -2 bad-argument-type var_to_rparams()
zulip ✅ Improvement -1 bad-argument-type is_param_spec()
pandas ✅ Improvement -7 no-matching-overload is_param_spec()
pytest-robotframework ✅ Improvement -1 bad-argument-type is_param_spec()
trio ✅ Improvement -1 bad-argument-type is_param_spec()
paasta ✅ Improvement -1 bad-argument-type is_param_spec()
scrapy ✅ Improvement -3 bad-argument-type is_param_spec()
prefect ✅ Improvement -3 bad-argument-type is_param_spec()
Detailed analysis

✅ Improvement (10)

async-utils (-3)

The PR fixes false positive errors where pyrefly incorrectly rejected valid ParamSpec forwarding between generic helpers. Per the typing spec, forwarding *args: P.args, **kwargs: P.kwargs from one generic function to another with the same ParamSpec constraint is valid. The removed errors were blocking legitimate code patterns used in async utilities.
Attribution: The change to var_to_rparams closure in pyrefly/lib/alt/callable.rs now recognizes Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) and treats it permissively like ..., allowing ParamSpec forwarding to work correctly.

pwndbg (-2)

Pyrefly was incorrectly rejecting valid ParamSpec forwarding patterns. The code at lines 948 and 963 shows _try2run_heap_command correctly forwarding *a: P.args, **kw: P.kwargs to a function of type Callable[P, T]. This is a standard decorator/wrapper pattern that should work. The PR fixed pyrefly's type solver to handle the case where a ParamSpec variable resolves to another quantified ParamSpec during forwarding. Removing these false positive errors is an improvement.
Attribution: The change in pyrefly/lib/alt/callable.rs added handling for Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) in the var_to_rparams closure. Previously, when a ParamSpec variable resolved to another quantified ParamSpec (forwarding scenario), the code fell through to the error branch producing the bogus 'Expected P to be a ParamSpec value' error. The fix recognizes this pattern and treats it permissively like ..., allowing the forwarded args to pass through.

starlette (-2)

These were false positives. The typing spec explicitly supports ParamSpec forwarding patterns where one generic function forwards *args: P.args, **kwargs: P.kwargs to another. In add_middleware(), the method correctly forwards its ParamSpec arguments to Middleware.__init__(). Similarly, add_task() forwards to BackgroundTask.__init__(). The PR fixed pyrefly's solver to handle these valid forwarding patterns instead of incorrectly claiming 'Expected P to be a ParamSpec value'.
Attribution: The change to var_to_rparams() in pyrefly/lib/alt/callable.rs added handling for Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs), treating forwarded ParamSpecs permissively like .... This fixed the solver's inability to handle ParamSpec variables that resolve to other quantified ParamSpecs during forwarding.

zulip (-1)

This was a false positive. The code shows a standard decorator pattern where ParamSpec correctly captures the decorated function's signature and forwards it through runcall. The PR explicitly fixes ParamSpec forwarding between generic helpers, which is exactly this pattern. Removing this error is an improvement.
Attribution: The change to handle Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) in pyrefly/lib/alt/callable.rs at line 582 fixed the false positive by recognizing that a forwarded ParamSpec should be treated permissively when resolving parameter expansion.

pandas (-7)

Pyrefly was reporting false positive no-matching-overload errors when generic functions forward ParamSpec arguments to other generic functions. The PR fixes this by recognizing quantified ParamSpecs and treating them permissively, allowing valid forwarding patterns like pandas' pipe method to type check correctly. This is an improvement - removing incorrect errors.
Attribution: The change to var_to_rparams closure in pyrefly/lib/alt/callable.rs that adds handling for Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) fixed the false positive errors by treating forwarded ParamSpecs permissively.

pytest-robotframework (-1)

This is an improvement. Pyrefly was incorrectly flagging valid ParamSpec forwarding between generic decorators. The PR fixed a bug where pyrefly couldn't handle a ParamSpec variable resolving to another quantified ParamSpec, which happens when generic helpers forward arguments. The [pyrefly-only] annotation confirms this was a false positive that other type checkers don't report.
Attribution: The change to var_to_rparams closure in pyrefly/lib/alt/callable.rs now handles Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) by treating it like ..., allowing ParamSpec forwarding to work correctly.

trio (-1)

This is an improvement. The removed error was a false positive where pyrefly incorrectly rejected valid ParamSpec forwarding between generic helper functions. The PR correctly fixes this by recognizing that when a ParamSpec variable resolves to another quantified ParamSpec (during forwarding), it should be treated permissively. This is a common pattern in async wrappers like Trio's socket implementation, and both mypy and pyright correctly accept it.
Attribution: The change to var_to_rparams closure in pyrefly/lib/alt/callable.rs added handling for Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs), treating it permissively like ... to allow forwarded ParamSpec arguments to pass through.

paasta (-1)

This is an improvement. The removed error was a false positive caused by pyrefly's inability to handle ParamSpec forwarding between generic functions. The code follows the typing spec's ParamSpec forwarding pattern correctly (https://typing.readthedocs.io/en/latest/spec/callables.html#paramspec). The PR fix allows pyrefly to recognize when a ParamSpec variable resolves to another quantified ParamSpec (as happens when to_blocking forwards to run_sync) and treats it permissively, which is the correct behavior.
Attribution: The change to var_to_rparams closure in pyrefly/lib/alt/callable.rs now handles Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) by treating it permissively like ..., allowing ParamSpec forwarding between generic helpers to work correctly.

scrapy (-3)

Pyrefly fixed false positives where it incorrectly flagged ParamSpec forwarding between generic helpers. The typing spec allows forwarding *args: P.args, **kwargs: P.kwargs between generic functions. All three removed errors show this exact pattern: one generic helper calling another with forwarded ParamSpec parameters. This is a standard pattern for decorators and wrappers that mypy/pyright handle correctly. The fix properly handles quantified ParamSpecs in the solver, eliminating these spurious errors.
Attribution: The change to handle Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) in pyrefly/lib/alt/callable.rs at line 582 fixed this issue. The solver now treats forwarded ParamSpecs permissively instead of falling through to the error branch that produced 'Expected P to be a ParamSpec value' errors.

prefect (-3)

Pyrefly was incorrectly reporting errors for valid ParamSpec forwarding between generic functions. The PR fixes a bug in pyrefly's type inference where it couldn't handle a ParamSpec variable resolving to another quantified ParamSpec. This is a common pattern in Python for creating generic wrappers and decorators. The removed errors were false positives - the code is correct and both mypy and pyright handle this pattern without errors. This is an improvement in pyrefly's type checking accuracy.
Attribution: The change in pyrefly/lib/alt/callable.rs added a new case to handle Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) in the var_to_rparams closure, treating it permissively like ... instead of falling through to the error branch. This directly fixes the false positive errors.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (10 LLM)

Copy link
Contributor

@migeed-z migeed-z left a comment

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

migeed-z added a commit to migeed-z/pyrefly that referenced this pull request Mar 21, 2026
…ssifier testing

Summary:
Mock commit to test the improved primer classifier workflow on GitHub.
This copies the code changes from PR facebook#2801 to trigger the primer and verify
classifier output formatting in the real GitHub Actions environment.

Differential Revision: D97576884
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.

2 participants