Added support for Concatenate as described in latest version of PEP 612. Added ParamSpec and Concatenate to typing.pyi.

This commit is contained in:
Eric Traut 2020-08-14 22:08:06 -07:00
parent e7e1f79b7f
commit 9e1539dba9
10 changed files with 188 additions and 36 deletions

View File

@ -46,6 +46,12 @@ if sys.version_info >= (3, 8):
# TypedDict is a (non-subscriptable) special form.
TypedDict: object
if sys.version_info >= (3, 10):
class ParamSpec:
__name__: str
def __init__(self, name: str) -> None: ...
Concatenate: _SpecialForm = ...
if sys.version_info < (3, 7):
class GenericMeta(type): ...

View File

@ -2573,6 +2573,7 @@ export class Binder extends ParseTreeWalker {
Annotated: true,
TypeAlias: true,
OrderedDict: true,
Concatenate: true,
};
const assignedName = assignedNameNode.value;

View File

@ -141,6 +141,7 @@ import {
NoneType,
ObjectType,
OverloadedFunctionType,
ParamSpecEntry,
removeNoneFromUnion,
removeUnboundFromUnion,
Type,
@ -4352,6 +4353,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
className === 'Protocol' ||
className === 'Generic' ||
className === 'Callable' ||
className === 'Concatenate' ||
className === 'Type'
) {
const fileInfo = getFileInfo(errorNode);
@ -5345,7 +5347,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
requiresTypeVarMatching: requiresSpecialization(paramType),
argument: funcArg,
errorNode: argList[argIndex].valueExpression || errorNode,
paramName: paramName,
paramName: typeParams[paramIndex].isNameSynthesized ? undefined : paramName,
});
trySetActive(argList[argIndex], typeParams[paramIndex]);
@ -5380,7 +5382,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
requiresTypeVarMatching: requiresSpecialization(paramType),
argument: argList[argIndex],
errorNode: argList[argIndex].valueExpression || errorNode,
paramName: paramName,
paramName: typeParams[paramIndex].isNameSynthesized ? undefined : paramName,
});
trySetActive(argList[argIndex], typeParams[paramIndex]);
@ -5505,7 +5507,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
type: param.defaultType,
},
errorNode: errorNode,
paramName: param.name,
paramName: param.isNameSynthesized ? undefined : param.name,
});
}
}
@ -7548,10 +7550,30 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
} else if (isEllipsisType(typeArgs[0].type)) {
FunctionType.addDefaultParameters(functionType);
} else if (isParamSpecType(typeArgs[0].type)) {
FunctionType.addDefaultParameters(functionType);
functionType.details.paramSpec = typeArgs[0].type as TypeVarType;
} else {
addError(Localizer.Diagnostic.callableFirstArg(), typeArgs[0].node);
if (isClass(typeArgs[0].type) && ClassType.isBuiltIn(typeArgs[0].type, 'Concatenate')) {
const concatTypeArgs = typeArgs[0].type.typeArguments;
if (concatTypeArgs && concatTypeArgs.length > 0) {
concatTypeArgs.forEach((typeArg, index) => {
if (index === concatTypeArgs.length - 1) {
if (isParamSpecType(typeArg)) {
functionType.details.paramSpec = typeArg as TypeVarType;
}
} else {
FunctionType.addParameter(functionType, {
category: ParameterCategory.Simple,
name: `__p${index}`,
isNameSynthesized: true,
hasDeclaredType: true,
type: typeArg,
});
}
});
}
} else {
addError(Localizer.Diagnostic.callableFirstArg(), typeArgs[0].node);
}
}
} else {
FunctionType.addDefaultParameters(functionType, /* useUnknown */ true);
@ -7737,6 +7759,30 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
return typeArgs[0].type;
}
function createConcatenateType(
errorNode: ParseNode,
classType: ClassType,
typeArgs: TypeResult[] | undefined
): Type {
if (!typeArgs || typeArgs.length === 0) {
addError(Localizer.Diagnostic.concatenateTypeArgsMissing(), errorNode);
} else {
typeArgs.forEach((typeArg, index) => {
if (index === typeArgs.length - 1) {
if (!isParamSpecType(typeArg.type)) {
addError(Localizer.Diagnostic.concatenateParamSpecMissing(), typeArg.node);
}
} else {
if (isParamSpecType(typeArg.type)) {
addError(Localizer.Diagnostic.paramSpecContext(), typeArg.node);
}
}
});
}
return createSpecialType(classType, typeArgs, /* paramLimit */ undefined, /* allowParamSpec */ true);
}
function createAnnotatedType(errorNode: ParseNode, typeArgs: TypeResult[] | undefined): Type {
if (!typeArgs || typeArgs.length < 1) {
addError(Localizer.Diagnostic.annotatedTypeArgMissing(), errorNode);
@ -7996,6 +8042,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
Optional: { alias: '', module: 'builtins' },
Annotated: { alias: '', module: 'builtins' },
TypeAlias: { alias: '', module: 'builtins' },
Concatenate: { alias: '', module: 'builtins' },
};
const aliasMapEntry = specialTypes[assignedName];
@ -11465,6 +11512,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
case 'Annotated': {
return createAnnotatedType(errorNode, typeArgs);
}
case 'Concatenate': {
return createConcatenateType(errorNode, classType, typeArgs);
}
}
}
@ -13875,13 +13926,15 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
const nonDefaultSrcParamCount = srcParams.filter((param) => !!param.name && !param.hasDefault).length;
if (destParamCount < nonDefaultSrcParamCount) {
diag.addMessage(
Localizer.DiagnosticAddendum.functionTooFewParams().format({
expected: nonDefaultSrcParamCount,
received: destParamCount,
})
);
canAssign = false;
if (!destType.details.paramSpec) {
diag.addMessage(
Localizer.DiagnosticAddendum.functionTooFewParams().format({
expected: nonDefaultSrcParamCount,
received: destParamCount,
})
);
canAssign = false;
}
}
if (destParamCount > srcParamCount) {
@ -13922,7 +13975,18 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
// Are we assigning to a function with a ParamSpec?
if (destType.details.paramSpec && typeVarMap && !typeVarMap.isLocked()) {
typeVarMap.setParamSpec(destType.details.paramSpec.name, srcType);
typeVarMap.setParamSpec(
destType.details.paramSpec.name,
srcType.details.parameters
.map((p, index) => {
const paramSpecEntry: ParamSpecEntry = {
name: p.name || `__p${index}`,
type: p.type,
};
return paramSpecEntry;
})
.slice(destType.details.parameters.length, srcType.details.parameters.length)
);
}
return canAssign;
@ -14638,6 +14702,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, printTypeFlags:
// Callable notation.
const parts = printFunctionParts(type, recursionCount);
if (type.details.paramSpec) {
if (type.details.parameters.length > 0) {
// Remove the args and kwargs parameters from the end.
const paramTypes = type.details.parameters.map((param) => printType(param.type));
return `Callable[Concatenate[${paramTypes.join(', ')}, ${type.details.paramSpec.name}], ${
parts[1]
}]`;
}
return `Callable[${type.details.paramSpec.name}, ${parts[1]}]`;
}
return `(${parts[0].join(', ')}) -> ${parts[1]}`;

View File

@ -1427,8 +1427,16 @@ function _specializeFunctionType(
// Handle functions with a parameter specification in a special manner.
if (functionType.details.paramSpec) {
const paramSpec = typeVarMap?.getParamSpec(functionType.details.paramSpec.name);
functionType = FunctionType.cloneForParamSpec(functionType, paramSpec);
let paramSpec = typeVarMap?.getParamSpec(functionType.details.paramSpec.name);
if (!paramSpec && makeConcrete) {
paramSpec = [
{ name: 'args', type: AnyType.create() },
{ name: 'kwargs', type: AnyType.create() },
];
}
if (paramSpec) {
functionType = FunctionType.cloneForParamSpec(functionType, paramSpec);
}
}
const declaredReturnType =

View File

@ -10,17 +10,17 @@
*/
import { assert } from '../common/debug';
import { ClassType, FunctionType, maxTypeRecursionCount, Type, TypeCategory } from './types';
import { ClassType, maxTypeRecursionCount, ParamSpecEntry, Type, TypeCategory } from './types';
export class TypeVarMap {
private _typeVarMap: Map<string, Type>;
private _paramSpecMap: Map<string, FunctionType>;
private _paramSpecMap: Map<string, ParamSpecEntry[]>;
private _isNarrowableMap: Map<string, boolean>;
private _isLocked = false;
constructor() {
this._typeVarMap = new Map<string, Type>();
this._paramSpecMap = new Map<string, FunctionType>();
this._paramSpecMap = new Map<string, ParamSpecEntry[]>();
this._isNarrowableMap = new Map<string, boolean>();
}
@ -64,11 +64,7 @@ export class TypeVarMap {
score += this._getComplexityScoreForType(value);
});
// Do the same for the param spec map.
this._paramSpecMap.forEach((value) => {
score += 1;
score += this._getComplexityScoreForType(value);
});
score += this._paramSpecMap.size;
return score;
}
@ -91,11 +87,11 @@ export class TypeVarMap {
return this._paramSpecMap.has(name);
}
getParamSpec(name: string): FunctionType | undefined {
getParamSpec(name: string): ParamSpecEntry[] | undefined {
return this._paramSpecMap.get(name);
}
setParamSpec(name: string, type: FunctionType) {
setParamSpec(name: string, type: ParamSpecEntry[]) {
assert(!this._isLocked);
this._paramSpecMap.set(name, type);
}

View File

@ -788,6 +788,11 @@ export interface FunctionType extends TypeBase {
inferredReturnType?: Type;
}
export interface ParamSpecEntry {
name: string;
type: Type;
}
export namespace FunctionType {
export function createInstance(
name: string,
@ -918,9 +923,8 @@ export namespace FunctionType {
return newFunction;
}
// Creates a new function based on the parameters of another function. If
// paramTemplate is undefined, use default (generic) parameters.
export function cloneForParamSpec(type: FunctionType, paramTemplate: FunctionType | undefined) {
// Creates a new function based on the parameters of another function.
export function cloneForParamSpec(type: FunctionType, paramTypes: ParamSpecEntry[] | undefined) {
const newFunction = create(
type.details.name,
type.details.moduleName,
@ -936,10 +940,16 @@ export namespace FunctionType {
// since we're replacing it.
delete newFunction.details.paramSpec;
if (paramTemplate) {
newFunction.details.parameters = paramTemplate.details.parameters;
} else {
FunctionType.addDefaultParameters(newFunction);
if (paramTypes) {
newFunction.details.parameters = paramTypes.map((specEntry, index) => {
return {
category: ParameterCategory.Simple,
name: specEntry.name,
isNameSynthesized: true,
hasDeclaredType: true,
type: specEntry.type,
};
});
}
return newFunction;

View File

@ -214,6 +214,8 @@ export namespace Localizer {
export const classVarTooManyArgs = () => getRawString('Diagnostic.classVarTooManyArgs');
export const comprehensionInDict = () => getRawString('Diagnostic.comprehensionInDict');
export const comprehensionInSet = () => getRawString('Diagnostic.comprehensionInSet');
export const concatenateParamSpecMissing = () => getRawString('Diagnostic.concatenateParamSpecMissing');
export const concatenateTypeArgsMissing = () => getRawString('Diagnostic.concatenateTypeArgsMissing');
export const constantRedefinition = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.constantRedefinition'));
export const constructorNoArgs = () =>

View File

@ -42,6 +42,8 @@
"classVarTooManyArgs": "Expected only one type argument after \"ClassVar\"",
"comprehensionInDict": "Comprehension cannot be used with other dictionary entries",
"comprehensionInSet": "Comprehension cannot be used with other set entries",
"concatenateParamSpecMissing": "Last type argument for \"Concatenate\" must be a ParamSpec",
"concatenateTypeArgsMissing": "\"Concatenate\" requires at least two type arguments",
"constantRedefinition": "\"{name}\" is constant and cannot be redefined",
"continueInFinally": "\"continue\" cannot be used within a finally clause",
"continueOutsideLoop": "\"continue\" can be used only within a loop",

View File

@ -1929,9 +1929,6 @@ test('Unions2', () => {
validateResults(analysisResults38, 0);
});
// Skip ParamSpec tests until they are added back in to the
// specification.
/*
test('ParamSpec1', () => {
const configOptions = new ConfigOptions('.');
@ -1959,7 +1956,14 @@ test('ParamSpec3', () => {
const results = TestUtils.typeAnalyzeSampleFiles(['paramSpec3.py'], configOptions);
validateResults(results, 1);
});
*/
test('ParamSpec4', () => {
const configOptions = new ConfigOptions('.');
configOptions.defaultPythonVersion = PythonVersion.V3_10;
const results = TestUtils.typeAnalyzeSampleFiles(['paramSpec4.py'], configOptions);
validateResults(results, 5);
});
test('ClassVar1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['classVar1.py']);

View File

@ -0,0 +1,52 @@
# This sample tests the type checker's handling of ParamSpec
# and Concatenate as described in PEP 612.
from typing import Callable, Concatenate, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
class Request:
...
def with_request(f: Callable[Concatenate[Request, P], R]) -> Callable[P, R]:
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
return f(Request(), *args, **kwargs)
return inner
@with_request
def takes_int_str(request: Request, x: int, y: str) -> int:
# use request
return x + 7
takes_int_str(1, "A")
# This should generate an error because the first arg
# is the incorrect type.
takes_int_str("B", "A")
# This should generate an error because there are too
# many parameters.
takes_int_str(1, "A", 2)
# This should generate an error because a ParamSpec can appear
# only within the last type arg for Concatenate
def decorator1(f: Callable[Concatenate[P, P]]) -> Callable[P, R]:
...
# This should generate an error because the last type arg
# for Concatenate should be a ParamSpec.
def decorator2(f: Callable[Concatenate[int, int]]) -> Callable[P, R]:
...
# This should generate an error because Concatenate is missing
# its type arguments.
def decorator3(f: Callable[Concatenate]) -> Callable[P, R]:
...