diff --git a/packages/pyright-internal/src/analyzer/dataClasses.ts b/packages/pyright-internal/src/analyzer/dataClasses.ts index 7367def7af1c..2555de3a5f91 100644 --- a/packages/pyright-internal/src/analyzer/dataClasses.ts +++ b/packages/pyright-internal/src/analyzer/dataClasses.ts @@ -331,6 +331,7 @@ export function synthesizeDataClassMethods( let aliasName: string | undefined; let variableTypeEvaluator: EntryTypeEvaluator | undefined; let hasDefault = false; + let hasAssignedValue = false; let isDefaultFactory = false; let isKeywordOnly = ClassType.isDataClassKeywordOnly(classType) || sawKeywordOnlySeparator; let defaultExpr: ExpressionNode | undefined; @@ -362,6 +363,7 @@ export function synthesizeDataClassMethods( } hasDefault = true; + hasAssignedValue = true; defaultExpr = statement.d.rightExpr; // If the RHS of the assignment is assigning a field instance where the @@ -578,7 +580,16 @@ export function synthesizeDataClassMethods( // While this isn't documented behavior, it appears that the dataclass implementation // causes overridden variables to "inherit" default values from parent classes. - if (!dataClassEntry.hasDefault && oldEntry.hasDefault && oldEntry.includeInInit) { + // This applies only when the override is a bare annotation (`x: int`). If the + // override assigns a field specifier that supplies no default (`x: int = field()`), + // the runtime replaces the entry outright, so the inherited default is dropped and + // the parameter becomes required. + if ( + !dataClassEntry.hasDefault && + !hasAssignedValue && + oldEntry.hasDefault && + oldEntry.includeInInit + ) { dataClassEntry.hasDefault = true; dataClassEntry.defaultExpr = oldEntry.defaultExpr; hasDefault = true; diff --git a/packages/pyright-internal/src/tests/samples/dataclass4.py b/packages/pyright-internal/src/tests/samples/dataclass4.py index 31640b1ff8a0..cd6f39dc7f93 100644 --- a/packages/pyright-internal/src/tests/samples/dataclass4.py +++ b/packages/pyright-internal/src/tests/samples/dataclass4.py @@ -108,3 +108,48 @@ class DC10: class DC11(DC10): a: str = field() b: bool = field() + + +@dataclass +class DC12: + a: int = 0 + + +@dataclass +class DC13(DC12): + # Unlike a bare annotation, an assigned field specifier that supplies no + # default replaces the inherited entry rather than inheriting its default, + # so "a" becomes a required parameter. + a: int = field() + + +reveal_type(DC13.__init__, expected_text="(self: DC13, a: int) -> None") + +# This should generate an error because "a" no longer has an +# inherited default value, so it must be provided. +DC13() + + +@dataclass +class DC14: + a: int = 0 + b: int = 1 + + +@dataclass +class DC15(DC14): + # This should generate an error because "b" drops its inherited default + # and would then follow "a", which still has one. + b: int = field() + + +@dataclass +class DC16(DC12): + # An assigned field specifier with init=False removes the parameter from + # __init__ entirely rather than dropping the inherited default, so the + # attribute keeps the base class value. + a: int = field(init=False) + + +reveal_type(DC16.__init__, expected_text="(self: DC16) -> None") +reveal_type(DC16().a, expected_text="int") diff --git a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts index 47eed9ce1b23..382b4026aee9 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts @@ -326,7 +326,7 @@ test('DataClass3', () => { test('DataClass4', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass4.py']); - TestUtils.validateResults(analysisResults, 6); + TestUtils.validateResults(analysisResults, 8); }); test('DataClass5', () => {