Skip to content
Merged
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
15 changes: 12 additions & 3 deletions packages/pyright-internal/src/analyzer/dataClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export function synthesizeDataClassMethods(
let variableTypeEvaluator: EntryTypeEvaluator | undefined;
let hasDefault = false;
let isDefaultFactory = false;
let isFieldSpecifierWithoutDefault = false;
let isKeywordOnly = ClassType.isDataClassKeywordOnly(classType) || sawKeywordOnlySeparator;
let defaultExpr: ExpressionNode | undefined;
let includeInInit = true;
Expand Down Expand Up @@ -437,6 +438,8 @@ export function synthesizeDataClassMethods(
defaultExpr = defaultFactoryArg.d.valueExpr;
}

isFieldSpecifierWithoutDefault = !hasDefault;

const aliasArg = statement.d.rightExpr.d.args.find((arg) => arg.d.name?.d.value === 'alias');
if (aliasArg) {
const valueType = evaluator.getTypeOfExpression(aliasArg.d.valueExpr).type;
Expand Down Expand Up @@ -576,9 +579,15 @@ export function synthesizeDataClassMethods(
if (insertIndex >= 0) {
const oldEntry = fullDataClassEntries[insertIndex];

// 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) {
// A bare annotation (`x: int`) inherits a parent field's default at
// runtime. An explicit `field()` with no default or default_factory
// does not, so the synthesized __init__ parameter is required.
if (
!dataClassEntry.hasDefault &&
oldEntry.hasDefault &&
oldEntry.includeInInit &&
!isFieldSpecifierWithoutDefault
) {
dataClassEntry.hasDefault = true;
dataClassEntry.defaultExpr = oldEntry.defaultExpr;
hasDefault = true;
Expand Down
38 changes: 38 additions & 0 deletions packages/pyright-internal/src/tests/samples/dataclass19.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This sample tests that overriding a dataclass field with field()
# and no default does not inherit the parent default.

from dataclasses import dataclass, field


@dataclass
class Base:
x: int = 1


@dataclass
class Foo(Base):
# This should not generate an error. field() with no default
# removes the inherited default at runtime.
x: int = field()


# This should generate an error because x is required.
Foo()

foo = Foo(2)
reveal_type(foo.x, expected_text="int")


@dataclass
class Base2:
a: int = 0


@dataclass
class BareOverride(Base2):
# This should generate an error because a bare annotation still
# inherits the parent default at runtime.
a: int

# This should generate an error because a still has a default.
b: str
6 changes: 6 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ test('DataClass18', () => {
TestUtils.validateResults(analysisResults, 0);
});

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

TestUtils.validateResults(analysisResults, 3);
});

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.

Warning · Non-blocking recommendation

Checking only the total diagnostic count allows an expected diagnostic to disappear while an unrelated one replaces it. Assert the diagnostic locations or categories so this regression test specifically proves the three intended outcomes.

[verified]


test('DataClassReplace1', () => {
const configOptions = new ConfigOptions(Uri.empty());

Expand Down
Loading