diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 68b3343e6fae..7be775ecdc4d 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -28393,7 +28393,14 @@ export function createTypeEvaluator( return; } - const destParamInfo = destParamMap.get(srcParamInfo.param.name); + const destParamInfo = + destParamMap.get(srcParamInfo.param.name) ?? + destParamDetails.params.find( + (paramInfo) => + paramInfo.param.name === srcParamInfo.param.name && + paramInfo.kind === ParamKind.Standard && + paramInfo.param.category === ParamCategory.Simple + ); const paramDiag = diag?.createAddendum(); const srcParamType = srcParamInfo.type; @@ -28523,6 +28530,43 @@ export function createTypeEvaluator( } }); + // Positional-or-keyword dest parameters can also be passed by name. + // If the source has no matching named parameter, the keyword form must + // be compatible with the source **kwargs (when present). + destParamDetails.params.forEach((destParamInfo) => { + if ( + destParamInfo.kind !== ParamKind.Standard || + !destParamInfo.param.name || + destParamInfo.param.category !== ParamCategory.Simple + ) { + return; + } + + const srcHasNamed = srcParamDetails.params.some( + (srcParamInfo) => + srcParamInfo.param.name === destParamInfo.param.name && + srcParamInfo.param.category === ParamCategory.Simple && + srcParamInfo.kind !== ParamKind.Positional + ); + if (srcHasNamed || srcParamDetails.kwargsIndex === undefined) { + return; + } + + if ( + !assignParam( + destParamInfo.type, + srcParamDetails.params[srcParamDetails.kwargsIndex].type, + destParamInfo.index, + diag?.createAddendum(), + constraints, + flags, + recursionCount + ) + ) { + canAssign = false; + } + }); + // If both src and dest have a "**kwargs" parameter, make sure their types are compatible. if (srcParamDetails.kwargsIndex !== undefined && destParamDetails.kwargsIndex !== undefined) { if ( diff --git a/packages/pyright-internal/src/tests/samples/callbackProtocol12.py b/packages/pyright-internal/src/tests/samples/callbackProtocol12.py new file mode 100644 index 000000000000..72ac5b3fe97f --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/callbackProtocol12.py @@ -0,0 +1,37 @@ +# This sample tests that a callable with *args/**kwargs is not assignable +# to a callable whose positional-or-keyword parameter cannot be passed +# by keyword to the source (typing spec callable assignability). + +from typing import Protocol + + +class AcceptsArgsKwargs(Protocol): + def __call__(self, *args: int, **kwargs: bool) -> None: ... + + +class AcceptsKeywordOrPositional(Protocol): + def __call__(self, a: int) -> None: ... + + +def func1(cb: AcceptsArgsKwargs): + # This should generate an error because AcceptsKeywordOrPositional + # can be called as cb(a=10), which is not valid for **kwargs: bool. + x: AcceptsKeywordOrPositional = cb + + +class AcceptsKeywordOnly(Protocol): + def __call__(self, *args: int, a: bool = False) -> None: ... + + +def func2(cb: AcceptsKeywordOnly): + # This should generate an error because the keyword form of + # parameter "a" has type int on the dest and bool on the source. + y: AcceptsKeywordOrPositional = cb + + +def ok_cb(a: int) -> None: + pass + + +def func3(cb: AcceptsKeywordOrPositional): + z: AcceptsKeywordOrPositional = ok_cb diff --git a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts index b6db2402a2f3..f5f5ed0fd35a 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts @@ -79,6 +79,12 @@ test('CallbackProtocol11', () => { TestUtils.validateResults(analysisResults, 0); }); +test('CallbackProtocol12', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['callbackProtocol12.py']); + + TestUtils.validateResults(analysisResults, 2); +}); + test('Assignment1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['assignment1.py']);