Skip to content

Add strict validation for ParamSpec forwarding patterns (#2802)#2802

Open
grievejia wants to merge 2 commits intofacebook:mainfrom
grievejia:export-D96510931
Open

Add strict validation for ParamSpec forwarding patterns (#2802)#2802
grievejia wants to merge 2 commits intofacebook:mainfrom
grievejia:export-D96510931

Conversation

@grievejia
Copy link
Contributor

@grievejia grievejia commented Mar 14, 2026

Summary:

Follow-up to the minimal fix: instead of permissively accepting all arguments when a ParamSpec Var resolves to a quantified ParamSpec, validate that the remaining args/kwargs actually follow the *P.args / **P.kwargs forwarding pattern.

This mirrors the existing validation in the Params::ParamSpec / Type::Quantified dispatch (callable_infer), but for the deferred case where the Var is resolved mid-matching inside callable_infer_params. The validation is extracted into a check_paramspec_forwarding method to avoid code duplication and keep var_to_rparams simple.

Fixes #823

Differential Revision: D96510931

@meta-cla meta-cla bot added the cla signed label Mar 14, 2026
@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 D96510931.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

…imal fix

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()` in `var_to_rparams` so the forwarded ParamSpec is handled instead of erroring. The next diff in this stack adds strict validation of the forwarding pattern.

Differential Revision: D96510930
@meta-codesync meta-codesync bot changed the title Add strict validation for ParamSpec forwarding patterns Add strict validation for ParamSpec forwarding patterns (#2802) Mar 20, 2026
grievejia added a commit to grievejia/pyrefly that referenced this pull request Mar 20, 2026
Summary:

Follow-up to the minimal fix: instead of permissively accepting all arguments when a ParamSpec Var resolves to a quantified ParamSpec, validate that the remaining args/kwargs actually follow the `*P.args` / `**P.kwargs` forwarding pattern.

This mirrors the existing validation in the `Params::ParamSpec` / `Type::Quantified` dispatch (callable_infer), but for the deferred case where the Var is resolved mid-matching inside `callable_infer_params`. The validation is extracted into a `check_paramspec_forwarding` method to avoid code duplication and keep `var_to_rparams` simple.

Fixes facebook#823

Differential Revision: D96510931
Summary:
Pull Request resolved: facebook#2802

Follow-up to the minimal fix: instead of permissively accepting all arguments when a ParamSpec Var resolves to a quantified ParamSpec, validate that the remaining args/kwargs actually follow the `*P.args` / `**P.kwargs` forwarding pattern.

This mirrors the existing validation in the `Params::ParamSpec` / `Type::Quantified` dispatch (callable_infer), but for the deferred case where the Var is resolved mid-matching inside `callable_infer_params`. The validation is extracted into a `check_paramspec_forwarding` method to avoid code duplication and keep `var_to_rparams` simple.

Fixes facebook#823

Differential Revision: D96510931
@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 callable_infer_params()
pwndbg ✅ Improvement -2 bad-argument-type check_paramspec_forwarding()
starlette ✅ Improvement -2 bad-argument-type callable_infer_params()
zulip ✅ Improvement -1 bad-argument-type callable_infer_params()
pandas ✅ Improvement -7 no-matching-overload callable_infer_params()
pytest-robotframework ✅ Improvement -1 bad-argument-type check_paramspec_forwarding()
trio ✅ Improvement -1 bad-argument-type callable_infer_params()
paasta ✅ Improvement -1 bad-argument-type callable_infer_params()
scrapy ✅ Improvement -3 bad-argument-type callable_infer_params()
prefect ✅ Improvement -3 bad-argument-type check_paramspec_forwarding()
Detailed analysis

✅ Improvement (10)

async-utils (-3)

Pyrefly previously rejected valid ParamSpec forwarding patterns where one generic function forwards to another using *args, **kwargs. The typing spec requires this exact pattern for ParamSpec usage. The PR correctly implements validation for this pattern, removing false positives where pyrefly was too strict.
Attribution: The changes to callable_infer_params() in pyrefly/lib/alt/callable.rs now handle the case where a ParamSpec Var resolves to a quantified ParamSpec by calling check_paramspec_forwarding() to validate the forwarding pattern instead of erroring. The var_to_rparams function now returns Err(q) for quantified ParamSpecs, triggering the forwarding validation.

pwndbg (-2)

The removed errors were false positives. Looking at the source code, _try2run_heap_command correctly uses the ParamSpec forwarding pattern *a: P.args, **kw: P.kwargs to forward arguments to the decorated function. This is the exact pattern required by the typing spec for ParamSpec forwarding. The PR fixed pyrefly's handling of this pattern by recognizing when a ParamSpec Var resolves to another quantified ParamSpec (generic-to-generic forwarding) and validating the forwarding pattern instead of rejecting it. The test cases added in the PR confirm this is the intended behavior.
Attribution: The changes to check_paramspec_forwarding() in pyrefly/lib/alt/callable.rs fixed this by adding proper validation for quantified ParamSpec forwarding patterns. When a ParamSpec Var resolves to another quantified ParamSpec (indicating forwarding between generic helpers), the new code validates that the remaining arguments follow the *P.args / **P.kwargs pattern instead of incorrectly rejecting them.

starlette (-2)

These are false positive removals (improvement). The starlette code correctly forwards ParamSpec parameters using the standard *args, **kwargs pattern. While the typing spec shows examples using *P.args, **P.kwargs in function signatures, it doesn't require that every intermediate forwarding must explicitly use those exact names when passing arguments. The pattern Middleware(middleware_class, *args, **kwargs) where args and kwargs come from P.args and P.kwargs parameters is valid ParamSpec forwarding. Pyrefly was applying an overly strict interpretation not required by the spec.
Attribution: The change to callable_infer_params() in pyrefly/lib/alt/callable.rs added strict validation via the new check_paramspec_forwarding() method. When a ParamSpec Var resolves to a quantified ParamSpec during parameter matching, it now validates that the remaining arguments exactly match *P.args and **P.kwargs. This validation was removed in the PR branch, allowing the legitimate forwarding patterns in starlette to pass.

zulip (-1)

The removed error was a false positive. The code shows a textbook ParamSpec forwarding pattern where a decorator preserves the signature of the wrapped function. ParamT is properly defined as a ParamSpec, and the *args/**kwargs are correctly forwarded to both runcall and the original function. Per the typing spec, this is exactly what ParamSpec is designed to support. The PR's improved handling of quantified ParamSpec resolution during callable inference fixed this inference failure.
Attribution: The changes to callable_infer_params() in pyrefly/lib/alt/callable.rs, specifically the new handling of quantified ParamSpec resolution in the var_to_rparams closure, fixed this false positive. The PR now properly handles the case where a ParamSpec Var resolves to a quantified ParamSpec during argument matching.

pandas (-7)

Pyrefly fixed false positives in ParamSpec handling. The PR adds proper validation for ParamSpec forwarding patterns as defined in the typing spec. Previously, pyrefly was incorrectly rejecting valid cases where one generic function forwards ParamSpec arguments to another, which is explicitly allowed by the spec. The removed errors were all pyrefly-only (not flagged by mypy/pyright), confirming these were false positives.
Attribution: The changes to callable_infer_params() in pyrefly/lib/alt/callable.rs now properly handle the case where a ParamSpec Var resolves to a quantified ParamSpec by returning Err(q) and validating the forwarding pattern, instead of incorrectly rejecting it.

pytest-robotframework (-1)

This is an improvement. The removed error was a false positive - the code correctly implements ParamSpec forwarding by passing *args, **kwargs to a method that expects *P.args, **P.kwargs. The PR's stricter validation actually fixed a bug where valid forwarding patterns were incorrectly flagged. Per the typing spec, this is the correct way to forward ParamSpec arguments between generic functions.
Attribution: The changes to check_paramspec_forwarding() in pyrefly/lib/alt/callable.rs and the improved handling of quantified ParamSpec resolution in callable_infer_params() fixed the false positive by properly recognizing valid forwarding patterns.

trio (-1)

The removed error was a false positive. The code at line 440 correctly forwards ParamSpec arguments using the *args: P.args, **kwargs: P.kwargs pattern as required by the typing spec. The wrapper function properly propagates the ParamSpec P from the outer generic function to the inner _nonblocking_helper call. Pyrefly was incorrectly expecting P to resolve to a concrete ParamSpec value rather than accepting the generic forwarding pattern. The PR's new validation logic correctly recognizes this as valid ParamSpec forwarding, removing the false positive.
Attribution: The changes to callable_infer_params() in pyrefly/lib/alt/callable.rs added stricter validation via check_paramspec_forwarding(). However, this appears to have fixed a bug where pyrefly was incorrectly rejecting valid ParamSpec forwarding patterns. The removal of this error suggests the new validation correctly recognizes the *args/**kwargs forwarding pattern.

paasta (-1)

The removed error was a false positive. The code at line 184 correctly forwards ParamSpec arguments from wrapper to run_sync using the standard *args, **kwargs pattern. Both functions use the same ParamSpec P, and the forwarding follows the typing spec's requirements for ParamSpec. The PR's changes to properly handle quantified ParamSpec forwarding in callable.rs fixed this incorrect error.
Attribution: The changes to callable_infer_params() in pyrefly/lib/alt/callable.rs, specifically the new handling when var_to_rparams returns Err(q) for quantified ParamSpecs, fixed the false positive by properly recognizing valid ParamSpec forwarding patterns instead of erroring with 'Expected P to be a ParamSpec value'

scrapy (-3)

These are false positives being removed. All three cases show valid ParamSpec forwarding patterns as defined in the typing spec. The functions correctly forward *args: P.args and **kwargs: P.kwargs to callees expecting the same ParamSpec. The PR fixed pyrefly's overly strict validation that was incorrectly rejecting these standard patterns used for generic decorators and wrappers.
Attribution: The changes to callable_infer_params() in pyrefly/lib/alt/callable.rs fixed the issue. The PR added proper handling for when a ParamSpec Var resolves to a quantified ParamSpec (the Type::Quantified(q) if q.[is_param_spec()](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/callable.rs) case), allowing valid forwarding patterns instead of treating them as errors.

prefect (-3)

These are false positive removals (improvement). The code correctly uses ParamSpec forwarding with *args and **kwargs which is the proper pattern according to the typing spec. The functions Call.new(), run_async_from_worker_thread(), and run_async_in_new_loop() all properly forward their *args: P.args, **kwargs: P.kwargs parameters to callables that expect ParamSpec arguments. Pyrefly was incorrectly flagging these valid patterns as 'Expected P to be a ParamSpec value'.
Attribution: The change to check_paramspec_forwarding() in pyrefly/lib/alt/callable.rs added validation that was too strict. The PR description indicates this was meant to add 'strict validation' but the removed errors show it was rejecting valid forwarding patterns.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (10 LLM)

Copy link
Contributor

@yangdanny97 yangdanny97 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.

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.

ParamSpec forwarding doesn't work

2 participants