diff --git a/packages/pyright-internal/src/analyzer/checker.ts b/packages/pyright-internal/src/analyzer/checker.ts index 460d13400..28d9c4332 100644 --- a/packages/pyright-internal/src/analyzer/checker.ts +++ b/packages/pyright-internal/src/analyzer/checker.ts @@ -6337,6 +6337,12 @@ export class Checker extends ParseTreeWalker { type: this._evaluator.printType(entry.valueType), }) ); + } else if (!baseTypedDictEntries.extraItems.isReadOnly && entry.isReadOnly) { + diag.addMessage( + LocAddendum.typedDictClosedFieldNotReadOnly().format({ + name, + }) + ); } else if (!baseTypedDictEntries.extraItems.isReadOnly && entry.isRequired) { diag.addMessage( LocAddendum.typedDictClosedFieldNotRequired().format({ diff --git a/packages/pyright-internal/src/localization/localize.ts b/packages/pyright-internal/src/localization/localize.ts index a2ca2056e..fa30d3966 100644 --- a/packages/pyright-internal/src/localization/localize.ts +++ b/packages/pyright-internal/src/localization/localize.ts @@ -1563,6 +1563,10 @@ export namespace Localizer { new ParameterizedString<{ name: string; type: string }>( getRawString('DiagnosticAddendum.typedDictClosedExtraTypeMismatch') ); + export const typedDictClosedFieldNotReadOnly = () => + new ParameterizedString<{ name: string }>( + getRawString('DiagnosticAddendum.typedDictClosedFieldNotReadOnly') + ); export const typedDictClosedFieldNotRequired = () => new ParameterizedString<{ name: string }>( getRawString('DiagnosticAddendum.typedDictClosedFieldNotRequired') 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 df9cbe69c..63f1bebf8 100644 --- a/packages/pyright-internal/src/localization/package.nls.en-us.json +++ b/packages/pyright-internal/src/localization/package.nls.en-us.json @@ -2114,6 +2114,10 @@ }, "typedDictClosedExtraNotAllowed": "Cannot add item \"{name}\"", "typedDictClosedExtraTypeMismatch": "Cannot add item \"{name}\" with type \"{type}\"", + "typedDictClosedFieldNotReadOnly": { + "message": "Cannot add item \"{name}\" because it must be ReadOnly", + "comment": "{Locked='ReadOnly'}" + }, "typedDictClosedFieldNotRequired": { "message": "Cannot add item \"{name}\" because it must be NotRequired", "comment": "{Locked='NotRequired'}" diff --git a/packages/pyright-internal/src/tests/samples/typedDictClosed3.py b/packages/pyright-internal/src/tests/samples/typedDictClosed3.py index 84a3b786e..235eec71a 100644 --- a/packages/pyright-internal/src/tests/samples/typedDictClosed3.py +++ b/packages/pyright-internal/src/tests/samples/typedDictClosed3.py @@ -125,15 +125,28 @@ class MovieWithYear(MovieBase): class ParentNonOpen5(TypedDict, closed=True): pass + # This should generate an error because a subclass of # a closed TypedDict cannot be open. class ChildNotClosed5(ParentNonOpen5, closed=False): pass + class ParentNonOpen6(TypedDict, extra_items=str): pass + # This should generate an error because a subclass of # a closed TypedDict cannot be open. class ChildNotClosed6(ParentNonOpen6, closed=False): pass + + +class ParentNonOpen7(TypedDict, extra_items=str): + pass + + +# This should generate an error because added fields +# cannot be ReadOnly. +class ChildNotClosed7(ParentNonOpen7): + a: NotRequired[ReadOnly[str]] diff --git a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts index d4ed2bd10..bf35f9c0e 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts @@ -360,7 +360,7 @@ test('TypedDictClosed2', () => { test('TypedDictClosed3', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictClosed3.py']); - TestUtils.validateResults(analysisResults, 12); + TestUtils.validateResults(analysisResults, 13); }); test('TypedDictClosed4', () => {