Added support for auto-synthesized __replace__ method in dataclass … (#8514)

* Added support for auto-synthesized `__replace__` method in dataclass and namedtuple classes, a new feature in Python 3.13. This addresses #8300.

* Fixed broken test on Linux due to file system case sensitivity.
This commit is contained in:
Eric Traut 2024-07-22 22:31:18 -07:00 committed by GitHub
parent 39757e4519
commit 77684ec7fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 6 deletions

View File

@ -11,6 +11,7 @@
import { assert } from '../common/debug';
import { DiagnosticAddendum } from '../common/diagnostic';
import { DiagnosticRule } from '../common/diagnosticRules';
import { pythonVersion3_13 } from '../common/pythonVersion';
import { LocMessage } from '../localization/localize';
import {
ArgumentCategory,
@ -108,18 +109,23 @@ export function synthesizeDataClassMethods(
}
newType.shared.declaredReturnType = convertToInstance(classTypeVar);
const selfParam = FunctionParam.create(
ParameterCategory.Simple,
synthesizeTypeVarForSelfCls(classType, /* isClsParam */ false),
FunctionParamFlags.TypeDeclared,
'self'
);
const selfType = synthesizeTypeVarForSelfCls(classType, /* isClsParam */ false);
const selfParam = FunctionParam.create(ParameterCategory.Simple, selfType, FunctionParamFlags.TypeDeclared, 'self');
FunctionType.addParameter(initType, selfParam);
if (isNamedTuple) {
FunctionType.addDefaultParameters(initType);
}
initType.shared.declaredReturnType = evaluator.getNoneType();
// For Python 3.13 and newer, synthesize a __replace__ method.
let replaceType: FunctionType | undefined;
if (AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.pythonVersion >= pythonVersion3_13) {
replaceType = FunctionType.createSynthesizedInstance('__replace__');
FunctionType.addParameter(replaceType, selfParam);
FunctionType.addKeywordOnlyParameterSeparator(replaceType);
replaceType.shared.declaredReturnType = selfType;
}
// Maintain a list of all dataclass entries (including
// those from inherited classes) plus a list of only those
// entries added by this class.
@ -132,6 +138,10 @@ export function synthesizeDataClassMethods(
// safely determine the parameter list, so we'll accept any parameters
// to avoid a false positive.
FunctionType.addDefaultParameters(initType);
if (replaceType) {
FunctionType.addDefaultParameters(replaceType);
}
}
// Add field-based parameters to either the __new__ or __init__ method
@ -536,6 +546,14 @@ export function synthesizeDataClassMethods(
} else {
FunctionType.addParameter(constructorType, functionParam);
}
if (replaceType) {
const paramWithDefault = {
...functionParam,
defaultType: AnyType.create(/* isEllipsis */ true),
};
FunctionType.addParameter(replaceType, paramWithDefault);
}
}
});
@ -549,6 +567,10 @@ export function synthesizeDataClassMethods(
symbolTable.set('__init__', Symbol.createWithType(SymbolFlags.ClassMember, initType));
symbolTable.set('__new__', Symbol.createWithType(SymbolFlags.ClassMember, newType));
if (replaceType) {
symbolTable.set('__replace__', Symbol.createWithType(SymbolFlags.ClassMember, replaceType));
}
}
// Synthesize the __match_args__ class variable if it doesn't exist.

View File

@ -0,0 +1,45 @@
# This sample tests the synthesis of a "__replace__" method for dataclass
# classes in Python 3.13 and newer.
from dataclasses import dataclass
from typing import NamedTuple
@dataclass
class DC1:
a: int
b: str
c: str = ""
dc1: DC1 = DC1(1, "")
dc1_clone = dc1.__replace__(b="", a=1, c="")
reveal_type(dc1_clone, expected_text="DC1")
dc1.__replace__(c="")
dc1.__replace__(b="2")
# This should generate an error.
dc1.__replace__(b=2)
# This should generate an error.
dc1.__replace__(d="")
class NT1(NamedTuple):
a: int
b: str
c: str = ""
nt1 = NT1(1, "")
nt1_clone = nt1.__replace__(c="")
reveal_type(nt1_clone, expected_text="NT1")
# This should generate an error.
nt1.__replace__(b=2)
# This should generate an error.
nt1.__replace__(d="")

View File

@ -13,6 +13,7 @@ import {
pythonVersion3_10,
pythonVersion3_11,
pythonVersion3_12,
pythonVersion3_13,
pythonVersion3_7,
pythonVersion3_8,
pythonVersion3_9,
@ -365,6 +366,17 @@ test('DataClass17', () => {
TestUtils.validateResults(analysisResults, 5);
});
test('DataClassReplace1', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['dataclassReplace1.py'], configOptions);
TestUtils.validateResults(analysisResults1, 10);
configOptions.defaultPythonVersion = pythonVersion3_13;
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['dataclassReplace1.py'], configOptions);
TestUtils.validateResults(analysisResults2, 4);
});
test('DataClassFrozen1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclassFrozen1.py']);