Skip to content

Commit 3b0f028

Browse files
authored
Fixed a bug that results in incorrect type evaluation when passing a function with a callable parameter that uses Concatenate plus ParamSpec to a function that accepts a callable with just a ParamSpec. This addresses #9742. (#9765)
1 parent 4cae89f commit 3b0f028

File tree

3 files changed

+75
-28
lines changed

3 files changed

+75
-28
lines changed

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26629,42 +26629,60 @@ export function createTypeEvaluator(
2662926629
!effectiveSrcParamSpec ||
2663026630
!isTypeSame(effectiveSrcParamSpec, effectiveDestParamSpec, { ignoreTypeFlags: true })
2663126631
) {
26632-
const remainingFunction = FunctionType.createInstance(
26633-
'',
26634-
'',
26635-
'',
26636-
effectiveSrcType.shared.flags | FunctionTypeFlags.SynthesizedMethod,
26637-
effectiveSrcType.shared.docString
26638-
);
26639-
remainingFunction.shared.deprecatedMessage = effectiveSrcType.shared.deprecatedMessage;
26640-
remainingFunction.shared.typeVarScopeId = effectiveSrcType.shared.typeVarScopeId;
26641-
remainingFunction.priv.constructorTypeVarScopeId = effectiveSrcType.priv.constructorTypeVarScopeId;
26642-
remainingFunction.shared.methodClass = effectiveSrcType.shared.methodClass;
26643-
remainingParams.forEach((param) => {
26644-
FunctionType.addParam(remainingFunction, param);
26645-
});
26646-
if (effectiveSrcParamSpec) {
26647-
FunctionType.addParamSpecVariadics(remainingFunction, convertToInstance(effectiveSrcParamSpec));
26648-
}
26632+
const effectiveSrcPosCount = isContra ? destPositionalCount : srcPositionalCount;
26633+
const effectiveDestPosCount = isContra ? srcPositionalCount : destPositionalCount;
26634+
26635+
// If the src and dest both have ParamSpecs but the src has additional positional
26636+
// parameters that have not been matched to dest positional parameters (probably due
26637+
// to a Concatenate), don't attempt to assign the remaining parameters to the ParamSpec.
26638+
if (!effectiveSrcParamSpec || effectiveSrcPosCount >= effectiveDestPosCount) {
26639+
const remainingFunction = FunctionType.createInstance(
26640+
'',
26641+
'',
26642+
'',
26643+
effectiveSrcType.shared.flags | FunctionTypeFlags.SynthesizedMethod,
26644+
effectiveSrcType.shared.docString
26645+
);
26646+
remainingFunction.shared.deprecatedMessage = effectiveSrcType.shared.deprecatedMessage;
26647+
remainingFunction.shared.typeVarScopeId = effectiveSrcType.shared.typeVarScopeId;
26648+
remainingFunction.priv.constructorTypeVarScopeId =
26649+
effectiveSrcType.priv.constructorTypeVarScopeId;
26650+
remainingFunction.shared.methodClass = effectiveSrcType.shared.methodClass;
26651+
remainingParams.forEach((param) => {
26652+
FunctionType.addParam(remainingFunction, param);
26653+
});
26654+
if (effectiveSrcParamSpec) {
26655+
FunctionType.addParamSpecVariadics(
26656+
remainingFunction,
26657+
convertToInstance(effectiveSrcParamSpec)
26658+
);
26659+
}
2664926660

26650-
if (
26651-
!assignType(effectiveDestParamSpec, remainingFunction, /* diag */ undefined, constraints, flags)
26652-
) {
26653-
// If we couldn't assign the function to the ParamSpec, see if we can
26654-
// assign only the ParamSpec. This is possible if there were no
26655-
// remaining parameters.
2665626661
if (
26657-
remainingParams.length > 0 ||
26658-
!effectiveSrcParamSpec ||
2665926662
!assignType(
26660-
convertToInstance(effectiveDestParamSpec),
26661-
convertToInstance(effectiveSrcParamSpec),
26663+
effectiveDestParamSpec,
26664+
remainingFunction,
2666226665
/* diag */ undefined,
2666326666
constraints,
2666426667
flags
2666526668
)
2666626669
) {
26667-
canAssign = false;
26670+
// If we couldn't assign the function to the ParamSpec, see if we can
26671+
// assign only the ParamSpec. This is possible if there were no
26672+
// remaining parameters.
26673+
if (
26674+
remainingParams.length > 0 ||
26675+
!effectiveSrcParamSpec ||
26676+
!assignType(
26677+
convertToInstance(effectiveDestParamSpec),
26678+
convertToInstance(effectiveSrcParamSpec),
26679+
/* diag */ undefined,
26680+
constraints,
26681+
flags
26682+
)
26683+
) {
26684+
canAssign = false;
26685+
}
2666826686
}
2666926687
}
2667026688
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# This sample tests the case where a function with a ParamSpec
2+
# is assigned to another function with a Concatenate and a ParamSpec.
3+
4+
from typing import Any, Concatenate, Callable
5+
6+
7+
class MyGeneric[**P0]:
8+
def __call__(self, *args: P0.args, **kwargs: P0.kwargs) -> Any: ...
9+
10+
11+
def deco1[**P1](func: Callable[[Callable[P1, Any]], Any]) -> MyGeneric[P1]: ...
12+
13+
14+
@deco1
15+
def func1[**P2](func: Callable[Concatenate[int, P2], Any]): ...
16+
17+
18+
reveal_type(func1, expected_text="MyGeneric[(int, **P2@func1)]")
19+
20+
21+
v1: MyGeneric[[int]] = func1
22+
23+
# This should generate an error.
24+
v2: MyGeneric[[int, int]] = func1

packages/pyright-internal/src/tests/typeEvaluator4.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,11 @@ test('ParamSpec54', () => {
857857
TestUtils.validateResults(results, 0);
858858
});
859859

860+
test('ParamSpec55', () => {
861+
const results = TestUtils.typeAnalyzeSampleFiles(['paramSpec55.py']);
862+
TestUtils.validateResults(results, 1);
863+
});
864+
860865
test('Slice1', () => {
861866
const results = TestUtils.typeAnalyzeSampleFiles(['slice1.py']);
862867
TestUtils.validateResults(results, 0);

0 commit comments

Comments
 (0)