diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py index 6f2f5845cbeb4..66105f96a3eb5 100644 --- a/mypy/plugins/common.py +++ b/mypy/plugins/common.py @@ -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( @@ -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 diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 86b24f0a58fb0..81f50d1a4c4d8 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -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. @@ -31,6 +33,7 @@ def __init__( has_default: bool, line: int, column: int, + type: Optional[Type], ) -> None: self.name = name self.is_in_init = is_in_init @@ -38,19 +41,21 @@ def __init__( 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, @@ -58,11 +63,16 @@ def serialize(self) -> JsonDict: '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: @@ -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: ctx.api.defer() return decorator_arguments = { @@ -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(), ) @@ -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] @@ -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 @@ -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: @@ -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. """ diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 9748ab8942bc1..fd6fc2b3755ef 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -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] diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 36b62d53dbf1e..97bb9954ec293 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -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 @@ -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]