Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/pyright-internal/src/analyzer/dataClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 45 additions & 0 deletions packages/pyright-internal/src/tests/samples/dataclass4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Info · Optional note

📍 packages/pyright-internal/src/tests/samples/dataclass4.py:141
[unverified] Add the documented field(init=False) override case and pin its no-parameter constructor signature and inherited-member behavior. The current tests cover only field() with init=True, leaving this distinct branch unprotected.

[verified]

# 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")
2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/typeEvaluator4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ test('DataClass3', () => {
test('DataClass4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass4.py']);

TestUtils.validateResults(analysisResults, 6);
TestUtils.validateResults(analysisResults, 8);
});

test('DataClass5', () => {
Expand Down