Skip to content

Fix ParamSpec prefix params incorrectly demoted to positional-only#3675

Open
thatfunkymunki wants to merge 2 commits into
facebook:mainfrom
thatfunkymunki:fix-paramspec-prefix-posonly
Open

Fix ParamSpec prefix params incorrectly demoted to positional-only#3675
thatfunkymunki wants to merge 2 commits into
facebook:mainfrom
thatfunkymunki:fix-paramspec-prefix-posonly

Conversation

@thatfunkymunki
Copy link
Copy Markdown

@thatfunkymunki thatfunkymunki commented Jun 3, 2026

Summary

When I am using the pyinfra library , it fails pyrefly type check with these errors:

ERROR Unexpected keyword argument `name` in function `pyinfra.api.arguments_typed.PyinfraOperation.__call__` [unexpected-keyword]
ERROR Unexpected keyword argument `_sudo` in function `pyinfra.api.arguments_typed.PyinfraOperation.__call__` [unexpected-keyword]

and similar ones. MyPy correctly recognizes the types here successfully.

prepend_types() was converting PrefixParam::Pos to Param::PosOnly via to_param(), making keyword-passable params from function definitions positional-only after ParamSpec expansion. This caused keyword arguments like _sudo=True to be rejected as "unexpected keyword" when calling through a Protocol with explicit params before *args: P.args.

Switch to to_subset_param() which preserves the Pos vs PosOnly distinction. PrefixParam::Pos is only ever created from function definitions (not from Concatenate syntax, which always uses PosOnly), so this change is safe for all callers.

Repro file is here

https://gist.github.com/thatfunkymunki/c0c8a57d60f0f02217b918ef6a377189

observe additional unexpected kwarg errors in pyrefly vs mypy

pyrefly

PS C:\Users\munki> uvx pyrefly check repro.py 
ERROR Unexpected keyword argument `_bar` in function `FooProtocol.__call__` [unexpected-keyword]                                                                                                                                   
  --> repro.py:24:16
   |
24 | foo(biz="ccc", _bar=True)
   |                ^^^^
   |
ERROR Unexpected keyword argument `_bar` in function `FooProtocol.__call__` [unexpected-keyword]
  --> repro.py:25:26
   |
25 | foo(biz="ddd", baf=True, _bar=True, baz="abcdef")
   |                          ^^^^
   |
ERROR Unexpected keyword argument `baz` in function `FooProtocol.__call__` [unexpected-keyword]
  --> repro.py:25:37
   |
25 | foo(biz="ddd", baf=True, _bar=True, baz="abcdef")
   |                                     ^^^
   |
ERROR Unexpected keyword argument `nonexistent_param` in function `FooProtocol.__call__` [unexpected-keyword]
  --> repro.py:27:5
   |
27 | foo(nonexistent_param=True)
   |     ^^^^^^^^^^^^^^^^^
   |
 INFO 4 errors
No `pyrefly.toml` found — using preset `basic`.
Run `pyrefly init` to continue setting up Pyrefly.
Docs: https://pyrefly.org/getting-started-cli

mypy

PS C:\Users\munki> uvx mypy --strict repro.py
repro.py:27: error: Unexpected keyword argument "nonexistent_param" for "__call__" of "FooProtocol"  [call-arg]                                                                                                                    
Found 1 error in 1 file (checked 1 source file)

Test Plan

Created a failing test and confirmed that it passes with my changes. I also observed that some of the existing tests were expecting incorrect result which was also resolved with the changes.

…cast()

When a Protocol's __call__ mixes explicit parameters (_sudo, name) with
*args: P.args / **kwargs: P.kwargs, and a decorator returns
cast(Protocol[P], wrapped_func), pyrefly resolves the P-bound params
from the original function but drops the Protocol's own explicit params.

This pattern is used by pyinfra's @operation decorator, where operations
accept both operation-specific parameters (from P) and global arguments
(_sudo, _env, name, etc.) defined on the Protocol.

Mypy 1.17 handles this correctly: keyword args route to either the
Protocol's explicit params or P.kwargs based on name matching.
prepend_types() was converting PrefixParam::Pos to Param::PosOnly via
to_param(), making keyword-passable params from function definitions
positional-only after ParamSpec expansion. This caused keyword arguments
like _sudo=True to be rejected as unexpected when calling through a
Protocol with explicit params before *args: P.args.

Switch to to_subset_param() which preserves the Pos vs PosOnly
distinction. PrefixParam::Pos is only created from function definitions
(not Concatenate syntax, which always uses PosOnly), so this change is
safe for all callers.
@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented Jun 3, 2026

Hi @thatfunkymunki!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@github-actions github-actions Bot added the size/s label Jun 3, 2026
@meta-cla
Copy link
Copy Markdown

meta-cla Bot commented Jun 3, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the cla signed label Jun 3, 2026
@thatfunkymunki thatfunkymunki marked this pull request as ready for review June 3, 2026 23:26
@yangdanny97 yangdanny97 self-assigned this Jun 4, 2026
@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Jun 4, 2026

@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D107530856.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

According to mypy_primer, this change doesn't affect type check results on a corpus of open source 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.

2 participants