Skip to content

Commit

Permalink
Extend the dataclass plugin to deal with callable properties (#10292)
Browse files Browse the repository at this point in the history
At runtime, the callable properties of dataclasses are handled in the
way one would expect: they are not passed a `self` argument.  Mypy,
however, just sees them as callable class attributes and generates
errors about missing arguments.  This is a special case of what is
discussed in #708.  I donʼt have a general solution for that problem,
but for dataclasses, I can fix it by automatically converting the
callable entries in a data class into (settable) properties.  That makes
them work properly via-a-vis the typechecker.
  • Loading branch information
aecay authored Apr 23, 2021
1 parent 8c82dac commit 3996f43
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 1 deletion.
25 changes: 24 additions & 1 deletion mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
add_method, _get_decorator_bool_argument, deserialize_and_fixup_type,
)
from mypy.typeops import map_type_from_supertype
from mypy.types import Type, Instance, NoneType, TypeVarDef, TypeVarType, get_proper_type
from mypy.types import (
Type, Instance, NoneType, TypeVarDef, TypeVarType, CallableType,
get_proper_type
)
from mypy.server.trigger import make_wildcard_trigger

# The set of decorators that generate dataclasses.
Expand Down Expand Up @@ -170,6 +173,8 @@ def transform(self) -> None:

if decorator_arguments['frozen']:
self._freeze(attributes)
else:
self._propertize_callables(attributes)

self.reset_init_only_vars(info, attributes)

Expand Down Expand Up @@ -353,6 +358,24 @@ def _freeze(self, attributes: List[DataclassAttribute]) -> None:
var._fullname = info.fullname + '.' + var.name
info.names[var.name] = SymbolTableNode(MDEF, var)

def _propertize_callables(self, attributes: List[DataclassAttribute]) -> None:
"""Converts all attributes with callable types to @property methods.
This avoids the typechecker getting confused and thinking that
`my_dataclass_instance.callable_attr(foo)` is going to receive a
`self` argument (it is not).
"""
info = self._ctx.cls.info
for attr in attributes:
if isinstance(get_proper_type(attr.type), CallableType):
var = attr.to_var()
var.info = info
var.is_property = True
var.is_settable_property = True
var._fullname = info.fullname + '.' + var.name
info.names[var.name] = SymbolTableNode(MDEF, var)


def dataclass_class_maker_callback(ctx: ClassDefContext) -> None:
"""Hooks into the class typechecking process to add support for dataclasses.
Expand Down
69 changes: 69 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -1108,3 +1108,72 @@ class B(A):
reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> __main__.B"

[builtins fixtures/property.pyi]

[case testDataclassCallableProperty]
# flags: --python-version 3.7
from dataclasses import dataclass
from typing import Callable

@dataclass
class A:
foo: Callable[[int], int]

def my_foo(x: int) -> int:
return x

a = A(foo=my_foo)
a.foo(1)
reveal_type(a.foo) # N: Revealed type is "def (builtins.int) -> builtins.int"
reveal_type(A.foo) # N: Revealed type is "def (builtins.int) -> builtins.int"
[typing fixtures/typing-medium.pyi]
[case testDataclassCallableAssignment]
# flags: --python-version 3.7
from dataclasses import dataclass
from typing import Callable

@dataclass
class A:
foo: Callable[[int], int]

def my_foo(x: int) -> int:
return x

a = A(foo=my_foo)

def another_foo(x: int) -> int:
return x + 1

a.foo = another_foo
[case testDataclassCallablePropertyWrongType]
# flags: --python-version 3.7
from dataclasses import dataclass
from typing import Callable

@dataclass
class A:
foo: Callable[[int], int]

def my_foo(x: int) -> str:
return "foo"

a = A(foo=my_foo) # E: Argument "foo" to "A" has incompatible type "Callable[[int], str]"; expected "Callable[[int], int]"
[typing fixtures/typing-medium.pyi]
[case testDataclassCallablePropertyWrongTypeAssignment]
# flags: --python-version 3.7
from dataclasses import dataclass
from typing import Callable

@dataclass
class A:
foo: Callable[[int], int]

def my_foo(x: int) -> int:
return x

a = A(foo=my_foo)

def another_foo(x: int) -> str:
return "foo"

a.foo = another_foo # E: Incompatible types in assignment (expression has type "Callable[[int], str]", variable has type "Callable[[int], int]")
[typing fixtures/typing-medium.pyi]

0 comments on commit 3996f43

Please sign in to comment.