Added support for PEP 585. Standard collection types defined in builtins can now be used like their typing counterparts. This includes "tuple", which needs special-case handling because its class definition in builtins.pyi indicates that it has a single type parameter, but it actually supports variadic parameters.

This commit is contained in:
Eric Traut 2020-09-08 14:24:28 -07:00
parent d9e588c7af
commit 7bed1e1aaa
9 changed files with 193 additions and 59 deletions

View File

@ -14,6 +14,7 @@ Pyright supports [configuration files](/docs/configuration.md) that provide gran
* [PEP 544](https://www.python.org/dev/peps/pep-0544/) structural subtyping
* [PEP 561](https://www.python.org/dev/peps/pep-0561/) distributing and packaging type information
* [PEP 563](https://www.python.org/dev/peps/pep-0563/) postponed evaluation of annotations
* [PEP 585](https://www.python.org/dev/peps/pep-0585/) type hinting generics in standard collections
* [PEP 589](https://www.python.org/dev/peps/pep-0589/) typed dictionaries
* [PEP 591](https://www.python.org/dev/peps/pep-0591/) final qualifier
* [PEP 593](https://www.python.org/dev/peps/pep-0593/) flexible variable annotations

View File

@ -108,6 +108,7 @@ import {
isNoReturnType,
isPartlyUnknown,
isProperty,
isTupleClass,
lookUpClassMember,
makeTypeVarsConcrete,
partiallySpecializeType,
@ -597,7 +598,7 @@ export class Checker extends ParseTreeWalker {
// assert (x > 3, "bad value x")
const type = this._evaluator.getType(node.testExpression);
if (type && isObject(type)) {
if (ClassType.isBuiltIn(type.classType, 'Tuple') && type.classType.typeArguments) {
if (isTupleClass(type.classType) && type.classType.typeArguments) {
if (type.classType.typeArguments.length > 0) {
const lastTypeArg = type.classType.typeArguments[type.classType.typeArguments.length - 1];
if (!isEllipsisType(lastTypeArg)) {
@ -1460,11 +1461,7 @@ export class Checker extends ParseTreeWalker {
};
let isValidType = true;
if (
isObject(arg1Type) &&
ClassType.isBuiltIn(arg1Type.classType, 'Tuple') &&
arg1Type.classType.typeArguments
) {
if (isObject(arg1Type) && isTupleClass(arg1Type.classType) && arg1Type.classType.typeArguments) {
isValidType = !arg1Type.classType.typeArguments.some((typeArg) => !isSupportedTypeForIsInstance(typeArg));
} else {
isValidType = isSupportedTypeForIsInstance(arg1Type);
@ -1513,7 +1510,7 @@ export class Checker extends ParseTreeWalker {
// The isinstance and issubclass call supports a variation where the second
// parameter is a tuple of classes.
const objClass = arg1Type.classType;
if (ClassType.isBuiltIn(objClass, 'Tuple') && objClass.typeArguments) {
if (isTupleClass(objClass) && objClass.typeArguments) {
objClass.typeArguments.forEach((typeArg) => {
if (isClass(typeArg)) {
classTypeList.push(typeArg);

View File

@ -182,6 +182,7 @@ import {
isParamSpecType,
isPartlyUnknown,
isProperty,
isTupleClass,
isTypeAliasRecursive,
lookUpClassMember,
lookUpObjectMember,
@ -426,6 +427,7 @@ const nonSubscriptableBuiltinTypes: { [builtinName: string]: PythonVersion } = {
'builtins.list': PythonVersion.V3_9,
'builtins._PathLike': PythonVersion.V3_9,
'builtins.set': PythonVersion.V3_9,
'builtins.tuple': PythonVersion.V3_9,
'collections.ChainMap': PythonVersion.V3_9,
'collections.Counter': PythonVersion.V3_9,
'collections.DefaultDict': PythonVersion.V3_9,
@ -1013,7 +1015,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
if (!TypeBase.isInstantiable(resultType)) {
const isEmptyTuple =
isObject(resultType) &&
ClassType.isBuiltIn(resultType.classType, 'Tuple') &&
isTupleClass(resultType.classType) &&
resultType.classType.typeArguments?.length === 0;
if ((flags & EvaluatorFlags.AllowEmptyTupleAsType) === 0 || !isEmptyTuple) {
@ -4029,7 +4031,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
// perform special processing of the type args in this case to permit
// empty tuples.
let adjustedFlags = flags;
if (isClass(subtype) && ClassType.isBuiltIn(subtype, 'Tuple')) {
if (isClass(subtype) && isTupleClass(subtype)) {
adjustedFlags |= EvaluatorFlags.AllowEmptyTupleAsType;
}
@ -4329,7 +4331,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
if (expectedType && isObject(expectedType)) {
const tupleClass = expectedType.classType;
if (ClassType.isBuiltIn(tupleClass, 'Tuple') && tupleClass.typeArguments) {
if (isTupleClass(tupleClass) && tupleClass.typeArguments) {
// Is this a homogeneous tuple of indeterminate length? If so,
// match the number of expected types to the number of entries
// in the tuple expression.
@ -4360,10 +4362,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
// unpacked entries onto the new tuple. If it's not an upacked tuple
// but some other iterator (e.g. a List), we won't know the number of
// items, so we'll need to leave the Tuple open-ended.
if (
isObject(typeResult.unpackedType) &&
ClassType.isBuiltIn(typeResult.unpackedType.classType, 'Tuple')
) {
if (isObject(typeResult.unpackedType) && isTupleClass(typeResult.unpackedType.classType)) {
const typeArgs = typeResult.unpackedType.classType.typeArguments;
// If the Tuple wasn't specialized or has a "..." type parameter, we can't
@ -4393,20 +4392,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
return { type, node };
}
// Classes of type Tuple and tuple require special handling because they
// support variadic type parameters (including a form that represents homogenous
// arbitrary-length tuples).
function cloneTupleForSpecialization(tupleClass: ClassType, typeArgs: Type[], isTypeArgumentExplicit: boolean) {
// Verify our assumptions in case typing.pyi changes its definition of Tuple.
if (
tupleClass.details.mro.length < 2 ||
!isClass(tupleClass.details.mro[0]) ||
!ClassType.isBuiltIn(tupleClass.details.mro[0], 'Tuple') ||
!isClass(tupleClass.details.mro[1]) ||
!ClassType.isBuiltIn(tupleClass.details.mro[1], 'tuple')
) {
return tupleClass;
}
// Create a copy of the Tuple class that overrides the normal MRO
// entries with a version of Tuple and tuple that are specialized
// entries with a version of Tuple and/or tuple that are specialized
// appropriately.
let combinedTupleType: Type = AnyType.create(false);
if (typeArgs.length === 2 && isEllipsisType(typeArgs[1])) {
@ -4415,17 +4406,51 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
combinedTupleType = combineTypes(typeArgs);
}
const specializedTuple = ClassType.cloneForSpecialization(tupleClass, typeArgs, isTypeArgumentExplicit);
const effectiveTypeArguments = [combinedTupleType];
const specializedTuple = ClassType.cloneForSpecialization(
tupleClass,
typeArgs,
isTypeArgumentExplicit,
/* skipAbstractClassTest */ undefined,
effectiveTypeArguments
);
specializedTuple.details = { ...specializedTuple.details };
specializedTuple.details.mro = [...specializedTuple.details.mro];
specializedTuple.details.mro[0] = specializedTuple;
specializedTuple.details.mro[1] = ClassType.cloneForSpecialization(
specializedTuple.details.mro[1] as ClassType,
[combinedTupleType],
isTypeArgumentExplicit
);
return specializedTuple;
// Handle the specialization of "Tuple" which inherits from "tuple".
if (ClassType.isBuiltIn(tupleClass, 'Tuple')) {
assert(
tupleClass.details.mro.length >= 2 &&
isClass(tupleClass.details.mro[0]) &&
ClassType.isBuiltIn(tupleClass.details.mro[0], 'Tuple') &&
isClass(tupleClass.details.mro[1]) &&
ClassType.isBuiltIn(tupleClass.details.mro[1], 'tuple')
);
specializedTuple.details.mro[1] = ClassType.cloneForSpecialization(
specializedTuple.details.mro[1] as ClassType,
[combinedTupleType],
isTypeArgumentExplicit,
/* skipAbstractClassTest */ undefined,
effectiveTypeArguments
);
return specializedTuple;
}
// Handle the specialization of "tuple" directly.
if (ClassType.isBuiltIn(tupleClass, 'tuple')) {
assert(
tupleClass.details.mro.length >= 1 &&
isClass(tupleClass.details.mro[0]) &&
ClassType.isBuiltIn(tupleClass.details.mro[0], 'tuple')
);
return specializedTuple;
}
return tupleClass;
}
function updateNamedTupleBaseClass(classType: ClassType, typeArgs: Type[], isTypeArgumentExplicit: boolean) {
@ -5236,7 +5261,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
}
// If the expected type is generic (not specialized), we can't proceed.
const expectedTypeArgs = expectedTypeWithoutNone.classType.typeArguments;
const expectedTypeArgs =
expectedTypeWithoutNone.classType.effectiveTypeArguments || expectedTypeWithoutNone.classType.typeArguments;
if (expectedTypeArgs === undefined) {
return;
}
@ -5619,7 +5645,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
// type which will be a union of all element types.
if (
isObject(argType) &&
ClassType.isBuiltIn(argType.classType, 'Tuple') &&
isTupleClass(argType.classType) &&
argType.classType.typeArguments &&
argType.classType.typeArguments.length > 0 &&
!isEllipsisType(argType.classType.typeArguments[argType.classType.typeArguments.length - 1])
@ -6362,7 +6388,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
const arg1Type = getTypeForArgument(argList[1]);
if (
!isObject(arg1Type) ||
!ClassType.isBuiltIn(arg1Type.classType, 'Tuple') ||
!isTupleClass(arg1Type.classType) ||
arg1Type.classType.typeArguments === undefined
) {
return undefined;
@ -7494,7 +7520,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
// The result should be a Tuple
if (isObject(dictEntryType)) {
const classType = dictEntryType.classType;
if (ClassType.isBuiltIn(classType, 'Tuple')) {
if (isTupleClass(classType)) {
const typeArgs = classType.typeArguments;
if (typeArgs && typeArgs.length === 2) {
keyTypes.push(typeArgs[0]);
@ -8209,7 +8235,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
paramLimit?: number,
allowParamSpec = false
): Type {
const isTuple = ClassType.isBuiltIn(classType, 'Tuple');
const isTuple = isTupleClass(classType);
if (typeArgs) {
// Verify that we didn't receive any inappropriate ellipses or modules.
@ -8228,11 +8254,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
});
// Handle Tuple[()] as a special case, as defined in PEP 483.
if (ClassType.isBuiltIn(classType, 'Tuple')) {
if (isTuple) {
if (
typeArgs.length === 1 &&
isObject(typeArgs[0].type) &&
ClassType.isBuiltIn(typeArgs[0].type.classType, 'Tuple') &&
isTupleClass(typeArgs[0].type.classType) &&
typeArgs[0].type.classType.typeArguments &&
typeArgs[0].type.classType.typeArguments.length === 0
) {
@ -11812,7 +11838,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
if (isObject(argType)) {
const objClass = argType.classType;
if (ClassType.isBuiltIn(objClass, 'Tuple') && objClass.typeArguments) {
if (isTupleClass(objClass) && objClass.typeArguments) {
let foundNonClassType = false;
const classTypeList: ClassType[] = [];
objClass.typeArguments.forEach((typeArg) => {
@ -12184,6 +12210,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
}
}
// Handle "tuple" specially, since it needs to act like "Tuple"
// in Python 3.9 and newer.
if (ClassType.isBuiltIn(classType, 'tuple')) {
return createSpecialType(classType, typeArgs, undefined);
}
let typeArgCount = typeArgs ? typeArgs.length : 0;
// Make sure the argument list count is correct.
@ -13545,7 +13577,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
if (ancestorIndex === 0 && ClassType.isSpecialBuiltIn(destType)) {
// Handle built-in types that support arbitrary numbers
// of type parameters like Tuple.
if (destType.details.name === 'Tuple') {
if (isTupleClass(destType)) {
if (destType.typeArguments && curSrcType.typeArguments) {
const destTypeArgs = destType.typeArguments;
let destArgCount = destTypeArgs.length;
@ -13607,8 +13639,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
}
// If the dest type isn't specialized, there are no type args to validate.
const ancestorTypeArgs = ancestorType.typeArguments;
if (!ancestorTypeArgs) {
if (!ancestorType.typeArguments) {
return true;
}
@ -13634,9 +13665,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
!typeVarMap.isLocked()
) {
// Populate the typeVar map with type arguments of the source.
const srcTypeArgs = curSrcType.effectiveTypeArguments || curSrcType.typeArguments;
for (let i = 0; i < destType.details.typeParameters.length; i++) {
const typeArgType =
i < curSrcType.typeArguments.length ? curSrcType.typeArguments[i] : UnknownType.create();
i < srcTypeArgs.length ? srcTypeArgs[i] : UnknownType.create();
typeVarMap.setTypeVar(destType.details.typeParameters[i], typeArgType, /* isNarrowable */ true);
}
}
@ -13672,9 +13704,9 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
assert(ClassType.isSameGenericClass(destType, srcType));
const destTypeParams = ClassType.getTypeParameters(destType);
const destTypeArgs = destType.typeArguments!;
const destTypeArgs = destType.effectiveTypeArguments || destType.typeArguments!;
assert(destTypeArgs !== undefined);
const srcTypeArgs = srcType.typeArguments;
const srcTypeArgs = srcType.effectiveTypeArguments || srcType.typeArguments;
if (srcTypeArgs) {
if (ClassType.isSpecialBuiltIn(srcType) || srcTypeArgs.length === destTypeParams.length) {
@ -15442,7 +15474,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
']';
}
} else {
if (ClassType.isBuiltIn(type, 'Tuple')) {
if (isTupleClass(type)) {
objName += '[()]';
}
}

View File

@ -400,7 +400,7 @@ export function canBeFalsy(type: Type, recursionLevel = 0): boolean {
case TypeCategory.Object: {
// Handle tuples specially.
if (ClassType.isBuiltIn(type.classType, 'Tuple') && type.classType.typeArguments) {
if (isTupleClass(type.classType) && type.classType.typeArguments) {
if (type.classType.typeArguments.length === 0) {
return true;
}
@ -463,7 +463,7 @@ export function canBeTruthy(type: Type, recursionLevel = 0): boolean {
case TypeCategory.Object: {
// Check for Tuple[()] (an empty tuple).
if (ClassType.isBuiltIn(type.classType, 'Tuple')) {
if (isTupleClass(type.classType)) {
if (type.classType.typeArguments && type.classType.typeArguments.length === 0) {
return false;
}
@ -495,11 +495,9 @@ export function getSpecializedTupleType(type: Type): ClassType | undefined {
return undefined;
}
// See if this class derives from Tuple. If it does, we'll assume that it
// See if this class derives from Tuple or tuple. If it does, we'll assume that it
// hasn't been overridden in a way that changes the behavior of the tuple class.
const tupleClass = classType.details.mro.find(
(mroClass) => isClass(mroClass) && ClassType.isBuiltIn(mroClass, 'Tuple')
);
const tupleClass = classType.details.mro.find((mroClass) => isClass(mroClass) && isTupleClass(mroClass));
if (!tupleClass || !isClass(tupleClass)) {
return undefined;
}
@ -556,6 +554,10 @@ export function isProperty(type: Type): type is ObjectType {
return isObject(type) && ClassType.isPropertyClass(type.classType);
}
export function isTupleClass(type: ClassType) {
return ClassType.isBuiltIn(type) && (type.details.name === 'Tuple' || type.details.name === 'tuple');
}
// Partially specializes a type within the context of a specified
// (presumably specialized) class.
export function partiallySpecializeType(type: Type, contextClassType: ClassType): Type {
@ -955,6 +957,11 @@ export function setTypeArgumentsRecursive(destType: Type, srcType: Type, typeVar
setTypeArgumentsRecursive(typeArg, srcType, typeVarMap, recursionCount + 1);
});
}
if (destType.effectiveTypeArguments) {
destType.effectiveTypeArguments.forEach((typeArg) => {
setTypeArgumentsRecursive(typeArg, srcType, typeVarMap, recursionCount + 1);
});
}
break;
case TypeCategory.Object:
@ -1403,6 +1410,7 @@ function _specializeClassType(
}
let newTypeArgs: Type[] = [];
let newEffectiveTypeArgs: Type[] | undefined;
let specializationNeeded = false;
// If type args were previously provided, specialize them.
@ -1414,6 +1422,16 @@ function _specializeClassType(
}
return newTypeArgType;
});
if (classType.effectiveTypeArguments) {
newEffectiveTypeArgs = classType.effectiveTypeArguments.map((oldTypeArgType) => {
const newTypeArgType = specializeType(oldTypeArgType, typeVarMap, makeConcrete, recursionLevel + 1);
if (newTypeArgType !== oldTypeArgType) {
specializationNeeded = true;
}
return newTypeArgType;
});
}
} else {
ClassType.getTypeParameters(classType).forEach((typeParam) => {
let typeArgType: Type;
@ -1441,7 +1459,13 @@ function _specializeClassType(
return classType;
}
return ClassType.cloneForSpecialization(classType, newTypeArgs, /* isTypeArgumentExplicit */ false);
return ClassType.cloneForSpecialization(
classType,
newTypeArgs,
/* isTypeArgumentExplicit */ false,
/* skipAbstractClassTest */ undefined,
newEffectiveTypeArgs
);
}
// Converts a type var type into the most specific type

View File

@ -310,6 +310,13 @@ export interface ClassType extends TypeBase {
// some or all of the type parameters.
typeArguments?: Type[];
// For a few classes (e.g., Tuple and tuple), the class definition
// calls for a single type parameter but the spec allows the programmer
// to provide variadic type arguments. To make these compatible, we need
// to derive a single effective type argument from the provided type
// arguments.
effectiveTypeArguments?: Type[];
// If type arguments are present, were they explicit (i.e.
// provided explicitly in the code)?
isTypeArgumentExplicit?: boolean;
@ -359,7 +366,8 @@ export namespace ClassType {
classType: ClassType,
typeArguments: Type[] | undefined,
isTypeArgumentExplicit: boolean,
skipAbstractClassTest = false
skipAbstractClassTest = false,
effectiveTypeArguments?: Type[]
): ClassType {
const newClassType = create(
classType.details.name,
@ -372,8 +380,13 @@ export namespace ClassType {
);
newClassType.details = classType.details;
newClassType.typeArguments = typeArguments;
if (typeArguments) {
newClassType.typeArguments = typeArguments;
}
newClassType.isTypeArgumentExplicit = isTypeArgumentExplicit;
if (effectiveTypeArguments) {
newClassType.effectiveTypeArguments = effectiveTypeArguments;
}
if (classType.literalValue !== undefined) {
newClassType.literalValue = classType.literalValue;

View File

@ -762,6 +762,12 @@ test('Tuples7', () => {
validateResults(analysisResults, 7);
});
test('Tuples8', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['tuples8.py']);
validateResults(analysisResults, 7);
});
test('NamedTuples1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['namedTuples1.py']);

View File

@ -1,7 +1,7 @@
# This sample tests handling of tuples and tracking
# of specific types within a tuple.
from typing import NamedTuple, Tuple, TypeVar
from typing import Tuple, TypeVar
_T = TypeVar("_T")
@ -38,6 +38,7 @@ for aaa in objA:
class ClassB(Tuple[_T, ...]):
pass
objB = ClassB[complex]()
(x, y, z) = objB
@ -56,4 +57,3 @@ xx3: int = x
yy3: int = y
zz3: int = z

View File

@ -0,0 +1,60 @@
# This sample verifies that the "tuple" type is treated
# analogously to "Tuple" type.
from typing import TypeVar
_T = TypeVar("_T")
class ClassA(tuple[int, str, int, _T]):
pass
objA = ClassA[complex]()
(a, b, c, d) = objA
aa1: int = a
bb1: str = b
cc1: int = c
dd1: complex = d
aa2: int = objA[0]
bb2: str = objA[1]
cc2: int = objA[2]
dd2: complex = objA[3]
# These should generate errors because
# these are not the correct types.
aa3: str = a
bb3: complex = b
cc3: str = c
dd3: int = d
for aaa in objA:
print(aaa)
class ClassB(tuple[_T, ...]):
pass
objB = ClassB[complex]()
(x, y, z) = objB
xx1: complex = x
yy1: complex = y
zz1: complex = z
xx2: complex = objB[0]
yy2: complex = objB[1]
zz2: complex = objB[2]
# These should generate errors because
# these are not the correct types.
xx3: int = x
yy3: int = y
zz3: int = z

View File

@ -15,6 +15,7 @@ Pyright supports [configuration files](/docs/configuration.md) that provide gran
* [PEP 544](https://www.python.org/dev/peps/pep-0544/) structural subtyping
* [PEP 561](https://www.python.org/dev/peps/pep-0561/) distributing and packaging type information
* [PEP 563](https://www.python.org/dev/peps/pep-0563/) postponed evaluation of annotations
* [PEP 585](https://www.python.org/dev/peps/pep-0585/) type hinting generics in standard collections
* [PEP 589](https://www.python.org/dev/peps/pep-0589/) typed dictionaries
* [PEP 591](https://www.python.org/dev/peps/pep-0591/) final qualifier
* [PEP 593](https://www.python.org/dev/peps/pep-0593/) flexible variable annotations