Skip to content

Commit

Permalink
Fixed a bug that results in incorrect type evaluation when passing a …
Browse files Browse the repository at this point in the history
…function with a callable parameter that uses Concatenate plus ParamSpec to a function that accepts a callable with just a ParamSpec. This addresses #9742.
  • Loading branch information
erictraut committed Jan 26, 2025
1 parent 4cae89f commit 6a87b88
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 28 deletions.
74 changes: 46 additions & 28 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26629,42 +26629,60 @@ export function createTypeEvaluator(
!effectiveSrcParamSpec ||
!isTypeSame(effectiveSrcParamSpec, effectiveDestParamSpec, { ignoreTypeFlags: true })
) {
const remainingFunction = FunctionType.createInstance(
'',
'',
'',
effectiveSrcType.shared.flags | FunctionTypeFlags.SynthesizedMethod,
effectiveSrcType.shared.docString
);
remainingFunction.shared.deprecatedMessage = effectiveSrcType.shared.deprecatedMessage;
remainingFunction.shared.typeVarScopeId = effectiveSrcType.shared.typeVarScopeId;
remainingFunction.priv.constructorTypeVarScopeId = effectiveSrcType.priv.constructorTypeVarScopeId;
remainingFunction.shared.methodClass = effectiveSrcType.shared.methodClass;
remainingParams.forEach((param) => {
FunctionType.addParam(remainingFunction, param);
});
if (effectiveSrcParamSpec) {
FunctionType.addParamSpecVariadics(remainingFunction, convertToInstance(effectiveSrcParamSpec));
}
const effectiveSrcPosCount = isContra ? destPositionalCount : srcPositionalCount;
const effectiveDestPosCount = isContra ? srcPositionalCount : destPositionalCount;

// If the src and dest both have ParamSpecs but the src has additional positional
// parameters that have not been matched to dest positional parameters (probably due
// to a Concatenate), don't attempt to assign the remaining parameters to the ParamSpec.
if (!effectiveSrcParamSpec || effectiveSrcPosCount >= effectiveDestPosCount) {
const remainingFunction = FunctionType.createInstance(
'',
'',
'',
effectiveSrcType.shared.flags | FunctionTypeFlags.SynthesizedMethod,
effectiveSrcType.shared.docString
);
remainingFunction.shared.deprecatedMessage = effectiveSrcType.shared.deprecatedMessage;
remainingFunction.shared.typeVarScopeId = effectiveSrcType.shared.typeVarScopeId;
remainingFunction.priv.constructorTypeVarScopeId =
effectiveSrcType.priv.constructorTypeVarScopeId;
remainingFunction.shared.methodClass = effectiveSrcType.shared.methodClass;
remainingParams.forEach((param) => {
FunctionType.addParam(remainingFunction, param);
});
if (effectiveSrcParamSpec) {
FunctionType.addParamSpecVariadics(
remainingFunction,
convertToInstance(effectiveSrcParamSpec)
);
}

if (
!assignType(effectiveDestParamSpec, remainingFunction, /* diag */ undefined, constraints, flags)
) {
// If we couldn't assign the function to the ParamSpec, see if we can
// assign only the ParamSpec. This is possible if there were no
// remaining parameters.
if (
remainingParams.length > 0 ||
!effectiveSrcParamSpec ||
!assignType(
convertToInstance(effectiveDestParamSpec),
convertToInstance(effectiveSrcParamSpec),
effectiveDestParamSpec,
remainingFunction,
/* diag */ undefined,
constraints,
flags
)
) {
canAssign = false;
// If we couldn't assign the function to the ParamSpec, see if we can
// assign only the ParamSpec. This is possible if there were no
// remaining parameters.
if (
remainingParams.length > 0 ||
!effectiveSrcParamSpec ||
!assignType(
convertToInstance(effectiveDestParamSpec),
convertToInstance(effectiveSrcParamSpec),
/* diag */ undefined,
constraints,
flags
)
) {
canAssign = false;
}
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions packages/pyright-internal/src/tests/samples/paramSpec55.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This sample tests the case where a function with a ParamSpec
# is assigned to another function with a Concatenate and a ParamSpec.

from typing import Any, Concatenate, Callable


class MyGeneric[**P0]:
def __call__(self, *args: P0.args, **kwargs: P0.kwargs) -> Any: ...


def deco1[**P1](func: Callable[[Callable[P1, Any]], Any]) -> MyGeneric[P1]: ...


@deco1
def func1[**P2](func: Callable[Concatenate[int, P2], Any]): ...


reveal_type(func1, expected_text="MyGeneric[(int, **P2@func1)]")


v1: MyGeneric[[int]] = func1

# This should generate an error.
v2: MyGeneric[[int, int]] = func1
5 changes: 5 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,11 @@ test('ParamSpec54', () => {
TestUtils.validateResults(results, 0);
});

test('ParamSpec55', () => {
const results = TestUtils.typeAnalyzeSampleFiles(['paramSpec55.py']);
TestUtils.validateResults(results, 1);
});

test('Slice1', () => {
const results = TestUtils.typeAnalyzeSampleFiles(['slice1.py']);
TestUtils.validateResults(results, 0);
Expand Down

0 comments on commit 6a87b88

Please sign in to comment.