Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15264,6 +15264,7 @@ export function createTypeEvaluator(
// more sophisticated in the future, but it becomes very complex to handle
// all of the permutations.
let sawParamMismatch = false;
let sawLambdaArgsParam = false;

node.d.params.forEach((param, index) => {
let paramType: Type | undefined;
Expand All @@ -15277,7 +15278,8 @@ export function createTypeEvaluator(
// from the expected parameter.
if (
expectedParam.param.category === param.d.category &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

A bare * appears to consume an index even though contextual parameter details omit separators, potentially leaving lambda *, value: ... without the contextual type for value. Add that protocol scenario with assert_type(value, int) and, if confirmed, track lambda and contextual indexes separately.

!param.d.name === !expectedParam.param.name
!param.d.name === !expectedParam.param.name &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

This guard also prevents contextual typing for a compatible single callable such as Callable[*, value: int] assigned from lambda value: value: that lambda accepts the required value= keyword, but its parameter now falls back to Unknown. Please preserve contextual typing when the lambda parameter name matches the keyword-only expected parameter, or add coverage showing why this case must be rejected.

[verified]

(expectedParam.kind !== ParamKind.Keyword || sawLambdaArgsParam)
) {
paramType = expectedParam.type;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

Once sawLambdaArgsParam is true, any keyword-only contextual parameter is accepted regardless of its name, so lambda *args, other: ... may inherit the type of contextual value. Add a callable-union regression using assert_type(other, ...); require matching names after *args if the candidate can otherwise influence inference.

} else {
Expand Down Expand Up @@ -15348,6 +15350,10 @@ export function createTypeEvaluator(
);

FunctionType.addParam(functionType, functionParam);

if (param.d.category === ParamCategory.ArgsList) {
sawLambdaArgsParam = true;
}
});

if (paramsArePositionOnly && functionType.shared.parameters.length > 0) {
Expand Down
37 changes: 36 additions & 1 deletion packages/pyright-internal/src/tests/samples/lambda4.py
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


U1 = Callable[[int, str], bool] | Callable[[str], bool]
Expand Down Expand Up @@ -76,3 +76,38 @@ 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Warning · Non-blocking recommendation

Add a positive regression case for the enabled branch: a lambda with *args followed by a keyword-only parameter contextually typed against a keyword-only callable. The new guard's rejection behavior is well covered, but this ensures valid keyword-only lambda parameters still receive their expected type.

[verified]

2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/typeEvaluator1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ test('Lambda3', () => {
test('Lambda4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['lambda4.py']);

TestUtils.validateResults(analysisResults, 2);
TestUtils.validateResults(analysisResults, 3);
});

test('Lambda5', () => {
Expand Down