Skip to content

Commit

Permalink
Added experimental support for draft PEP 764: Inlined typed dictionar…
Browse files Browse the repository at this point in the history
…ies. (#9350)
  • Loading branch information
erictraut authored Oct 29, 2024
1 parent 84b77b0 commit d0b4eb5
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 12 deletions.
3 changes: 3 additions & 0 deletions packages/pyright-internal/src/analyzer/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export interface DeclarationBase {
// The declaration is within an except clause of a try
// statement. We may want to ignore such declarations.
isInExceptSuite: boolean;

// This declaration is within an inlined TypedDict definition.
isInInlinedTypedDict?: boolean;
}

export interface IntrinsicDeclaration extends DeclarationBase {
Expand Down
73 changes: 61 additions & 12 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ import {
assignToTypedDict,
assignTypedDictToTypedDict,
createTypedDictType,
createTypedDictTypeInlined,
getTypedDictDictEquivalent,
getTypedDictMappingEquivalent,
getTypedDictMembersForClass,
Expand Down Expand Up @@ -368,6 +369,7 @@ interface GetTypeArgsOptions {
hasCustomClassGetItem?: boolean;
isFinalAnnotation?: boolean;
isClassVarAnnotation?: boolean;
supportsTypedDictTypeArg?: boolean;
}

interface MatchArgsToParamsResult {
Expand Down Expand Up @@ -654,6 +656,7 @@ export function createTypeEvaluator(
let strClass: Type | undefined;
let dictClass: Type | undefined;
let moduleTypeClass: Type | undefined;
let typedDictClass: Type | undefined;
let typedDictPrivateClass: Type | undefined;
let supportsKeysAndGetItemClass: Type | undefined;
let mappingClass: Type | undefined;
Expand Down Expand Up @@ -1022,6 +1025,7 @@ export function createTypeEvaluator(
strClass = getBuiltInType(node, 'str');
dictClass = getBuiltInType(node, 'dict');
moduleTypeClass = getTypingType(node, 'ModuleType');
typedDictClass = getTypingType(node, 'TypedDict');
typedDictPrivateClass = getTypingType(node, '_TypedDict');
awaitableClass = getTypingType(node, 'Awaitable');
mappingClass = getTypingType(node, 'Mapping');
Expand Down Expand Up @@ -7523,11 +7527,17 @@ export function createTypeEvaluator(
const isClassVarAnnotation =
isInstantiableClass(concreteSubtype) && ClassType.isBuiltIn(concreteSubtype, 'ClassVar');

// This feature is currently experimental.
const supportsTypedDictTypeArg =
AnalyzerNodeInfo.getFileInfo(node).diagnosticRuleSet.enableExperimentalFeatures &&
ClassType.isBuiltIn(concreteSubtype, 'TypedDict');

let typeArgs = getTypeArgs(node, flags, {
isAnnotatedClass,
hasCustomClassGetItem: hasCustomClassGetItem || !isGenericClass,
isFinalAnnotation,
isClassVarAnnotation,
supportsTypedDictTypeArg,
});

if (!isAnnotatedClass) {
Expand Down Expand Up @@ -8014,7 +8024,7 @@ export function createTypeEvaluator(
node: expr,
};
} else {
typeResult = getTypeArg(expr, adjFlags);
typeResult = getTypeArg(expr, adjFlags, !!options?.supportsTypedDictTypeArg && argIndex === 0);
}

return typeResult;
Expand Down Expand Up @@ -8120,7 +8130,7 @@ export function createTypeEvaluator(
return undefined;
}

function getTypeArg(node: ExpressionNode, flags: EvalFlags): TypeResultWithNode {
function getTypeArg(node: ExpressionNode, flags: EvalFlags, supportsDictExpression: boolean): TypeResultWithNode {
let typeResult: TypeResultWithNode;

let adjustedFlags =
Expand All @@ -8142,6 +8152,18 @@ export function createTypeEvaluator(

// Set the node's type so it isn't reevaluated later.
setTypeResultForNode(node, { type: UnknownType.create() });
} else if (node.nodeType === ParseNodeType.Dictionary && supportsDictExpression) {
const inlinedTypeDict =
typedDictClass && isInstantiableClass(typedDictClass)
? createTypedDictTypeInlined(evaluatorInterface, node, typedDictClass)
: undefined;
const keyTypeFallback = strClass && isInstantiableClass(strClass) ? strClass : UnknownType.create();

typeResult = {
type: keyTypeFallback,
inlinedTypeDict,
node,
};
} else {
typeResult = { ...getTypeOfExpression(node, adjustedFlags), node };

Expand Down Expand Up @@ -20577,11 +20599,17 @@ export function createTypeEvaluator(

case 'TypedDict': {
if ((flags & (EvalFlags.NoNonTypeSpecialForms | EvalFlags.TypeExpression)) !== 0) {
addDiagnostic(
DiagnosticRule.reportInvalidTypeForm,
LocMessage.typedDictNotAllowed(),
errorNode
);
const isInlinedTypedDict =
AnalyzerNodeInfo.getFileInfo(errorNode).diagnosticRuleSet.enableExperimentalFeatures &&
!!typeArgs;

if (!isInlinedTypedDict) {
addDiagnostic(
DiagnosticRule.reportInvalidTypeForm,
LocMessage.typedDictNotAllowed(),
errorNode
);
}
}
isValidTypeForm = false;
break;
Expand Down Expand Up @@ -20746,7 +20774,22 @@ export function createTypeEvaluator(
minTypeArgCount = firstDefaultParamIndex;
}

if (typeArgCount > typeParams.length) {
// Classes that accept inlined type dict type args allow only one.
if (typeArgs.length > 0 && typeArgs[0].inlinedTypeDict) {
if (typeArgs.length > 1) {
addDiagnostic(
DiagnosticRule.reportInvalidTypeArguments,
LocMessage.typeArgsTooMany().format({
name: classType.priv.aliasName || classType.shared.name,
expected: 1,
received: typeArgCount,
}),
typeArgs[1].node
);
}

return { type: typeArgs[0].inlinedTypeDict };
} else if (typeArgCount > typeParams.length) {
if (!ClassType.isPartiallyEvaluated(classType) && !ClassType.isTupleClass(classType)) {
if (typeParams.length === 0) {
isValidTypeForm = false;
Expand Down Expand Up @@ -21862,12 +21905,18 @@ export function createTypeEvaluator(
declaration.node.parent?.nodeType === ParseNodeType.MemberAccess
? declaration.node.parent
: declaration.node;
const allowClassVar = ParseTreeUtils.isClassVarAllowedForAssignmentTarget(declNode);
const allowFinal = ParseTreeUtils.isFinalAllowedForAssignmentTarget(declNode);
const allowRequired =
ParseTreeUtils.isRequiredAllowedForAssignmentTarget(declNode) ||
!!declaration.isInInlinedTypedDict;

declaredType = getTypeOfAnnotation(typeAnnotationNode, {
varTypeAnnotation: true,
allowClassVar: ParseTreeUtils.isClassVarAllowedForAssignmentTarget(declNode),
allowFinal: ParseTreeUtils.isFinalAllowedForAssignmentTarget(declNode),
allowRequired: ParseTreeUtils.isRequiredAllowedForAssignmentTarget(declNode),
allowReadOnly: ParseTreeUtils.isRequiredAllowedForAssignmentTarget(declNode),
allowClassVar,
allowFinal,
allowRequired,
allowReadOnly: allowRequired,
enforceClassTypeVarScope: declaration.isDefinedByMemberAccess,
});
}
Expand Down
3 changes: 3 additions & 0 deletions packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ export interface TypeResult<T extends Type = Type> {
// Type consistency errors detected when evaluating this type.
typeErrors?: boolean | undefined;

// For inlined TypedDict definitions.
inlinedTypeDict?: ClassType;

// Used for getTypeOfBoundMember to indicate that class
// that declares the member.
classType?: ClassType | UnknownType | AnyType;
Expand Down
29 changes: 29 additions & 0 deletions packages/pyright-internal/src/analyzer/typedDicts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,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.fileUri,
ClassTypeFlags.TypedDictClass,
ParseTreeUtils.getTypeSourceId(dictNode),
/* declaredMetaclass */ undefined,
typedDictClass.shared.effectiveMetaclass
);
classType.shared.baseClasses.push(typedDictClass);
computeMroLinearization(classType);

getTypedDictFieldsFromDictSyntax(evaluator, dictNode, ClassType.getSymbolTable(classType), /* isInline */ true);
synthesizeTypedDictClassMethods(evaluator, dictNode, classType);

return classType;
}

export function synthesizeTypedDictClassMethods(
evaluator: TypeEvaluator,
node: ClassNode | ExpressionNode,
Expand Down Expand Up @@ -964,6 +992,7 @@ function getTypedDictFieldsFromDictSyntax(
range: convertOffsetsToRange(entry.d.keyExpr.start, TextRange.getEnd(entry.d.keyExpr), fileInfo.lines),
moduleName: fileInfo.moduleName,
isInExceptSuite: false,
isInInlinedTypedDict: true,
};
newSymbol.addDeclaration(declaration);

Expand Down
40 changes: 40 additions & 0 deletions packages/pyright-internal/src/tests/samples/typedDictInline1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# This sample tests support for inlined TypedDict definitions.

from typing import NotRequired, ReadOnly, Required, TypedDict


td1: TypedDict[{"a": int, "b": str}] = {"a": 0, "b": ""}

td2: TypedDict[{"a": TypedDict[{"b": int}]}] = {"a": {"b": 0}}

td3: TypedDict[{"a": "list[float]"}] = {"a": [3]}

td4: TypedDict[
{"a": NotRequired[int], "b": Required[int], "c": NotRequired[ReadOnly[int]]}
] = {"b": 3}

# This should generate an error because dictionary comprehensions
# are not allowed.
err1: TypedDict[{"a": int for _ in range(1)}]

# This should generate an error because unpacked dictionary
# entries are not allowed.
err2: TypedDict[{**{"a": int}}]

# This should generate an error because an extra type argument is provided.
err3: TypedDict[{"a": int}, str]

# This should generate an error because TypedDict cannot be used without
# a subscript in this context.
err4: TypedDict

# This should generate an error because a dict expression is not a
# valid type expression by itself.
err5: TypedDict[{"a": {"b": int}}] = {"a": {"b": 0}}


def func1(val: TypedDict[{"a": int}]) -> TypedDict[{"a": int}]:
return {"a": val["a"] + 1}


func1({"a": 3})
8 changes: 8 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator7.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,14 @@ test('TypedDict24', () => {
TestUtils.validateResults(analysisResults, 1);
});

test('TypedDictInline1', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.enableExperimentalFeatures = true;

const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictInline1.py'], configOptions);
TestUtils.validateResults(analysisResults, 6);
});

test('ClassVar1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['classVar1.py']);

Expand Down

0 comments on commit d0b4eb5

Please sign in to comment.