mirror of
https://github.com/microsoft/pyright.git
synced 2024-09-11 07:55:56 +03:00
Added experimental support for inlined TypedDict definitions using the dict[{'a': int}]
syntax.
This commit is contained in:
parent
a8753c745f
commit
8e56215661
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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})
|
@ -1383,3 +1383,9 @@ test('TypedDict22', () => {
|
||||
|
||||
TestUtils.validateResults(analysisResults, 0);
|
||||
});
|
||||
|
||||
test('TypedDictInline1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictInline1.py']);
|
||||
|
||||
TestUtils.validateResults(analysisResults, 8);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user