Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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
21 changes: 20 additions & 1 deletion packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15264,20 +15264,35 @@ 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;
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) {

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.

Issue · Please address or respond

Can the unnamed bare * be normalized separately from the contextual parameter index? If getParamListDetails omits that separator, lambda *, value: ... compares * with the expected value parameter, marks a mismatch, and never contextually types value. Please skip or align the separator and add this regression case.

if (index < expectedParamDetails.params.length) {
const expectedParam = expectedParamDetails.params[index];
const isPositionOnlyParam =
(positionOnlySeparatorIndex >= 0 && index < positionOnlySeparatorIndex) ||
(positionOnlySeparatorIndex < 0 &&

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.

paramsArePositionOnly &&

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]

!!param.d.name &&
isPrivateName(param.d.name.d.value));
const isCompatibleKeywordParam =

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.

expectedParam.kind !== ParamKind.Keyword ||
sawLambdaArgsParam ||

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.

Info · Optional note

isPositionOnlyParam reads paramsArePositionOnly, which is mutated later in this loop. It is correct today, but that coupling makes the matching behavior sensitive to loop ordering. Consider deriving this status up front from node.d.params, alongside positionOnlySeparatorIndex, so future refactoring cannot silently change contextual-typing behavior.

[verified]

(!isPositionOnlyParam && param.d.name?.d.value === expectedParam.param.name);

// If the parameter category matches and both of the parameters are

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.

Issue · Please address or respond

Can this continue to require matching keyword names after *args? sawLambdaArgsParam currently accepts every following keyword-only expected parameter regardless of name, but lambda *args, value: ... does not accept other=.... This can let an incompatible union candidate provide contextual types; retain the name check and add a differing-keyword-name regression.

// either separators (/ or *) or not separators, copy the type
// from the expected parameter.
if (
expectedParam.param.category === param.d.category &&
!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.

Info · Optional note

📍 packages/pyright-internal/src/analyzer/typeEvaluator.ts:15289
Prior feedback remains: positional-only classification still depends partly on paramsArePositionOnly, which is mutated during iteration. Consider precomputing each lambda parameter's positional-only status to remove this low-risk ordering dependency.

[verified]

isCompatibleKeywordParam
) {
paramType = expectedParam.type;
} else {

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

isPositionOnlyParam depends on paramsArePositionOnly, which is mutated during iteration and makes matching sensitive to loop ordering. Precompute each lambda parameter's positional-only status before contextual matching.

[verified]

Expand Down Expand Up @@ -15348,6 +15363,10 @@ export function createTypeEvaluator(
);

FunctionType.addParam(functionType, functionParam);

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

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

Pylance's parallel evaluator still contains the previous matching condition, so vendoring this change without mirroring it could produce sync/async inference differences. Ensure the downstream ingestion updates that evaluator and runs this regression in both modes.

}
});

if (paramsArePositionOnly && functionType.shared.parameters.length > 0) {
Expand Down
54 changes: 53 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, assert_type


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



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)

# This should generate an error.
position_only_keyword_callback: KeywordOnlyIntCallback = lambda value, /: value
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, 4);
});

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