-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Fix lambda contextual typing with keyword-only callables #11609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15264,25 +15264,50 @@ export function createTypeEvaluator( | |
| // more sophisticated in the future, but it becomes very complex to handle | ||
| // all of the permutations. | ||
| let sawParamMismatch = false; | ||
| let expectedParamIndex = 0; | ||
| const positionOnlySeparatorIndex = node.d.params.findIndex( | ||
| (param) => param.d.category === ParamCategory.Simple && !param.d.name | ||
| ); | ||
|
|
||
| node.d.params.forEach((param, index) => { | ||
| let paramType: Type | undefined; | ||
|
|
||
| if (expectedParamDetails && !sawParamMismatch) { | ||
| if (index < expectedParamDetails.params.length) { | ||
| const expectedParam = expectedParamDetails.params[index]; | ||
| const isKeywordOnlySeparator = param.d.category === ParamCategory.ArgsList && !param.d.name; | ||
|
|
||
| if (isKeywordOnlySeparator) { | ||
| if ( | ||
| expectedParamIndex < expectedParamDetails.params.length && | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A bare |
||
| expectedParamDetails.params[expectedParamIndex].kind !== ParamKind.Keyword | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This guard also prevents contextual typing for a compatible single callable such as [verified] |
||
| ) { | ||
| sawParamMismatch = true; | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Once |
||
| } else if (expectedParamIndex < expectedParamDetails.params.length) { | ||
| const expectedParam = expectedParamDetails.params[expectedParamIndex]; | ||
| const isPositionOnlyParam = | ||
| (positionOnlySeparatorIndex >= 0 && index < positionOnlySeparatorIndex) || | ||
| (positionOnlySeparatorIndex < 0 && | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can this continue to require matching keyword names after |
||
| paramsArePositionOnly && | ||
| !!param.d.name && | ||
| isPrivateName(param.d.name.d.value)); | ||
| const isCompatibleKeywordParam = | ||
| expectedParam.kind !== ParamKind.Keyword || | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
📍 packages/pyright-internal/src/analyzer/typeEvaluator.ts:15289 [verified] |
||
| (!isPositionOnlyParam && param.d.name?.d.value === expectedParam.param.name); | ||
|
|
||
| // If the parameter category matches and both of the parameters are | ||
| // either separators (/ or *) or not separators, copy the type | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[verified] |
||
| // from the expected parameter. | ||
| if ( | ||
| expectedParam.param.category === param.d.category && | ||
| !param.d.name === !expectedParam.param.name | ||
| !param.d.name === !expectedParam.param.name && | ||
| isCompatibleKeywordParam | ||
| ) { | ||
| paramType = expectedParam.type; | ||
| } else { | ||
| sawParamMismatch = true; | ||
| } | ||
|
|
||
| expectedParamIndex++; | ||
| } else if (param.d.defaultValue) { | ||
| // If the lambda param has a default value but there is no associated | ||
| // parameter in the expected type, assume that the default value is | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| # This sample tests the case where a lambda is assigned to | ||
| # a union type that contains multiple callables. | ||
|
|
||
| from typing import Callable, Protocol, TypeVar | ||
| from typing import Callable, Generic, Protocol, Self, TypeVar, assert_type | ||
|
|
||
|
|
||
| U1 = Callable[[int, str], bool] | Callable[[str], bool] | ||
|
|
@@ -76,3 +76,63 @@ def accepts_u2(cb: U2) -> U2: | |
| def accepts_u3(u: U3): | ||
| # This should generate an error. | ||
| u(lambda v: v.lower()) | ||
|
|
||
|
|
||
| class KeywordOnlyCallable: | ||
| def __call__(self, *, kwarg: int) -> Self: ... | ||
|
|
||
|
|
||
| keyword_only_union: Callable[[KeywordOnlyCallable], KeywordOnlyCallable] | KeywordOnlyCallable = lambda x: x | ||
|
|
||
|
|
||
| class GenericKeywordOnlyCallable(Generic[T]): | ||
| def __call__(self, *, kwarg: T) -> Self: ... | ||
|
|
||
|
|
||
| generic_keyword_only_union: ( | ||
| Callable[[GenericKeywordOnlyCallable[int]], GenericKeywordOnlyCallable[int]] | GenericKeywordOnlyCallable[int] | ||
| ) = lambda x: x | ||
|
|
||
|
|
||
| class KeywordOnlyCallback(Protocol): | ||
| def __call__(self, *, value: int) -> Self: ... | ||
|
|
||
|
|
||
| protocol_keyword_only_union: Callable[[KeywordOnlyCallback], KeywordOnlyCallback] | KeywordOnlyCallback = lambda x: x | ||
|
|
||
| ordinary_callable_union: Callable[[int], int] | Callable[[str], str] = lambda x: x | ||
|
|
||
|
|
||
| class PositionalCallable: | ||
| def __call__(self, value: int) -> Self: ... | ||
|
|
||
|
|
||
| positional_callable_union: Callable[[PositionalCallable], PositionalCallable] | PositionalCallable = lambda x: x | ||
|
|
||
| # This should generate an error. | ||
| keyword_only_callback: KeywordOnlyCallback = lambda x: x | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Add a positive regression case for the enabled branch: a lambda with [verified] |
||
|
|
||
|
|
||
| class KeywordOnlyIntCallback(Protocol): | ||
| def __call__(self, *, value: int) -> int: ... | ||
|
|
||
|
|
||
| same_name_keyword_only_callback: KeywordOnlyIntCallback = lambda value: assert_type(value, int) | ||
|
|
||
|
|
||
| class VariadicKeywordOnlyCallback(Protocol): | ||
| def __call__(self, *args: object, value: int) -> int: ... | ||
|
|
||
|
|
||
| variadic_keyword_only_callback: VariadicKeywordOnlyCallback = lambda *args, value: assert_type(value, int) | ||
|
|
||
| # The bare `*` separator should not consume the contextual parameter index. | ||
| bare_keyword_only_callback: KeywordOnlyIntCallback = lambda *, value: assert_type(value, int) | ||
|
|
||
| # This should generate an error because the keyword-only parameter name differs. | ||
| variadic_keyword_only_callback_different_name: VariadicKeywordOnlyCallback = lambda *args, other: reveal_type( | ||
| other, expected_text="Unknown" | ||
| ) | ||
|
|
||
| # This should generate an error. | ||
| position_only_keyword_callback: KeywordOnlyIntCallback = lambda value, /: value | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the unnamed bare
*be normalized separately from the contextual parameter index? IfgetParamListDetailsomits that separator,lambda *, value: ...compares*with the expectedvalueparameter, marks a mismatch, and never contextually typesvalue. Please skip or align the separator and add this regression case.