Skip to content

Commit f2cac4a

Browse files
cdce8pwesleywright
andauthored
[1.1 backport] [dataclass_transform] detect transform spec changes in incremental mode (#14695) (#14768)
Adds support for triggering rechecking of downstream classes when `@dataclass_transform` is added or removed from a function/class, as well as when parameters to `dataclass_transform` are updated. These changes aren't propagated normally since they don't change the type signature of the `dataclass_transform` decorator. Also adds new a new `find-grained-dataclass-transform.test` file to test the new logic. (cherry picked from commit 29bcc7f) Co-authored-by: Wesley Collin Wright <[email protected]>
1 parent c03e979 commit f2cac4a

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

mypy/nodes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3907,7 +3907,7 @@ def serialize(self) -> JsonDict:
39073907
"order_default": self.order_default,
39083908
"kw_only_default": self.kw_only_default,
39093909
"frozen_only_default": self.frozen_default,
3910-
"field_specifiers": self.field_specifiers,
3910+
"field_specifiers": list(self.field_specifiers),
39113911
}
39123912

39133913
@classmethod
@@ -3917,7 +3917,7 @@ def deserialize(cls, data: JsonDict) -> DataclassTransformSpec:
39173917
order_default=data.get("order_default"),
39183918
kw_only_default=data.get("kw_only_default"),
39193919
frozen_default=data.get("frozen_default"),
3920-
field_specifiers=data.get("field_specifiers"),
3920+
field_specifiers=tuple(data.get("field_specifiers", [])),
39213921
)
39223922

39233923

mypy/server/astdiff.py

+8
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class level -- these are handled at attribute level (say, 'mod.Cls.method'
7373
TypeVarTupleExpr,
7474
Var,
7575
)
76+
from mypy.semanal_shared import find_dataclass_transform_spec
7677
from mypy.types import (
7778
AnyType,
7879
CallableType,
@@ -230,6 +231,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
230231
elif isinstance(node, OverloadedFuncDef) and node.impl:
231232
impl = node.impl.func if isinstance(node.impl, Decorator) else node.impl
232233
is_trivial_body = impl.is_trivial_body if impl else False
234+
dataclass_transform_spec = find_dataclass_transform_spec(node)
233235
return (
234236
"Func",
235237
common,
@@ -239,6 +241,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
239241
node.is_static,
240242
signature,
241243
is_trivial_body,
244+
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
242245
)
243246
elif isinstance(node, Var):
244247
return ("Var", common, snapshot_optional_type(node.type), node.is_final)
@@ -256,6 +259,10 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
256259
snapshot_definition(node.func, common),
257260
)
258261
elif isinstance(node, TypeInfo):
262+
dataclass_transform_spec = node.dataclass_transform_spec
263+
if dataclass_transform_spec is None:
264+
dataclass_transform_spec = find_dataclass_transform_spec(node)
265+
259266
attrs = (
260267
node.is_abstract,
261268
node.is_enum,
@@ -280,6 +287,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
280287
tuple(snapshot_type(tdef) for tdef in node.defn.type_vars),
281288
[snapshot_type(base) for base in node.bases],
282289
[snapshot_type(p) for p in node._promote],
290+
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
283291
)
284292
prefix = node.fullname
285293
symbol_table = snapshot_symbol_table(prefix, node.names)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
[case updateDataclassTransformParameterViaDecorator]
2+
# flags: --python-version 3.11
3+
from m import my_dataclass
4+
5+
@my_dataclass
6+
class Foo:
7+
x: int
8+
9+
foo = Foo(1)
10+
foo.x = 2
11+
12+
[file m.py]
13+
from typing import dataclass_transform
14+
15+
@dataclass_transform(frozen_default=False)
16+
def my_dataclass(cls): return cls
17+
18+
[file m.py.2]
19+
from typing import dataclass_transform
20+
21+
@dataclass_transform(frozen_default=True)
22+
def my_dataclass(cls): return cls
23+
24+
[typing fixtures/typing-full.pyi]
25+
[builtins fixtures/dataclasses.pyi]
26+
27+
[out]
28+
==
29+
main:9: error: Property "x" defined in "Foo" is read-only
30+
31+
[case updateDataclassTransformParameterViaParentClass]
32+
# flags: --python-version 3.11
33+
from m import Dataclass
34+
35+
class Foo(Dataclass):
36+
x: int
37+
38+
foo = Foo(1)
39+
foo.x = 2
40+
41+
[file m.py]
42+
from typing import dataclass_transform
43+
44+
@dataclass_transform(frozen_default=False)
45+
class Dataclass: ...
46+
47+
[file m.py.2]
48+
from typing import dataclass_transform
49+
50+
@dataclass_transform(frozen_default=True)
51+
class Dataclass: ...
52+
53+
[typing fixtures/typing-full.pyi]
54+
[builtins fixtures/dataclasses.pyi]
55+
56+
[out]
57+
==
58+
main:8: error: Property "x" defined in "Foo" is read-only
59+
60+
[case updateBaseClassToUseDataclassTransform]
61+
# flags: --python-version 3.11
62+
from m import A
63+
64+
class B(A):
65+
y: int
66+
67+
B(x=1, y=2)
68+
69+
[file m.py]
70+
class Dataclass: ...
71+
72+
class A(Dataclass):
73+
x: int
74+
75+
[file m.py.2]
76+
from typing import dataclass_transform
77+
78+
@dataclass_transform()
79+
class Dataclass: ...
80+
81+
class A(Dataclass):
82+
x: int
83+
84+
[typing fixtures/typing-full.pyi]
85+
[builtins fixtures/dataclasses.pyi]
86+
87+
[out]
88+
main:7: error: Unexpected keyword argument "x" for "B"
89+
builtins.pyi:12: note: "B" defined here
90+
main:7: error: Unexpected keyword argument "y" for "B"
91+
builtins.pyi:12: note: "B" defined here
92+
==

0 commit comments

Comments
 (0)