Skip to content

Commit

Permalink
[dataclass_transform] detect transform spec changes in incremental mo…
Browse files Browse the repository at this point in the history
…de (python#14695)

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.
  • Loading branch information
wesleywright authored Feb 22, 2023
1 parent 75aca65 commit 29bcc7f
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 2 deletions.
4 changes: 2 additions & 2 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3907,7 +3907,7 @@ def serialize(self) -> JsonDict:
"order_default": self.order_default,
"kw_only_default": self.kw_only_default,
"frozen_only_default": self.frozen_default,
"field_specifiers": self.field_specifiers,
"field_specifiers": list(self.field_specifiers),
}

@classmethod
Expand All @@ -3917,7 +3917,7 @@ def deserialize(cls, data: JsonDict) -> DataclassTransformSpec:
order_default=data.get("order_default"),
kw_only_default=data.get("kw_only_default"),
frozen_default=data.get("frozen_default"),
field_specifiers=data.get("field_specifiers"),
field_specifiers=tuple(data.get("field_specifiers", [])),
)


Expand Down
8 changes: 8 additions & 0 deletions mypy/server/astdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class level -- these are handled at attribute level (say, 'mod.Cls.method'
TypeVarTupleExpr,
Var,
)
from mypy.semanal_shared import find_dataclass_transform_spec
from mypy.types import (
AnyType,
CallableType,
Expand Down Expand Up @@ -230,6 +231,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
elif isinstance(node, OverloadedFuncDef) and node.impl:
impl = node.impl.func if isinstance(node.impl, Decorator) else node.impl
is_trivial_body = impl.is_trivial_body if impl else False
dataclass_transform_spec = find_dataclass_transform_spec(node)
return (
"Func",
common,
Expand All @@ -239,6 +241,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
node.is_static,
signature,
is_trivial_body,
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
)
elif isinstance(node, Var):
return ("Var", common, snapshot_optional_type(node.type), node.is_final)
Expand All @@ -256,6 +259,10 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
snapshot_definition(node.func, common),
)
elif isinstance(node, TypeInfo):
dataclass_transform_spec = node.dataclass_transform_spec
if dataclass_transform_spec is None:
dataclass_transform_spec = find_dataclass_transform_spec(node)

attrs = (
node.is_abstract,
node.is_enum,
Expand All @@ -280,6 +287,7 @@ def snapshot_definition(node: SymbolNode | None, common: SymbolSnapshot) -> Symb
tuple(snapshot_type(tdef) for tdef in node.defn.type_vars),
[snapshot_type(base) for base in node.bases],
[snapshot_type(p) for p in node._promote],
dataclass_transform_spec.serialize() if dataclass_transform_spec is not None else None,
)
prefix = node.fullname
symbol_table = snapshot_symbol_table(prefix, node.names)
Expand Down
92 changes: 92 additions & 0 deletions test-data/unit/fine-grained-dataclass-transform.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
[case updateDataclassTransformParameterViaDecorator]
# flags: --python-version 3.11
from m import my_dataclass

@my_dataclass
class Foo:
x: int

foo = Foo(1)
foo.x = 2

[file m.py]
from typing import dataclass_transform

@dataclass_transform(frozen_default=False)
def my_dataclass(cls): return cls

[file m.py.2]
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
def my_dataclass(cls): return cls

[typing fixtures/typing-full.pyi]
[builtins fixtures/dataclasses.pyi]

[out]
==
main:9: error: Property "x" defined in "Foo" is read-only

[case updateDataclassTransformParameterViaParentClass]
# flags: --python-version 3.11
from m import Dataclass

class Foo(Dataclass):
x: int

foo = Foo(1)
foo.x = 2

[file m.py]
from typing import dataclass_transform

@dataclass_transform(frozen_default=False)
class Dataclass: ...

[file m.py.2]
from typing import dataclass_transform

@dataclass_transform(frozen_default=True)
class Dataclass: ...

[typing fixtures/typing-full.pyi]
[builtins fixtures/dataclasses.pyi]

[out]
==
main:8: error: Property "x" defined in "Foo" is read-only

[case updateBaseClassToUseDataclassTransform]
# flags: --python-version 3.11
from m import A

class B(A):
y: int

B(x=1, y=2)

[file m.py]
class Dataclass: ...

class A(Dataclass):
x: int

[file m.py.2]
from typing import dataclass_transform

@dataclass_transform()
class Dataclass: ...

class A(Dataclass):
x: int

[typing fixtures/typing-full.pyi]
[builtins fixtures/dataclasses.pyi]

[out]
main:7: error: Unexpected keyword argument "x" for "B"
builtins.pyi:12: note: "B" defined here
main:7: error: Unexpected keyword argument "y" for "B"
builtins.pyi:12: note: "B" defined here
==

0 comments on commit 29bcc7f

Please sign in to comment.