Added experimental support for inlined TypedDict definitions using the dict[{'a': int}] syntax.

This commit is contained in:
Eric Traut 2023-03-07 12:59:36 -07:00
parent a8753c745f
commit 8e56215661
6 changed files with 190 additions and 57 deletions

View File

@ -146,6 +146,7 @@ import {
assignToTypedDict,
assignTypedDictToTypedDict as assignTypedDictToTypedDict,
createTypedDictType,
createTypedDictTypeInlined,
getTypedDictMembersForClass,
getTypeOfIndexedTypedDict,
synthesizeTypedDictClassMethods,
@ -357,6 +358,7 @@ interface GetTypeArgsOptions {
hasCustomClassGetItem?: boolean;
isFinalAnnotation?: boolean;
isClassVarAnnotation?: boolean;
supportsTypedDictTypeArg?: boolean;
}
interface MatchArgsToParamsResult {
@ -642,6 +644,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
let strClassType: Type | undefined;
let dictClassType: Type | undefined;
let typedDictClassType: Type | undefined;
let typedDictPrivateClassType: Type | undefined;
let printExpressionSpaceCount = 0;
let incompleteGenerationCount = 0;
@ -947,7 +950,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
intClassType = getBuiltInType(node, 'int');
strClassType = getBuiltInType(node, 'str');
dictClassType = getBuiltInType(node, 'dict');
typedDictClassType = getTypingType(node, '_TypedDict');
typedDictClassType = getTypingType(node, 'TypedDict');
typedDictPrivateClassType = getTypingType(node, '_TypedDict');
}
}
@ -2782,7 +2786,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
}
function getTypedDictClassType() {
return typedDictClassType;
return typedDictPrivateClassType;
}
function getTupleClassType() {
@ -6679,11 +6683,18 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
const isClassVarAnnotation =
isInstantiableClass(concreteSubtype) && ClassType.isBuiltIn(concreteSubtype, 'ClassVar');
// Inlined TypedDicts are supported only for 'dict' (and not for 'Dict').
const supportsTypedDictTypeArg =
isInstantiableClass(concreteSubtype) &&
ClassType.isBuiltIn(concreteSubtype, 'dict') &&
!concreteSubtype.aliasName;
let typeArgs = getTypeArgs(node, flags, {
isAnnotatedClass,
hasCustomClassGetItem: hasCustomClassGetItem || !isGenericClass,
isFinalAnnotation,
isClassVarAnnotation,
supportsTypedDictTypeArg,
});
if (!isAnnotatedClass) {
@ -7190,7 +7201,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
node: expr,
};
} else {
typeResult = getTypeArg(expr, adjFlags);
typeResult = getTypeArg(expr, adjFlags, !!options?.supportsTypedDictTypeArg && argIndex === 0);
}
return typeResult;
@ -7239,7 +7250,11 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
return typeArgs;
}
function getTypeArg(node: ExpressionNode, flags: EvaluatorFlags): TypeResultWithNode {
function getTypeArg(
node: ExpressionNode,
flags: EvaluatorFlags,
supportsDictExpression: boolean
): TypeResultWithNode {
let typeResult: TypeResultWithNode;
let adjustedFlags =
@ -7264,9 +7279,26 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
// Set the node's type so it isn't reevaluated later.
setTypeForNode(node, UnknownType.create());
} else if (node.nodeType === ParseNodeType.Dictionary && supportsDictExpression) {
const inlinedTypeDict =
typedDictClassType && isInstantiableClass(typedDictClassType)
? createTypedDictTypeInlined(evaluatorInterface, node, typedDictClassType)
: undefined;
const keyTypeFallback =
strClassType && isInstantiableClass(strClassType) ? strClassType : UnknownType.create();
typeResult = {
type: keyTypeFallback,
inlinedTypeDict,
node,
};
} else {
typeResult = { ...getTypeOfExpression(node, adjustedFlags), node };
if (node.nodeType === ParseNodeType.Dictionary) {
addError(Localizer.Diagnostic.dictInAnnotation(), node);
}
// "Protocol" is not allowed as a type argument.
if (isClass(typeResult.type) && ClassType.isBuiltIn(typeResult.type, 'Protocol')) {
addError(Localizer.Diagnostic.protocolNotAllowedInTypeArgument(), node);
@ -19727,11 +19759,28 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
if (typeArgs) {
let minTypeArgCount = typeParameters.length;
const firstNonDefaultParam = typeParameters.findIndex((param) => !!param.details.defaultType);
if (firstNonDefaultParam >= 0) {
minTypeArgCount = firstNonDefaultParam;
}
if (typeArgCount > typeParameters.length) {
// Classes that accept inlined type dict type args allow only one.
if (typeArgs[0].inlinedTypeDict) {
if (typeArgs.length > 1) {
addDiagnostic(
fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
DiagnosticRule.reportGeneralTypeIssues,
Localizer.Diagnostic.typeArgsTooMany().format({
name: classType.aliasName || classType.details.name,
expected: 1,
received: typeArgCount,
}),
typeArgs[1].node
);
}
return { type: typeArgs[0].inlinedTypeDict };
} else if (typeArgCount > typeParameters.length) {
if (!ClassType.isPartiallyEvaluated(classType) && !ClassType.isTupleClass(classType)) {
const fileInfo = AnalyzerNodeInfo.getFileInfo(errorNode);
if (typeParameters.length === 0) {

View File

@ -163,6 +163,9 @@ export interface TypeResult<T extends Type = Type> {
unpackedType?: Type | undefined;
typeList?: TypeResultWithNode[] | undefined;
// For inlined TypedDict definitions.
inlinedTypeDict?: ClassType;
// Type consistency errors detected when evaluating this type.
typeErrors?: boolean | undefined;

View File

@ -18,6 +18,7 @@ import { Localizer } from '../localization/localize';
import {
ArgumentCategory,
ClassNode,
DictionaryNode,
ExpressionNode,
IndexNode,
ParameterCategory,
@ -27,7 +28,7 @@ import { KeywordType } from '../parser/tokenizerTypes';
import * as AnalyzerNodeInfo from './analyzerNodeInfo';
import { DeclarationType, VariableDeclaration } from './declaration';
import * as ParseTreeUtils from './parseTreeUtils';
import { Symbol, SymbolFlags } from './symbol';
import { Symbol, SymbolFlags, SymbolTable } from './symbol';
import { getLastTypedDeclaredForSymbol } from './symbolUtils';
import { EvaluatorUsage, FunctionArgument, TypeEvaluator, TypeResult, TypeResultWithNode } from './typeEvaluatorTypes';
import {
@ -116,7 +117,6 @@ export function createTypedDictType(
evaluator.addError(Localizer.Diagnostic.typedDictSecondArgDict(), errorNode);
} else {
const entriesArg = argList[1];
const entrySet = new Set<string>();
if (
entriesArg.argumentCategory === ArgumentCategory.Simple &&
@ -124,57 +124,10 @@ export function createTypedDictType(
entriesArg.valueExpression.nodeType === ParseNodeType.Dictionary
) {
usingDictSyntax = true;
const entryDict = entriesArg.valueExpression;
entryDict.entries.forEach((entry) => {
if (entry.nodeType !== ParseNodeType.DictionaryKeyEntry) {
evaluator.addError(Localizer.Diagnostic.typedDictSecondArgDictEntry(), entry);
return;
}
if (entry.keyExpression.nodeType !== ParseNodeType.StringList) {
evaluator.addError(Localizer.Diagnostic.typedDictEntryName(), entry.keyExpression);
return;
}
const entryName = entry.keyExpression.strings.map((s) => s.value).join('');
if (!entryName) {
evaluator.addError(Localizer.Diagnostic.typedDictEmptyName(), entry.keyExpression);
return;
}
if (entrySet.has(entryName)) {
evaluator.addError(Localizer.Diagnostic.typedDictEntryUnique(), entry.keyExpression);
return;
}
// Record names in a set to detect duplicates.
entrySet.add(entryName);
const newSymbol = new Symbol(SymbolFlags.InstanceMember);
const declaration: VariableDeclaration = {
type: DeclarationType.Variable,
node: entry.keyExpression,
path: fileInfo.filePath,
typeAnnotationNode: entry.valueExpression,
isRuntimeTypeExpression: true,
range: convertOffsetsToRange(
entry.keyExpression.start,
TextRange.getEnd(entry.keyExpression),
fileInfo.lines
),
moduleName: fileInfo.moduleName,
isInExceptSuite: false,
};
newSymbol.addDeclaration(declaration);
classFields.set(entryName, newSymbol);
});
// Set the type in the type cache for the dict node so it doesn't
// get evaluated again.
evaluator.setTypeForNode(entryDict);
getTypedDictFieldsFromDictSyntax(evaluator, entriesArg.valueExpression, classFields);
} else if (entriesArg.name) {
const entrySet = new Set<string>();
for (let i = 1; i < argList.length; i++) {
const entry = argList[i];
if (!entry.name || !entry.valueExpression) {
@ -242,6 +195,34 @@ export function createTypedDictType(
return classType;
}
// Creates a new anonymous TypedDict class from an inlined dict[{}] type annotation.
export function createTypedDictTypeInlined(
evaluator: TypeEvaluator,
dictNode: DictionaryNode,
typedDictClass: ClassType
): ClassType {
const fileInfo = AnalyzerNodeInfo.getFileInfo(dictNode);
const className = '<TypedDict>';
const classType = ClassType.createInstantiable(
className,
ParseTreeUtils.getClassFullName(dictNode, fileInfo.moduleName, className),
fileInfo.moduleName,
fileInfo.filePath,
ClassTypeFlags.TypedDictClass,
ParseTreeUtils.getTypeSourceId(dictNode),
/* declaredMetaclass */ undefined,
typedDictClass.details.effectiveMetaclass
);
classType.details.baseClasses.push(typedDictClass);
computeMroLinearization(classType);
getTypedDictFieldsFromDictSyntax(evaluator, dictNode, classType.details.fields);
synthesizeTypedDictClassMethods(evaluator, dictNode, classType, /* isClassFinal */ true);
return classType;
}
export function synthesizeTypedDictClassMethods(
evaluator: TypeEvaluator,
node: ClassNode | ExpressionNode,
@ -557,6 +538,64 @@ export function getTypedDictMembersForClass(evaluator: TypeEvaluator, classType:
return entries;
}
function getTypedDictFieldsFromDictSyntax(
evaluator: TypeEvaluator,
entryDict: DictionaryNode,
classFields: SymbolTable
) {
const entrySet = new Set<string>();
const fileInfo = AnalyzerNodeInfo.getFileInfo(entryDict);
entryDict.entries.forEach((entry) => {
if (entry.nodeType !== ParseNodeType.DictionaryKeyEntry) {
evaluator.addError(Localizer.Diagnostic.typedDictSecondArgDictEntry(), entry);
return;
}
if (entry.keyExpression.nodeType !== ParseNodeType.StringList) {
evaluator.addError(Localizer.Diagnostic.typedDictEntryName(), entry.keyExpression);
return;
}
const entryName = entry.keyExpression.strings.map((s) => s.value).join('');
if (!entryName) {
evaluator.addError(Localizer.Diagnostic.typedDictEmptyName(), entry.keyExpression);
return;
}
if (entrySet.has(entryName)) {
evaluator.addError(Localizer.Diagnostic.typedDictEntryUnique(), entry.keyExpression);
return;
}
// Record names in a set to detect duplicates.
entrySet.add(entryName);
const newSymbol = new Symbol(SymbolFlags.InstanceMember);
const declaration: VariableDeclaration = {
type: DeclarationType.Variable,
node: entry.keyExpression,
path: fileInfo.filePath,
typeAnnotationNode: entry.valueExpression,
isRuntimeTypeExpression: true,
range: convertOffsetsToRange(
entry.keyExpression.start,
TextRange.getEnd(entry.keyExpression),
fileInfo.lines
),
moduleName: fileInfo.moduleName,
isInExceptSuite: false,
};
newSymbol.addDeclaration(declaration);
classFields.set(entryName, newSymbol);
});
// Set the type in the type cache for the dict node so it doesn't
// get evaluated again.
evaluator.setTypeForNode(entryDict);
}
function getTypedDictMembersForClassRecursive(
evaluator: TypeEvaluator,
classType: ClassType,

View File

@ -3844,7 +3844,7 @@ export class Parser {
return listNode;
} else if (nextToken.type === TokenType.OpenCurlyBrace) {
const dictNode = this._parseDictionaryOrSetAtom();
if (this._isParsingTypeAnnotation) {
if (this._isParsingTypeAnnotation && !this._isParsingIndexTrailer) {
const diag = new DiagnosticAddendum();
diag.addMessage(Localizer.DiagnosticAddendum.useDictInstead());
this._addError(Localizer.Diagnostic.dictInAnnotation() + diag.getString(), dictNode);

View File

@ -0,0 +1,36 @@
# This sample tests support for inlined TypedDict definitions.
from typing import Dict
td1: dict[{"a": int, "b": str}] = {"a": 0, "b": ""}
td2: dict[{"a": dict[{"b": int}]}] = {"a": {"b": 0}}
td3: dict[{"a": "list[float]"}] = {"a": [3]}
# This should generate two errors because dictionary literals can be used
# only with dict or Dict.
err1: list[{"a": 1}]
# This should generate an error because dictionary comprehensions
# are not allowed.
err2: dict[{"a": int for _ in range(1)}]
# This should generate an error because unpacked dictionary
# entries are not allowed.
err3: dict[{**{"a": int}}]
# This should generate three errors because Dict doesn't support inlined
# TypedDict. It generates an exception at runtime.
err4: Dict[{"c": int}]
# This should generate an error because an extra type argument is provided.
err5: dict[{"a": int}, str]
def func1(val: dict[{"a": int}]) -> dict[{"a": int}]:
return {"a": val["a"] + 1}
func1({"a": 3})

View File

@ -1383,3 +1383,9 @@ test('TypedDict22', () => {
TestUtils.validateResults(analysisResults, 0);
});
test('TypedDictInline1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictInline1.py']);
TestUtils.validateResults(analysisResults, 8);
});