From de64cae19b48d270a4b69de16d12c5c1c623071a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 12 Sep 2023 17:59:26 -0700 Subject: [PATCH] Fixed a bug that led to a false negative when using a non-TypedDict base class within a TypedDict class statement. This addresses #5937. --- .../src/analyzer/typeEvaluator.ts | 28 ++++++++++++++----- .../src/localization/localize.ts | 2 ++ .../src/localization/package.nls.en-us.json | 1 + .../src/tests/samples/typedDict1.py | 6 ++++ .../src/tests/typeEvaluator2.test.ts | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 81f870f74467..48aecb8549f5 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -15935,13 +15935,6 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions // a TypedDict, it is considered a TypedDict. if (ClassType.isBuiltIn(argType, 'TypedDict') || ClassType.isTypedDictClass(argType)) { classType.details.flags |= ClassTypeFlags.TypedDictClass; - } else if (ClassType.isTypedDictClass(classType) && !ClassType.isTypedDictClass(argType)) { - // Exempt Generic from this test. As of Python 3.11, generic TypedDict - // classes are supported. - if (!isInstantiableClass(argType) || !ClassType.isBuiltIn(argType, 'Generic')) { - // TypedDict classes must derive only from other TypedDict classes. - addError(Localizer.Diagnostic.typedDictBaseClass(), arg); - } } // Validate that the class isn't deriving from itself, creating a @@ -16385,6 +16378,27 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions // Synthesize TypedDict methods. if (ClassType.isTypedDictClass(classType)) { + // TypedDict classes must derive only from other TypedDict classes. + let foundInvalidBaseClass = false; + const diag = new DiagnosticAddendum(); + + classType.details.baseClasses.forEach((baseClass) => { + if ( + isClass(baseClass) && + !ClassType.isTypedDictClass(baseClass) && + !ClassType.isBuiltIn(baseClass, ['_TypedDict', 'Generic']) + ) { + foundInvalidBaseClass = true; + diag.addMessage( + Localizer.DiagnosticAddendum.typedDictBaseClass().format({ type: baseClass.details.name }) + ); + } + }); + + if (foundInvalidBaseClass) { + addError(Localizer.Diagnostic.typedDictBaseClass() + diag.getString(), node.name); + } + synthesizeTypedDictClassMethods( evaluatorInterface, node, diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index 14f4e4ec8dd6..e7dc2c609142 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -1353,6 +1353,8 @@ export namespace Localizer { new ParameterizedString<{ type: string; name: string }>( getRawString('DiagnosticAddendum.typeConstrainedTypeVar') ); + export const typedDictBaseClass = () => + new ParameterizedString<{ type: string }>(getRawString('DiagnosticAddendum.typedDictBaseClass')); export const typedDictFieldMissing = () => new ParameterizedString<{ name: string; type: string }>( getRawString('DiagnosticAddendum.typedDictFieldMissing') diff --git a/packages/pyright-internal/src/localization/package.nls.en-us.json b/packages/pyright-internal/src/localization/package.nls.en-us.json index b1d00ed27747..86726e78203a 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -680,6 +680,7 @@ "typeAssignmentMismatch": "Type \"{sourceType}\" cannot be assigned to type \"{destType}\"", "typeBound": "Type \"{sourceType}\" is incompatible with bound type \"{destType}\" for type variable \"{name}\"", "typeConstrainedTypeVar": "Type \"{type}\" is incompatible with constrained type variable \"{name}\"", + "typedDictBaseClass": "Class \"{type}\" is not a TypedDict", "typedDictFieldMissing": "\"{name}\" is missing from \"{type}\"", "typedDictFieldNotReadOnly": "\"{name}\" is not read-only in \"{type}\"", "typedDictFieldNotRequired": "\"{name}\" is not required in \"{type}\"", diff --git a/packages/pyright-internal/src/tests/samples/typedDict1.py b/packages/pyright-internal/src/tests/samples/typedDict1.py index f7ff684a13bf..9ddc356fd9a7 100644 --- a/packages/pyright-internal/src/tests/samples/typedDict1.py +++ b/packages/pyright-internal/src/tests/samples/typedDict1.py @@ -62,3 +62,9 @@ class NotATD: # base classes shouldn't be allowed for TD classes. class TD6(TD3, NotATD): pass + + +# This should generate an error because non-TypeDict +# base classes shouldn't be allowed for TD classes. +class TD7(NotATD, TypedDict): + pass diff --git a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts index 4d07c4d29c28..5f011091aed0 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts @@ -1320,7 +1320,7 @@ test('Protocol44', () => { test('TypedDict1', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDict1.py']); - TestUtils.validateResults(analysisResults, 6); + TestUtils.validateResults(analysisResults, 7); }); test('TypedDict2', () => {