From d184ed00f1cb766d5fc3620d2dd58dd4381385a9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 10:28:22 +0100 Subject: [PATCH 01/10] [WIP] Basic implementation of dataclass_transform descriptor support --- mypy/plugins/dataclasses.py | 30 ++++++++++++++++--- test-data/unit/check-dataclass-transform.test | 29 ++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 16b1595e3cb8..24c307dd492d 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -36,6 +36,7 @@ TypeInfo, TypeVarExpr, Var, + FuncDef, ) from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface from mypy.plugins.common import ( @@ -58,6 +59,7 @@ Type, TypeOfAny, TypeVarType, + CallableType, get_proper_type, ) from mypy.typevars import fill_typevars @@ -535,9 +537,11 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: elif not isinstance(stmt.rvalue, TempNode): has_default = True - if not has_default: - # Make all non-default attributes implicit because they are de-facto set - # on self in the generated __init__(), not in the class body. + if not has_default and self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES: + # Make all non-default dataclass attributes implicit because they are de-facto + # set on self in the generated __init__(), not in the class body. On the other + # hand, we don't know how custom dataclass transforms initialize attributes, + # so we don't treat them as implicit. This is required to support descriptors. sym.implicit = True is_kw_only = kw_only @@ -578,6 +582,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: ) current_attr_names.add(lhs.name) + attr_type = _infer_dataclass_attr_type(sym) found_attrs[lhs.name] = DataclassAttribute( name=lhs.name, alias=alias, @@ -586,7 +591,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: has_default=has_default, line=stmt.line, column=stmt.column, - type=sym.type, + type=attr_type, info=cls.info, kw_only=is_kw_only, is_neither_frozen_nor_nonfrozen=_has_direct_dataclass_transform_metaclass( @@ -811,3 +816,20 @@ def _has_direct_dataclass_transform_metaclass(info: TypeInfo) -> bool: info.declared_metaclass is not None and info.declared_metaclass.type.dataclass_transform_spec is not None ) + + +def _infer_dataclass_attr_type(sym: SymbolTableNode) -> Type | None: + if sym.implicit: + return sym.type + t = get_proper_type(sym.type) + if not isinstance(t, Instance): + return sym.type + if "__set__" in t.type.names: + n = t.type.names["__set__"] + if isinstance(n.node, FuncDef): + setter_type = get_proper_type(n.type) + if not isinstance(setter_type, CallableType): + assert False, "unknown type" + if setter_type.arg_kinds == [ARG_POS, ARG_POS, ARG_POS]: + return setter_type.arg_types[2] + return sym.type diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 8d8e38997582..ca30b98317ac 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -807,3 +807,32 @@ reveal_type(bar.base) # N: Revealed type is "builtins.int" [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] + +[case testDataclassTransformDescriptors] +# flags: --python-version 3.11 + +from typing import dataclass_transform, overload, Any + +@dataclass_transform() +def my_dataclass(cls): ... + +class Desc: + @overload + def __get__(self, instance: None, owner: Any) -> Desc: ... + @overload + def __get__(self, instance: object, owner: Any) -> str: ... + def __get__(self, instance: object | None, owner: Any) -> Desc | str: ... + + def __set__(self, instance: Any, value: str) -> None: ... + +@my_dataclass +class C: + x: Desc + +C(x='x') +C(x=1) # E: Argument "x" to "C" has incompatible type "int"; expected "str" +reveal_type(C(x='x').x) # N: Revealed type is "builtins.str" +reveal_type(C.x) # N: Revealed type is "__main__.Desc" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dataclasses.pyi] From 381d6bc5134d5c32880f90a9c49612a82873a7cb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 11:39:54 +0100 Subject: [PATCH 02/10] Support generic descriptors --- mypy/plugins/dataclasses.py | 4 +- test-data/unit/check-dataclass-transform.test | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 24c307dd492d..2b25c7b3afa4 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -6,7 +6,7 @@ from typing_extensions import Final from mypy import errorcodes, message_registry -from mypy.expandtype import expand_type +from mypy.expandtype import expand_type, expand_type_by_instance from mypy.nodes import ( ARG_NAMED, ARG_NAMED_OPT, @@ -831,5 +831,5 @@ def _infer_dataclass_attr_type(sym: SymbolTableNode) -> Type | None: if not isinstance(setter_type, CallableType): assert False, "unknown type" if setter_type.arg_kinds == [ARG_POS, ARG_POS, ARG_POS]: - return setter_type.arg_types[2] + return expand_type_by_instance(setter_type.arg_types[2], t) return sym.type diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index ca30b98317ac..0752e9e756b3 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -808,7 +808,7 @@ reveal_type(bar.base) # N: Revealed type is "builtins.int" [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] -[case testDataclassTransformDescriptors] +[case testDataclassTransformSimpleDescriptor] # flags: --python-version 3.11 from typing import dataclass_transform, overload, Any @@ -828,11 +828,44 @@ class Desc: @my_dataclass class C: x: Desc + y: int + +C(x='x', y=1) +C(x=1, y=1) # E: Argument "x" to "C" has incompatible type "int"; expected "str" +reveal_type(C(x='x', y=1).x) # N: Revealed type is "builtins.str" +reveal_type(C(x='x', y=1).y) # N: Revealed type is "builtins.int" +reveal_type(C.x) # N: Revealed type is "__main__.Desc" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dataclasses.pyi] + +[case testDataclassTransformGenericDescriptor] +# flags: --python-version 3.11 + +from typing import dataclass_transform, overload, Any, TypeVar, Generic + +@dataclass_transform() +def my_dataclass(cls): ... + +T = TypeVar("T") + +class Desc(Generic[T]): + @overload + def __get__(self, instance: None, owner: Any) -> Desc[T]: ... + @overload + def __get__(self, instance: object, owner: Any) -> T: ... + def __get__(self, instance: object | None, owner: Any) -> Desc | T: ... + + def __set__(self, instance: Any, value: T) -> None: ... + +@my_dataclass +class C: + x: Desc[str] C(x='x') C(x=1) # E: Argument "x" to "C" has incompatible type "int"; expected "str" reveal_type(C(x='x').x) # N: Revealed type is "builtins.str" -reveal_type(C.x) # N: Revealed type is "__main__.Desc" +reveal_type(C.x) # N: Revealed type is "__main__.Desc[builtins.str]" [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] From fc6e11fc7da5607d5f5072e09d807e45ebb58f32 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 12:44:30 +0100 Subject: [PATCH 03/10] Test fronen dataclass --- test-data/unit/check-dataclass-transform.test | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 0752e9e756b3..2ab1757eb2c1 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -845,7 +845,7 @@ reveal_type(C.x) # N: Revealed type is "__main__.Desc" from typing import dataclass_transform, overload, Any, TypeVar, Generic @dataclass_transform() -def my_dataclass(cls): ... +def my_dataclass(frozen: bool = False): ... T = TypeVar("T") @@ -858,7 +858,7 @@ class Desc(Generic[T]): def __set__(self, instance: Any, value: T) -> None: ... -@my_dataclass +@my_dataclass() class C: x: Desc[str] @@ -867,5 +867,14 @@ C(x=1) # E: Argument "x" to "C" has incompatible type "int"; expected "str" reveal_type(C(x='x').x) # N: Revealed type is "builtins.str" reveal_type(C.x) # N: Revealed type is "__main__.Desc[builtins.str]" +@my_dataclass(frozen=True) +class F: + x: Desc[str] + +F(x='x') +F(x=1) # E: Argument "x" to "F" has incompatible type "int"; expected "str" +reveal_type(F(x='x').x) # N: Revealed type is "builtins.str" +reveal_type(F.x) # N: Revealed type is "__main__.Desc[builtins.str]" + [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] From a742c6c06de82e21eb51cd57dde29734c579357e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 12:53:45 +0100 Subject: [PATCH 04/10] Test more --- mypy/plugins/dataclasses.py | 2 +- test-data/unit/check-dataclass-transform.test | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 2b25c7b3afa4..a94688f97ad0 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -100,7 +100,7 @@ def __init__( self.has_default = has_default self.line = line self.column = column - self.type = type + self.type = type # Type as __init__ argument self.info = info self.kw_only = kw_only self.is_neither_frozen_nor_nonfrozen = is_neither_frozen_nor_nonfrozen diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 2ab1757eb2c1..9cbeb99b588c 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -867,9 +867,19 @@ C(x=1) # E: Argument "x" to "C" has incompatible type "int"; expected "str" reveal_type(C(x='x').x) # N: Revealed type is "builtins.str" reveal_type(C.x) # N: Revealed type is "__main__.Desc[builtins.str]" +@my_dataclass() +class D(C): + y: Desc[int] + +d = D(x='x', y=1) +reveal_type(d.x) # N: Revealed type is "builtins.str" +reveal_type(d.y) # N: Revealed type is "builtins.int" +reveal_type(D.x) # N: Revealed type is "__main__.Desc[builtins.str]" +reveal_type(D.y) # N: Revealed type is "__main__.Desc[builtins.int]" + @my_dataclass(frozen=True) class F: - x: Desc[str] + x: Desc[str] = Desc() F(x='x') F(x=1) # E: Argument "x" to "F" has incompatible type "int"; expected "str" @@ -878,3 +888,34 @@ reveal_type(F.x) # N: Revealed type is "__main__.Desc[builtins.str]" [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] + +[case testDataclassTransformDescriptorWithDifferentGetSetTypes] +# flags: --python-version 3.11 + +from typing import dataclass_transform, overload, Any + +@dataclass_transform() +def my_dataclass(cls): ... + +class Desc: + @overload + def __get__(self, instance: None, owner: Any) -> int: ... + @overload + def __get__(self, instance: object, owner: Any) -> str: ... + def __get__(self, instance, owner): ... + + def __set__(self, instance: Any, value: bytes) -> None: ... + +@my_dataclass +class C: + x: Desc + +c = C(x=b'x') +C(x=1) # E: Argument "x" to "C" has incompatible type "int"; expected "bytes" +reveal_type(c.x) # N: Revealed type is "builtins.str" +reveal_type(C.x) # N: Revealed type is "builtins.int" +c.x = b'x' +c.x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "bytes") + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dataclasses.pyi] From 1cdd5f3fd1260cd1813cc8c643683e2be7e73b34 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 12:55:28 +0100 Subject: [PATCH 05/10] Minor refactoring --- mypy/plugins/dataclasses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a94688f97ad0..d39becd6c2ac 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -582,7 +582,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: ) current_attr_names.add(lhs.name) - attr_type = _infer_dataclass_attr_type(sym) + init_type = _infer_dataclass_attr_init_type(sym) found_attrs[lhs.name] = DataclassAttribute( name=lhs.name, alias=alias, @@ -591,7 +591,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: has_default=has_default, line=stmt.line, column=stmt.column, - type=attr_type, + type=init_type, info=cls.info, kw_only=is_kw_only, is_neither_frozen_nor_nonfrozen=_has_direct_dataclass_transform_metaclass( @@ -818,7 +818,7 @@ def _has_direct_dataclass_transform_metaclass(info: TypeInfo) -> bool: ) -def _infer_dataclass_attr_type(sym: SymbolTableNode) -> Type | None: +def _infer_dataclass_attr_init_type(sym: SymbolTableNode) -> Type | None: if sym.implicit: return sym.type t = get_proper_type(sym.type) From 9ff3c6ce2a4d6a471d11ce6ab0729af2ce02a36f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 13:13:33 +0100 Subject: [PATCH 06/10] Add more error checking --- mypy/plugins/dataclasses.py | 49 ++++++++++++------- test-data/unit/check-dataclass-transform.test | 37 ++++++++++++++ 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index d39becd6c2ac..a8967eaaa5b1 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -582,7 +582,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: ) current_attr_names.add(lhs.name) - init_type = _infer_dataclass_attr_init_type(sym) + init_type = self._infer_dataclass_attr_init_type(sym, lhs.name) found_attrs[lhs.name] = DataclassAttribute( name=lhs.name, alias=alias, @@ -760,6 +760,36 @@ def _get_bool_arg(self, name: str, default: bool) -> bool: return require_bool_literal_argument(self._api, expression, name, default) return default + def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str) -> Type | None: + """Infer __init__ argument type for an attribute. + + In particular, possibly use the signature of __set__. + """ + default = sym.type + if sym.implicit: + return default + t = get_proper_type(sym.type) + if not isinstance(t, Instance): + return default + if "__set__" in t.type.names: + setter = t.type.names["__set__"] + if isinstance(setter.node, FuncDef): + setter_type = get_proper_type(setter.type) + if isinstance(setter_type, CallableType) and setter_type.arg_kinds == [ + ARG_POS, + ARG_POS, + ARG_POS, + ]: + return expand_type_by_instance(setter_type.arg_types[2], t) + else: + self._api.fail( + f'Unsupported signature for "__set__" in "{t.type.name}"', sym.node + ) + else: + self._api.fail(f'Unsupported "__set__" in "{t.type.name}"', sym.node) + + return default + def add_dataclass_tag(info: TypeInfo) -> None: # The value is ignored, only the existence matters. @@ -816,20 +846,3 @@ def _has_direct_dataclass_transform_metaclass(info: TypeInfo) -> bool: info.declared_metaclass is not None and info.declared_metaclass.type.dataclass_transform_spec is not None ) - - -def _infer_dataclass_attr_init_type(sym: SymbolTableNode) -> Type | None: - if sym.implicit: - return sym.type - t = get_proper_type(sym.type) - if not isinstance(t, Instance): - return sym.type - if "__set__" in t.type.names: - n = t.type.names["__set__"] - if isinstance(n.node, FuncDef): - setter_type = get_proper_type(n.type) - if not isinstance(setter_type, CallableType): - assert False, "unknown type" - if setter_type.arg_kinds == [ARG_POS, ARG_POS, ARG_POS]: - return expand_type_by_instance(setter_type.arg_types[2], t) - return sym.type diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 9cbeb99b588c..062a9033eab2 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -919,3 +919,40 @@ c.x = 1 # E: Incompatible types in assignment (expression has type "int", varia [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] + +[case testDataclassTransformUnsupportedDescriptors] +# flags: --python-version 3.11 + +from typing import dataclass_transform, overload, Any + +@dataclass_transform() +def my_dataclass(cls): ... + +class Desc: + @overload + def __get__(self, instance: None, owner: Any) -> int: ... + @overload + def __get__(self, instance: object, owner: Any) -> str: ... + def __get__(self, instance, owner): ... + + def __set__(*args, **kwargs): ... + +class Desc2: + @overload + def __get__(self, instance: None, owner: Any) -> int: ... + @overload + def __get__(self, instance: object, owner: Any) -> str: ... + def __get__(self, instance, owner): ... + + @overload + def __set__(self, instance: Any, value: bytes) -> None: ... + @overload + def __set__(self) -> None: ... + def __set__(self, *args, **kawrga) -> None: ... + +@my_dataclass +class C: + x: Desc # E: Unsupported signature for "__set__" in "Desc" + y: Desc2 # E: Unsupported "__set__" in "Desc2" +[typing fixtures/typing-full.pyi] +[builtins fixtures/dataclasses.pyi] From ae84e1775ff229775c2c7e16e7535d1277058411 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 13:19:59 +0100 Subject: [PATCH 07/10] Support inheritance --- mypy/plugins/dataclasses.py | 10 ++++-- test-data/unit/check-dataclass-transform.test | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a8967eaaa5b1..e90f05dfc546 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -771,10 +771,14 @@ def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str) -> Ty t = get_proper_type(sym.type) if not isinstance(t, Instance): return default - if "__set__" in t.type.names: - setter = t.type.names["__set__"] + setter = t.type.get("__set__") + if setter: if isinstance(setter.node, FuncDef): - setter_type = get_proper_type(setter.type) + super_info = t.type.get_containing_type_info("__set__") + assert super_info + setter_type = get_proper_type( + map_type_from_supertype(setter.type, t.type, super_info) + ) if isinstance(setter_type, CallableType) and setter_type.arg_kinds == [ ARG_POS, ARG_POS, diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 062a9033eab2..90b9a393290c 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -889,6 +889,40 @@ reveal_type(F.x) # N: Revealed type is "__main__.Desc[builtins.str]" [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] +[case testDataclassTransformGenericDescriptorWithInheritance] +# flags: --python-version 3.11 + +from typing import dataclass_transform, overload, Any, TypeVar, Generic + +@dataclass_transform() +def my_dataclass(cls): ... + +T = TypeVar("T") + +class Desc(Generic[T]): + @overload + def __get__(self, instance: None, owner: Any) -> Desc[T]: ... + @overload + def __get__(self, instance: object, owner: Any) -> T: ... + def __get__(self, instance: object | None, owner: Any) -> Desc | T: ... + + def __set__(self, instance: Any, value: T) -> None: ... + +class Desc2(Desc[str]): + pass + +@my_dataclass +class C: + x: Desc2 + +C(x='x') +C(x=1) # E: Argument "x" to "C" has incompatible type "int"; expected "str" +reveal_type(C(x='x').x) # N: Revealed type is "builtins.str" +reveal_type(C.x) # N: Revealed type is "__main__.Desc[builtins.str]" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dataclasses.pyi] + [case testDataclassTransformDescriptorWithDifferentGetSetTypes] # flags: --python-version 3.11 From 0df13eef24781fbe64e4b8f108275f1f6eb0aa7e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 13:27:52 +0100 Subject: [PATCH 08/10] Fixes --- mypy/plugins/dataclasses.py | 22 ++++++++----- test-data/unit/check-dataclass-transform.test | 33 ++++++++++++++++++- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index e90f05dfc546..60baddcfc0b1 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -23,6 +23,7 @@ Context, DataclassTransformSpec, Expression, + FuncDef, IfStmt, JsonDict, NameExpr, @@ -36,7 +37,6 @@ TypeInfo, TypeVarExpr, Var, - FuncDef, ) from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface from mypy.plugins.common import ( @@ -59,7 +59,6 @@ Type, TypeOfAny, TypeVarType, - CallableType, get_proper_type, ) from mypy.typevars import fill_typevars @@ -582,7 +581,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: ) current_attr_names.add(lhs.name) - init_type = self._infer_dataclass_attr_init_type(sym, lhs.name) + init_type = self._infer_dataclass_attr_init_type(sym, lhs.name, stmt) found_attrs[lhs.name] = DataclassAttribute( name=lhs.name, alias=alias, @@ -760,7 +759,9 @@ def _get_bool_arg(self, name: str, default: bool) -> bool: return require_bool_literal_argument(self._api, expression, name, default) return default - def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str) -> Type | None: + def _infer_dataclass_attr_init_type( + self, sym: SymbolTableNode, name: str, context: Context + ) -> Type | None: """Infer __init__ argument type for an attribute. In particular, possibly use the signature of __set__. @@ -776,9 +777,12 @@ def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str) -> Ty if isinstance(setter.node, FuncDef): super_info = t.type.get_containing_type_info("__set__") assert super_info - setter_type = get_proper_type( - map_type_from_supertype(setter.type, t.type, super_info) - ) + if setter.type: + setter_type = get_proper_type( + map_type_from_supertype(setter.type, t.type, super_info) + ) + else: + return AnyType(TypeOfAny.unannotated) if isinstance(setter_type, CallableType) and setter_type.arg_kinds == [ ARG_POS, ARG_POS, @@ -787,10 +791,10 @@ def _infer_dataclass_attr_init_type(self, sym: SymbolTableNode, name: str) -> Ty return expand_type_by_instance(setter_type.arg_types[2], t) else: self._api.fail( - f'Unsupported signature for "__set__" in "{t.type.name}"', sym.node + f'Unsupported signature for "__set__" in "{t.type.name}"', context ) else: - self._api.fail(f'Unsupported "__set__" in "{t.type.name}"', sym.node) + self._api.fail(f'Unsupported "__set__" in "{t.type.name}"', context) return default diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 90b9a393290c..0b696d36b9bd 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -839,6 +839,37 @@ reveal_type(C.x) # N: Revealed type is "__main__.Desc" [typing fixtures/typing-full.pyi] [builtins fixtures/dataclasses.pyi] +[case testDataclassTransformUnannotatedDescriptor] +# flags: --python-version 3.11 + +from typing import dataclass_transform, overload, Any + +@dataclass_transform() +def my_dataclass(cls): ... + +class Desc: + @overload + def __get__(self, instance: None, owner: Any) -> Desc: ... + @overload + def __get__(self, instance: object, owner: Any) -> str: ... + def __get__(self, instance: object | None, owner: Any) -> Desc | str: ... + + def __set__(*args, **kwargs): ... + +@my_dataclass +class C: + x: Desc + y: int + +C(x='x', y=1) +C(x=1, y=1) +reveal_type(C(x='x', y=1).x) # N: Revealed type is "builtins.str" +reveal_type(C(x='x', y=1).y) # N: Revealed type is "builtins.int" +reveal_type(C.x) # N: Revealed type is "__main__.Desc" + +[typing fixtures/typing-full.pyi] +[builtins fixtures/dataclasses.pyi] + [case testDataclassTransformGenericDescriptor] # flags: --python-version 3.11 @@ -969,7 +1000,7 @@ class Desc: def __get__(self, instance: object, owner: Any) -> str: ... def __get__(self, instance, owner): ... - def __set__(*args, **kwargs): ... + def __set__(*args, **kwargs) -> None: ... class Desc2: @overload From 7ac8d76cf794cdda0962209bc9bf6a98c88f2ed3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 13:36:32 +0100 Subject: [PATCH 09/10] Add comment --- mypy/plugins/dataclasses.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 60baddcfc0b1..39afecfa4c87 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -770,6 +770,11 @@ def _infer_dataclass_attr_init_type( if sym.implicit: return default t = get_proper_type(sym.type) + + # Perform a simple-minded inference from the signature of __set__, if present. + # We can't use mypy.checkmember here, since this plugin runs before type checking. + # We only support some basic scanerios here, which is hopefully sufficient for + # the vast majority of use cases. if not isinstance(t, Instance): return default setter = t.type.get("__set__") From 73a7cfb3c46ff672fbf4574389abbc22091acc32 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 4 Apr 2023 13:37:56 +0100 Subject: [PATCH 10/10] Update comment --- mypy/plugins/dataclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 39afecfa4c87..e84e1dbb9491 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -540,7 +540,8 @@ def collect_attributes(self) -> list[DataclassAttribute] | None: # Make all non-default dataclass attributes implicit because they are de-facto # set on self in the generated __init__(), not in the class body. On the other # hand, we don't know how custom dataclass transforms initialize attributes, - # so we don't treat them as implicit. This is required to support descriptors. + # so we don't treat them as implicit. This is required to support descriptors + # (https://github.com/python/mypy/issues/14868). sym.implicit = True is_kw_only = kw_only