Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions mypy/plugins/common.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from typing import List, Optional
from typing import List, Optional, Union

from mypy.nodes import (
ARG_POS, MDEF, Argument, Block, CallExpr, Expression, SYMBOL_FUNCBASE_TYPES,
FuncDef, PassStmt, RefExpr, SymbolTableNode, Var
FuncDef, PassStmt, RefExpr, SymbolTableNode, Var, JsonDict,
)
from mypy.plugin import ClassDefContext
from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface
from mypy.semanal import set_callable_name
from mypy.types import CallableType, Overloaded, Type, TypeVarDef, get_proper_type
from mypy.types import (
CallableType, Overloaded, Type, TypeVarDef, deserialize_type, get_proper_type,
)
from mypy.typevars import fill_typevars
from mypy.util import get_unique_redefinition_name
from mypy.typeops import try_getting_str_literals # noqa: F401 # Part of public API
from mypy.fixup import TypeFixer


def _get_decorator_bool_argument(
Expand Down Expand Up @@ -128,3 +131,11 @@ def add_method(

info.names[name] = SymbolTableNode(MDEF, func, plugin_generated=True)
info.defn.defs.body.append(func)


def deserialize_and_fixup_type(
data: Union[str, JsonDict], api: SemanticAnalyzerPluginInterface
) -> Type:
typ = deserialize_type(data)
typ.accept(TypeFixer(api.modules, allow_missing=False))
return typ
72 changes: 27 additions & 45 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

from mypy.nodes import (
ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, CallExpr,
Context, Expression, FuncDef, JsonDict, NameExpr, RefExpr,
Context, Expression, JsonDict, NameExpr, RefExpr,
SymbolTableNode, TempNode, TypeInfo, Var, TypeVarExpr, PlaceholderNode
)
from mypy.plugin import ClassDefContext
from mypy.plugins.common import add_method, _get_decorator_bool_argument
from mypy.types import Instance, NoneType, TypeVarDef, TypeVarType, get_proper_type
from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface
from mypy.plugins.common import (
add_method, _get_decorator_bool_argument, deserialize_and_fixup_type,
)
from mypy.types import Type, Instance, NoneType, TypeVarDef, TypeVarType, get_proper_type
from mypy.server.trigger import make_wildcard_trigger

# The set of decorators that generate dataclasses.
Expand All @@ -31,38 +33,46 @@ def __init__(
has_default: bool,
line: int,
column: int,
type: Optional[Type],
) -> None:
self.name = name
self.is_in_init = is_in_init
self.is_init_var = is_init_var
self.has_default = has_default
self.line = line
self.column = column
self.type = type

def to_argument(self, info: TypeInfo) -> Argument:
def to_argument(self) -> Argument:
return Argument(
variable=self.to_var(info),
type_annotation=info[self.name].type,
variable=self.to_var(),
type_annotation=self.type,
initializer=None,
kind=ARG_OPT if self.has_default else ARG_POS,
)

def to_var(self, info: TypeInfo) -> Var:
return Var(self.name, info[self.name].type)
def to_var(self) -> Var:
return Var(self.name, self.type)

def serialize(self) -> JsonDict:
assert self.type
return {
'name': self.name,
'is_in_init': self.is_in_init,
'is_init_var': self.is_init_var,
'has_default': self.has_default,
'line': self.line,
'column': self.column,
'type': self.type.serialize(),
}

@classmethod
def deserialize(cls, info: TypeInfo, data: JsonDict) -> 'DataclassAttribute':
return cls(**data)
def deserialize(
cls, info: TypeInfo, data: JsonDict, api: SemanticAnalyzerPluginInterface
) -> 'DataclassAttribute':
data = data.copy()
typ = deserialize_and_fixup_type(data.pop('type'), api)
return cls(type=typ, **data)


class DataclassTransformer:
Expand All @@ -81,12 +91,7 @@ def transform(self) -> None:
# Some definitions are not ready, defer() should be already called.
return
for attr in attributes:
node = info.get(attr.name)
if node is None:
# Nodes of superclass InitVars not used in __init__ cannot be reached.
assert attr.is_init_var and not attr.is_in_init
continue
if node.type is None:
if attr.type is None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the some attribute has no type? We seem to handle this case during deserialization, so maybe it can happen here as well. Could this result in always deferring for some attribute? If not, maybe we should require that the serialized type is not None?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only consider something as being an attribute if it has a type annotation, so the type missing is (should be) always due to resolution issues that can be fixed by deferring.

I think we can only serialize things with non-None types, yeah.

ctx.api.defer()
return
decorator_arguments = {
Expand All @@ -106,7 +111,7 @@ def transform(self) -> None:
add_method(
ctx,
'__init__',
args=[attr.to_argument(info) for attr in attributes if attr.is_in_init],
args=[attr.to_argument() for attr in attributes if attr.is_in_init],
return_type=NoneType(),
)

Expand Down Expand Up @@ -191,7 +196,7 @@ def reset_init_only_vars(self, info: TypeInfo, attributes: List[DataclassAttribu
del info.names[attr.name]
else:
# Nodes of superclass InitVars not used in __init__ cannot be reached.
assert attr.is_init_var and not attr.is_in_init
assert attr.is_init_var
for stmt in info.defn.defs.body:
if isinstance(stmt, AssignmentStmt) and stmt.unanalyzed_type:
lvalue = stmt.lvalues[0]
Expand Down Expand Up @@ -282,6 +287,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
has_default=has_default,
line=stmt.line,
column=stmt.column,
type=sym.type,
))

# Next, collect attributes belonging to any class in the MRO
Expand All @@ -301,23 +307,7 @@ def collect_attributes(self) -> Optional[List[DataclassAttribute]]:
for data in info.metadata['dataclass']['attributes']:
name = data['name'] # type: str
if name not in known_attrs:
attr = DataclassAttribute.deserialize(info, data)
if attr.is_init_var:
# InitVars are removed from classes so, in order for them to be inherited
# properly, we need to re-inject them into subclasses' sym tables here.
# To do that, we look 'em up from the parents' __init__. These variables
# are subsequently removed from the sym table at the end of
# DataclassTransformer.transform.
superclass_init = info.get_method('__init__')
if isinstance(superclass_init, FuncDef):
attr_node = _get_arg_from_init(superclass_init, attr.name)
if attr_node is None:
# Continue the loop: we will look it up in the next MRO entry.
# Don't add it to the known or super attrs because we don't know
# anything about it yet
continue
else:
cls.info.names[attr.name] = attr_node
attr = DataclassAttribute.deserialize(info, data, ctx.api)
known_attrs.add(name)
super_attrs.append(attr)
elif all_attrs:
Expand Down Expand Up @@ -365,21 +355,13 @@ def _freeze(self, attributes: List[DataclassAttribute]) -> None:
assert isinstance(var, Var)
var.is_property = True
else:
var = attr.to_var(info)
var = attr.to_var()
var.info = info
var.is_property = True
var._fullname = info.fullname + '.' + var.name
info.names[var.name] = SymbolTableNode(MDEF, var)


def _get_arg_from_init(init_method: FuncDef, attr_name: str) -> Optional[SymbolTableNode]:
"""Given an init method and an attribute name, find the Var in the init method's args."""
for arg, arg_name in zip(init_method.arguments, init_method.arg_names):
if arg_name == attr_name:
return SymbolTableNode(MDEF, arg.variable)
return None


def dataclass_class_maker_callback(ctx: ClassDefContext) -> None:
"""Hooks into the class typechecking process to add support for dataclasses.
"""
Expand Down
15 changes: 15 additions & 0 deletions test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -1244,3 +1244,18 @@ class A2:
b: int = attr.ib()

[builtins fixtures/list.pyi]

[case testAttrsInheritanceNoAnnotation]
import attr

@attr.s
class A:
foo = attr.ib() # type: int

x = 0
@attr.s
class B(A):
foo = x

reveal_type(B) # N: Revealed type is 'def (foo: builtins.int) -> __main__.B'
[builtins fixtures/bool.pyi]
75 changes: 75 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,51 @@ C(1, 'a') # E: Argument 2 to "C" has incompatible type "str"; expected "int"

[builtins fixtures/primitives.pyi]

[case testDataclassesInitVarIncremental]
import a

[file a.py]
import dataclasses
from b import A

@dataclasses.dataclass
class B(A):
b: int = dataclasses.field(init=False)

def __post_init__(self, a: int) -> None:
super().__post_init__(a)
self.b = a + 1

[file a.py.2]
import dataclasses
from b import A

@dataclasses.dataclass
class B(A):
b: int = dataclasses.field(init=False)

def __post_init__(self, a: int) -> None:
super().__post_init__(a)
self.b = a + 2

reveal_type(B)

[file b.py]
import dataclasses

@dataclasses.dataclass
class A:
a: dataclasses.InitVar[int]
_a: int = dataclasses.field(init=False)

def __post_init__(self, a: int) -> None:
self._a = a
[out2]
tmp/a.py:12: note: Revealed type is 'def (a: builtins.int) -> a.B'

[builtins fixtures/primitives.pyi]


[case testNoComplainFieldNone]
# flags: --python-version 3.6
# flags: --no-strict-optional
Expand Down Expand Up @@ -957,3 +1002,33 @@ class A: # E: Name 'x' already defined (possibly by an import)
@dataclass
class B(A):
pass

[case testDataclassInheritanceNoAnnotation]
from dataclasses import dataclass

@dataclass
class A:
foo: int

x = 0
@dataclass
class B(A):
foo = x

reveal_type(B) # N: Revealed type is 'def (foo: builtins.int) -> __main__.B'

[case testDataclassInheritanceNoAnnotation2]
from dataclasses import dataclass

@dataclass(frozen=True)
class A:
foo: int

@dataclass
class B(A):
@property
def foo(self) -> int: pass # E: Signature of "foo" incompatible with supertype "A"

reveal_type(B) # N: Revealed type is 'def (foo: builtins.int) -> __main__.B'

[builtins fixtures/property.pyi]