Modified overload matching algorithm to better match that of mypy. In particular, if overload matching is ambiguous due to an Any or Unknown anywhere within an argument type (even within type arguments) and the resulting filtered overloads return the same generic type but with different type arguments, the resulting type was previously the return type of the first filtered overload. It is now the common generic type with erased type arguments. This addresses https://github.com/microsoft/pyright/issues/5216.

This commit is contained in:
Eric Traut 2023-06-03 17:36:09 -06:00
parent 771b7e66bb
commit 746e03a49d
4 changed files with 151 additions and 14 deletions

View File

@ -307,7 +307,7 @@ Some functions or methods can return one of several different types. In cases wh
4. If more than one overload remains, the “winner” is chosen based on the order in which the overloads are declared. In general, the first remaining overload is the “winner”. There are two exceptions to this rule.
Exception 1: When an `*args` (unpacked) argument matches a `*args` parameter in one of the overload signatures, this overrides the normal order-based rule.
Exception 2: When two or more overloads match because an argument evaluates to `Any` or `Unknown`, the matching overload is ambiguous. In this case, pyright examines the return types of the remaining overloads and eliminates types that are duplicates or are subsumed by (i.e. proper subtypes of) other types in the list. If only one type remains after this coalescing step, that type is used. If more than one type remains after this coalescing step, the type of the call expression evaluates to `Unknown`. For example, if two overloads are matched due to an argument that evaluates to `Any`, and those two overloads have return types of `str` and `LiteralString`, pyright will coalesce this to just `str` because `LiteralString` is a proper subtype of `str`. If the two overloads have return types of `str` and `bytes`, the call expression will evaluate to `Unknown` because `str` and `bytes` have no overlap.
Exception 2: When two or more overloads match because an argument or a type argument evaluates to `Any` or `Unknown` and this causes overload matching to be ambiguous. In this case, pyright examines the return types of the remaining overloads and eliminates types that are duplicates or are subsumed by (i.e. proper subtypes of) other types in the list. If only one type remains after this coalescing step, that type is used. If more than one type remains after this coalescing step and all of the types are the same class but with different type arguments, these type arguments are “erased” (i.e. replaced with `Unknown`). Otherwise, the type of the call expression evaluates to `Unknown`. For example, if two overloads are matched due to an argument that evaluates to `Any`, and those two overloads have return types of `str` and `LiteralString`, pyright will coalesce this to just `str` because `LiteralString` is a proper subtype of `str`. If the two overloads have return types of `list[str]` and `list[int]`, the call expression will evaluate to `list[Unknown]`. If the two overloads have return types of `str` and `bytes`, the call expression will evaluate to `Unknown` because `str` and `bytes` have no overlap.
5. If no overloads remain, Pyright considers whether any of the arguments are union types. If so, these union types are expanded into their constituent subtypes, and the entire process of overload matching is repeated with the expanded argument types. If two or more overloads match, the union of their respective return types form the final return type for the call expression.

View File

@ -206,6 +206,7 @@ import {
doForEachSubtype,
ensureFunctionSignaturesAreUnique,
explodeGenericClass,
getCommonErasedType,
getContainerDepth,
getDeclaredGeneratorReturnType,
getGeneratorTypeArgs,
@ -8256,10 +8257,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
if (dedupedResultsIncludeAny) {
effectiveReturnType = AnyType.create();
} else {
effectiveReturnType = UnknownType.createPossibleType(
combinedTypes,
possibleMatchInvolvesIncompleteUnknown
);
// If all of the return types are the same generic class,
// replace the type arguments with Unknown. Otherwise return
// an Unknown type that has associated "possible types" to aid
// with completion suggestions.
effectiveReturnType =
getCommonErasedType(dedupedMatchResults) ??
UnknownType.createPossibleType(combinedTypes, possibleMatchInvolvesIncompleteUnknown);
}
}
@ -8314,6 +8318,17 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return matches;
}
// If the relevance of some matches differs, filter out the ones that
// are lower relevance. This favors *args parameters in cases where
// a *args argument is used.
if (matches[0].matchResults.relevance !== matches[matches.length - 1].matchResults.relevance) {
matches = matches.filter((m) => m.matchResults.relevance === matches[0].matchResults.relevance);
if (matches.length < 2) {
return matches;
}
}
// If all of the return types match, select the first one.
if (
areTypesSame(
@ -8330,9 +8345,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
}
for (let i = 0; i < firstArgResults.length; i++) {
// If the arg is Any or Unknown, see if the corresponding
// If the arg contains Any or Unknown, see if the corresponding
// parameter types differ in any way.
if (isAnyOrUnknown(firstArgResults[i].argType)) {
const anyOrUnknownInArg = containsAnyOrUnknown(firstArgResults[i].argType, /* recurse */ true);
if (anyOrUnknownInArg) {
const paramTypes = matches.map((match) =>
i < match.matchResults.argParams.length
? match.matchResults.argParams[i].paramType
@ -10409,7 +10426,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
let isTypeIncomplete = matchResults.isTypeIncomplete;
let argumentErrors = false;
let specializedInitSelfType: Type | undefined;
let anyOrUnknownArgument: UnknownType | AnyType | undefined;
let accumulatedAnyOrUnknownArg: UnknownType | AnyType | undefined;
const typeCondition = getTypeCondition(type);
if (type.boundTypeVarScopeId) {
@ -10564,10 +10581,14 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
condition = TypeCondition.combine(condition, argResult.condition) ?? [];
}
if (isAnyOrUnknown(argResult.argType)) {
anyOrUnknownArgument = anyOrUnknownArgument
? preserveUnknown(argResult.argType, anyOrUnknownArgument)
: argResult.argType;
// Determine if the argument type contains an Any or Unknown. Accumulate
// these across all arguments.
const anyOrUnknownInArg = containsAnyOrUnknown(argResult.argType, /* recurs */ true);
if (anyOrUnknownInArg) {
accumulatedAnyOrUnknownArg = accumulatedAnyOrUnknownArg
? preserveUnknown(anyOrUnknownInArg, accumulatedAnyOrUnknownArg)
: anyOrUnknownInArg;
}
if (type.details.paramSpec) {
@ -10722,7 +10743,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return {
argumentErrors,
argResults,
anyOrUnknownArgument,
anyOrUnknownArgument: accumulatedAnyOrUnknownArg,
returnType: specializedReturnType,
isTypeIncomplete,
activeParam: matchResults.activeParam,

View File

@ -2283,6 +2283,40 @@ export function specializeTupleClass(
return clonedClassType;
}
// If the array contains two or more class types that are all the same
// generic type, return the common "erased" type — a type that
// replaces all of the type arguments with Unknown.
export function getCommonErasedType(types: Type[]): ClassType | undefined {
if (types.length < 2) {
return undefined;
}
if (!isClass(types[0])) {
return undefined;
}
for (let i = 1; i < types.length; i++) {
const candidate = types[i];
if (
!isClass(candidate) ||
TypeBase.isInstance(candidate) !== TypeBase.isInstance(types[0]) ||
!ClassType.isSameGenericClass(types[0], candidate)
) {
return undefined;
}
}
if (isTupleClass(types[0])) {
return specializeTupleClass(types[0], [{ type: UnknownType.create(), isUnbounded: true }]);
}
return ClassType.cloneForSpecialization(
types[0],
types[0].details.typeParameters.map((t) => UnknownType.create()),
/* isTypeArgumentExplicit */ true
);
}
// If the type is a function or overloaded function that has a paramSpec
// associated with it and P.args and P.kwargs at the end of the signature,
// it removes these parameters from the function.

View File

@ -1,9 +1,12 @@
# This sample tests overload matching in cases where one or more
# matches are found due to an Any or Unknown argument.
from typing import Any, Literal, overload
from __future__ import annotations
from typing import Any, Generic, Literal, TypeVar, overload
from typing_extensions import LiteralString
_T = TypeVar("_T")
@overload
def overload1(x: int, y: float) -> float:
@ -119,3 +122,82 @@ def unknown_any() -> Any:
def func5(a: Any):
reveal_type(overload4(a, flag=False), expected_text="str")
reveal_type(overload4("0", flag=a), expected_text="Unknown")
@overload
def overload5(x: list[int]) -> list[int]:
...
@overload
def overload5(x: list[str]) -> list[str]:
...
def overload5(x: list[str] | list[int]) -> list[str] | list[int]:
return x
def func6(y: list[Any]):
reveal_type(overload5(y), expected_text="list[Unknown]")
class ClassA(Generic[_T]):
@overload
def m1(self: ClassA[int]) -> ClassA[int]:
...
@overload
def m1(self: ClassA[str]) -> ClassA[str]:
...
def m1(self) -> ClassA[Any]:
return self
def func7(a: ClassA[Any]):
reveal_type(a.m1(), expected_text="ClassA[int]")
class ClassB(Generic[_T]):
@overload
def m1(self: ClassB[int], obj: int | ClassB[int]) -> ClassB[int]:
...
@overload
def m1(self: ClassB[str], obj: str | ClassB[str]) -> ClassB[str]:
...
def m1(self, obj: Any) -> ClassB[Any]:
return self
def func8(b: ClassB[Any]):
reveal_type(b.m1(b), expected_text="ClassB[Unknown]")
_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
@overload
def overload6(a: _T1, /) -> tuple[_T1]:
...
@overload
def overload6(a: _T1, b: _T2, /) -> tuple[_T1, _T2]:
...
@overload
def overload6(*args: _T1) -> tuple[_T1, ...]:
...
def overload6(*args: Any) -> tuple[Any, ...]:
return tuple(args)
def func9(*args: int):
reveal_type(overload6(*args), expected_text="tuple[int, ...]")