From 92b53471e5acc67fc5f45509405d9c83c96bd0a3 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 6 May 2018 16:15:29 +0300 Subject: [PATCH 01/21] add basic support for dataclasses --- mypy/plugin.py | 15 +- mypy/plugins/attrs.py | 64 +---- mypy/plugins/common.py | 110 +++++++ mypy/plugins/dataclasses.py | 279 ++++++++++++++++++ mypy/test/testcheck.py | 1 + test-data/unit/check-dataclasses.test | 362 ++++++++++++++++++++++++ test-data/unit/lib-stub/dataclasses.pyi | 36 +++ 7 files changed, 801 insertions(+), 66 deletions(-) create mode 100644 mypy/plugins/common.py create mode 100644 mypy/plugins/dataclasses.py create mode 100644 test-data/unit/check-dataclasses.test create mode 100644 test-data/unit/lib-stub/dataclasses.pyi diff --git a/mypy/plugin.py b/mypy/plugin.py index 75b370857536..5aba636fa7b4 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -4,7 +4,6 @@ from functools import partial from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict -import mypy.plugins.attrs from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, TypeInfo, SymbolTableNode, MypyFile @@ -302,13 +301,19 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - if fullname in mypy.plugins.attrs.attr_class_makers: - return mypy.plugins.attrs.attr_class_maker_callback - elif fullname in mypy.plugins.attrs.attr_dataclass_makers: + from mypy.plugins import attrs + from mypy.plugins import dataclasses + + if fullname in attrs.attr_class_makers: + return attrs.attr_class_maker_callback + elif fullname in attrs.attr_dataclass_makers: return partial( - mypy.plugins.attrs.attr_class_maker_callback, + attrs.attr_class_maker_callback, auto_attribs_default=True ) + # TODO: Drop the or clause once dataclasses lands in typeshed. + elif fullname in dataclasses.dataclass_makers or fullname.endswith('.dataclass'): + return dataclasses.dataclass_class_maker_callback return None diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 4a7d97f3d28b..02947385ca47 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -11,6 +11,9 @@ is_class_var, TempNode, Decorator, MemberExpr, Expression, FuncDef, Block, PassStmt, SymbolTableNode, MDEF, JsonDict, OverloadedFuncDef ) +from mypy.plugins.common import ( + _get_argument, _get_bool_argument, _get_decorator_bool_argument +) from mypy.types import ( Type, AnyType, TypeOfAny, CallableType, NoneTyp, TypeVarDef, TypeVarType, Overloaded, Instance, UnionType, FunctionLike @@ -468,67 +471,6 @@ def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute], func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info) -def _get_decorator_bool_argument( - ctx: 'mypy.plugin.ClassDefContext', - name: str, - default: bool) -> bool: - """Return the bool argument for the decorator. - - This handles both @attr.s(...) and @attr.s - """ - if isinstance(ctx.reason, CallExpr): - return _get_bool_argument(ctx, ctx.reason, name, default) - else: - return default - - -def _get_bool_argument(ctx: 'mypy.plugin.ClassDefContext', expr: CallExpr, - name: str, default: bool) -> bool: - """Return the boolean value for an argument to a call or the default if it's not found.""" - attr_value = _get_argument(expr, name) - if attr_value: - ret = ctx.api.parse_bool(attr_value) - if ret is None: - ctx.api.fail('"{}" argument must be True or False.'.format(name), expr) - return default - return ret - return default - - -def _get_argument(call: CallExpr, name: str) -> Optional[Expression]: - """Return the expression for the specific argument.""" - # To do this we use the CallableType of the callee to find the FormalArgument, - # then walk the actual CallExpr looking for the appropriate argument. - # - # Note: I'm not hard-coding the index so that in the future we can support other - # attrib and class makers. - callee_type = None - if (isinstance(call.callee, RefExpr) - and isinstance(call.callee.node, (Var, FuncBase)) - and call.callee.node.type): - callee_node_type = call.callee.node.type - if isinstance(callee_node_type, Overloaded): - # We take the last overload. - callee_type = callee_node_type.items()[-1] - elif isinstance(callee_node_type, CallableType): - callee_type = callee_node_type - - if not callee_type: - return None - - argument = callee_type.argument_by_name(name) - if not argument: - return None - assert argument.name - - for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): - if argument.pos is not None and not attr_name and i == argument.pos: - return attr_value - if attr_name == argument.name: - return attr_value - return None - - class MethodAdder: """Helper to add methods to a TypeInfo. diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py new file mode 100644 index 000000000000..dc808852043a --- /dev/null +++ b/mypy/plugins/common.py @@ -0,0 +1,110 @@ +from typing import List, Optional + +from mypy.nodes import ( + ARG_OPT, ARG_POS, MDEF, Argument, Block, CallExpr, Expression, FuncBase, + FuncDef, PassStmt, RefExpr, SymbolTableNode, Var +) +from mypy.plugin import ClassDefContext +from mypy.semanal import set_callable_name +from mypy.types import CallableType, Overloaded, Type, TypeVarDef +from mypy.typevars import fill_typevars + + +def _get_decorator_bool_argument( + ctx: ClassDefContext, + name: str, + default: bool, +) -> bool: + """Return the bool argument for the decorator. + + This handles both @decorator(...) and @decorator. + """ + if isinstance(ctx.reason, CallExpr): + return _get_bool_argument(ctx, ctx.reason, name, default) + else: + return default + + +def _get_bool_argument(ctx: ClassDefContext, expr: CallExpr, + name: str, default: bool) -> bool: + """Return the boolean value for an argument to a call or the + default if it's not found. + """ + attr_value = _get_argument(expr, name) + if attr_value: + ret = ctx.api.parse_bool(attr_value) + if ret is None: + ctx.api.fail('"{}" argument must be True or False.'.format(name), expr) + return default + return ret + return default + + +def _get_argument(call: CallExpr, name: str) -> Optional[Expression]: + """Return the expression for the specific argument.""" + # To do this we use the CallableType of the callee to find the FormalArgument, + # then walk the actual CallExpr looking for the appropriate argument. + # + # Note: I'm not hard-coding the index so that in the future we can support other + # attrib and class makers. + callee_type = None + if (isinstance(call.callee, RefExpr) + and isinstance(call.callee.node, (Var, FuncBase)) + and call.callee.node.type): + callee_node_type = call.callee.node.type + if isinstance(callee_node_type, Overloaded): + # We take the last overload. + callee_type = callee_node_type.items()[-1] + elif isinstance(callee_node_type, CallableType): + callee_type = callee_node_type + + if not callee_type: + return None + + argument = callee_type.argument_by_name(name) + if not argument: + return None + assert argument.name + + for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): + if argument.pos is not None and not attr_name and i == argument.pos: + return attr_value + if attr_name == argument.name: + return attr_value + return None + + +def _add_method( + ctx: ClassDefContext, + name: str, + args: List[Argument], + return_type: Type, + self_type: Optional[Type] = None, + tvar_def: Optional[TypeVarDef] = None, +) -> None: + """Adds a new method to a class. + """ + info = ctx.cls.info + self_type = self_type or fill_typevars(info) + function_type = ctx.api.named_type('__builtins__.function') + + args = [Argument(Var('self'), self_type, None, ARG_POS)] + args + arg_types, arg_names, arg_kinds = [], [], [] + for arg in args: + assert arg.type_annotation, 'All arguments must be fully typed.' + arg_types.append(arg.type_annotation) + arg_names.append(arg.variable.name()) + arg_kinds.append(arg.kind) + + signature = CallableType(arg_types, arg_kinds, arg_names, return_type, function_type) + if tvar_def: + signature.variables = [tvar_def] + + func = FuncDef(name, args, Block([PassStmt()])) + func.info = info + func.type = set_callable_name(signature, func) + func._fullname = info.fullname() + '.' + name + func.line = info.line + + info.names[name] = SymbolTableNode(MDEF, func) + info.defn.defs.body.append(func) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py new file mode 100644 index 000000000000..e9ebec4135e5 --- /dev/null +++ b/mypy/plugins/dataclasses.py @@ -0,0 +1,279 @@ +from collections import OrderedDict +from typing import Dict, List, Optional, Set, Tuple, cast + +from mypy.nodes import ( + ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, Block, CallExpr, Context, + Decorator, Expression, JsonDict, NameExpr, SymbolTableNode, TempNode, + TypeInfo, Var +) +from mypy.plugin import ClassDefContext +from mypy.plugins.common import _add_method, _get_decorator_bool_argument +from mypy.types import CallableType, NoneTyp, Type, TypeVarDef, TypeVarType +from mypy.typevars import fill_typevars + +#: The set of decorators that generate dataclasses. +dataclass_makers = { + 'dataclass', + 'dataclasses.dataclass', +} + + +class DataclassAttribute: + def __init__( + self, + name: str, + is_in_init: bool, + has_default: bool, + line: int, column: int, + ) -> None: + self.name = name + self.is_in_init = is_in_init + self.has_default = has_default + self.line = line + self.column = column + + def to_argument(self, info: TypeInfo) -> Argument: + return Argument( + variable=self.to_var(info), + type_annotation=info[self.name].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 serialize(self) -> JsonDict: + return { + 'name': self.name, + 'is_in_init': self.is_in_init, + 'has_default': self.has_default, + 'line': self.line, + 'column': self.column, + } + + @classmethod + def deserialize(cls, data: JsonDict) -> "DataclassAttribute": + return cls(**data) + + +class DataclassTransformer: + def __init__(self, ctx: ClassDefContext) -> None: + self._ctx = ctx + + def transform(self) -> None: + ctx = self._ctx + info = self._ctx.cls.info + attributes = self.collect_attributes() + decorator_arguments = { + 'init': _get_decorator_bool_argument(self._ctx, 'init', True), + 'eq': _get_decorator_bool_argument(self._ctx, 'eq', True), + 'order': _get_decorator_bool_argument(self._ctx, 'order', False), + 'frozen': _get_decorator_bool_argument(self._ctx, 'frozen', False), + } + + if decorator_arguments['init']: + _add_method( + ctx, + '__init__', + args=[attr.to_argument(info) for attr in attributes if attr.is_in_init], + return_type=NoneTyp(), + ) + for stmt in self._ctx.cls.defs.body: + if isinstance(stmt, Decorator) and stmt.func.is_class: + func_type = stmt.func.type + if isinstance(func_type, CallableType): + func_type.arg_types[0] = self._ctx.api.class_type(self._ctx.cls.info) + + # Add an eq method, but only if the class doesn't already have one. + if decorator_arguments['eq'] and info.get('__eq__') is None: + cmp_tvar_def = TypeVarDef('T', 'T', 1, [], ctx.api.named_type('__builtins__.object')) + cmp_other_type = TypeVarType(cmp_tvar_def) + cmp_return_type = ctx.api.named_type('__builtins__.bool') + + for method_name in ['__eq__', '__ne__']: + _add_method( + ctx, + method_name, + args=[Argument(Var('other', cmp_other_type), cmp_other_type, None, ARG_POS)], + return_type=cmp_return_type, + self_type=cmp_other_type, + tvar_def=cmp_tvar_def, + ) + + # Add <,>,<=,>=, but only if the class has an eq method. + if decorator_arguments['order']: + if not decorator_arguments['eq']: + ctx.api.fail('eq must be True if order is True', ctx.cls) + + order_tvar_def = TypeVarDef('T', 'T', 1, [], ctx.api.named_type('__builtins__.object')) + order_other_type = TypeVarType(order_tvar_def) + order_return_type = ctx.api.named_type('__builtins__.bool') + order_args = [ + Argument(Var('other', order_other_type), order_other_type, None, ARG_POS) + ] + + for method_name in ['__lt__', '__gt__', '__le__', '__ge__']: + existing_method = info.get(method_name) + if existing_method is not None: + assert existing_method.node + ctx.api.fail( + 'You may not have a custom %s method when order=True' % method_name, + existing_method.node, + ) + + _add_method( + ctx, + method_name, + args=order_args, + return_type=order_return_type, + self_type=order_other_type, + tvar_def=order_tvar_def, + ) + + if decorator_arguments['frozen']: + self._freeze(attributes) + + info.metadata['dataclass'] = { + 'attributes': OrderedDict((attr.name, attr.serialize()) for attr in attributes), + 'frozen': decorator_arguments['frozen'], + } + + def collect_attributes(self) -> List[DataclassAttribute]: + """Collect all attributes declared in the dataclass and its parents. + + All assignments of the form + + a: SomeType + b: SomeOtherType = ... + + are collected. + """ + # First, collect attributes belonging to the current class. + ctx = self._ctx + cls = self._ctx.cls + attrs = [] # type: List[DataclassAttribute] + known_attrs = set() # type: Set[str] + for stmt in cls.defs.body: + # Any assignment that doesn't use the new type declaration + # syntax can be ignored out of hand. + if not (isinstance(stmt, AssignmentStmt) and stmt.new_syntax): + continue + + # a: int, b: str = 1, 'foo' is not supported syntax so we + # don't have to worry about it. + lhs = stmt.lvalues[0] + if not isinstance(lhs, NameExpr): + continue + + try: + node = cls.info.names[lhs.name].node + assert isinstance(node, Var) + + # x: ClassVar[int] is ignored by dataclasses. + if node.is_classvar: + continue + + # Treat the assignment as an instance-level assignment. + node.is_initialized_in_class = False + except KeyError: + continue + + has_field_call, field_args = _collect_field_args(stmt.rvalue) + + try: + is_in_init = bool(ctx.api.parse_bool(field_args['init'])) + except KeyError: + is_in_init = True + + has_default = False + # Ensure that something like x: int = field() is rejected + # after an attribute with a default. + if has_field_call: + has_default = 'default' in field_args or 'default_factory' in field_args + + # All other assignments are type checked. + elif not isinstance(stmt.rvalue, TempNode): + has_default = True + + known_attrs.add(lhs.name) + attrs.append(DataclassAttribute( + name=lhs.name, + is_in_init=is_in_init, + has_default=has_default, + line=stmt.line, + column=stmt.column, + )) + + # Next, collect attributes belonging to any class in the MRO + # as long as those attributes weren't already collected. This + # makes it possible to overwrite attributes in subclasses. + super_attrs = [] + for info in cls.info.mro[1:-1]: + if 'dataclass' not in info.metadata: + continue + + for name, data in info.metadata['dataclass']['attributes'].items(): + if name not in known_attrs: + attr = DataclassAttribute.deserialize(data) + known_attrs.add(name) + super_attrs.append(attr) + + all_attrs = super_attrs + attrs + + # Ensure that arguments without a default don't follow + # arguments that have a default. + found_default = False + for attr in all_attrs: + if found_default and attr.is_in_init and not attr.has_default: + ctx.api.fail( + "Attributes without a default cannot follow attributes with one", + Context(line=attr.line, column=attr.column), + ) + + found_default = found_default or attr.has_default + + return all_attrs + + def _freeze(self, attributes: List[DataclassAttribute]) -> None: + """Converts all attributes to @property methods in order to + emulate frozen classes. + """ + info = self._ctx.cls.info + for attr in attributes: + try: + node = info.names[attr.name].node + assert isinstance(node, Var) + node.is_property = True + except KeyError: + var = attr.to_var(info) + var.info = info + var.is_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. + """ + transformer = DataclassTransformer(ctx) + transformer.transform() + + +def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]: + """Returns a tuple where the first value represents whether or not + the expression is a call to dataclass.field and the second is a + dictionary of the keyword arguments that field() was called with. + """ + if ( + isinstance(expr, CallExpr) and + isinstance(expr.callee, NameExpr) and + expr.callee.fullname == "dataclasses.field" + ): + # field() only takes keyword arguments. + args = {} + for name, arg in zip(expr.arg_names, expr.args): + assert name is not None + args[name] = arg + return True, args + return False, {} diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index c4ff9551d3bf..c96fc4eeb90a 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -77,6 +77,7 @@ 'check-custom-plugin.test', 'check-default-plugin.test', 'check-attr.test', + 'check-dataclasses.test', ] diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test new file mode 100644 index 000000000000..df9f73082d35 --- /dev/null +++ b/test-data/unit/check-dataclasses.test @@ -0,0 +1,362 @@ +[case testDataclassesBasic] +# flags: --python-version 3.6 +from dataclasses import dataclass + +@dataclass +class Person: + name: str + age: int + + def summary(self): + return "%s is %d years old." % (self.name, self.age) + +reveal_type(Person) # E: Revealed type is 'def (name: builtins.str, age: builtins.int) -> __main__.Person' +Person('John', 32) +Person('Jonh', 21, None) # E: Too many arguments for "Person" + +[builtins fixtures/list.pyi] + + +[case testDataclassesBasicInheritance] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Mammal: + age: int + + +@dataclass +class Person(Mammal): + name: str + + def summary(self): + return "%s is %d years old." % (self.name, self.age) + +reveal_type(Person) # E: Revealed type is 'def (age: builtins.int, name: builtins.str) -> __main__.Person' +Mammal(10) +Person(32, 'John') +Person(21, 'Jonh', None) # E: Too many arguments for "Person" + +[builtins fixtures/list.pyi] + + +[case testDataclassesDeepInheritance] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class A: + a: int + + +@dataclass +class B(A): + b: int + + +@dataclass +class C(B): + c: int + + +@dataclass +class D(C): + d: int + +reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.A' +reveal_type(B) # E: Revealed type is 'def (a: builtins.int, b: builtins.int) -> __main__.B' +reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.int, c: builtins.int) -> __main__.C' +reveal_type(D) # E: Revealed type is 'def (a: builtins.int, b: builtins.int, c: builtins.int, d: builtins.int) -> __main__.D' + +[builtins fixtures/list.pyi] + + +[case testDataclassesOverriding] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Mammal: + age: int + + +@dataclass +class Person(Mammal): + name: str + age: int + +reveal_type(Person) # E: Revealed type is 'def (name: builtins.str, age: builtins.int) -> __main__.Person' +Person('John', 32) +Person('John', 21, None) # E: Too many arguments for "Person" + +[builtins fixtures/list.pyi] + + +[case testDataclassesFreezing] +# flags: --python-version 3.6 +from dataclasses import dataclass + +@dataclass(frozen=True) +class Person: + name: str + +john = Person('John') +john.name = 'Ben' # E: Property "name" defined in "Person" is read-only + +[builtins fixtures/list.pyi] + + +[case testDataclassesFields] +# flags: --python-version 3.6 +from dataclasses import dataclass, field + +@dataclass +class Person: + name: str + age: int = field(default=0, init=False) + +reveal_type(Person) # E: Revealed type is 'def (name: builtins.str) -> __main__.Person' +john = Person('John') +john.age = 'invalid' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +john.age = 24 + +[builtins fixtures/list.pyi] + + +[case testDataclassesBadInit] +# flags: --python-version 3.6 +from dataclasses import dataclass, field + +@dataclass +class Person: + name: str + age: int = field(init=None) # E: Argument "init" to "field" has incompatible type "None"; expected "bool" + +[builtins fixtures/list.pyi] + + +[case testDataclassesDefaults] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Application: + name: str = 'Unnamed' + rating: int = 0 + +reveal_type(Application) # E: Revealed type is 'def (name: builtins.str =, rating: builtins.int =) -> __main__.Application' +app = Application() + +[builtins fixtures/list.pyi] + + +[case testDataclassesDefaultFactories] +# flags: --python-version 3.6 +from dataclasses import dataclass, field + + +@dataclass +class Application: + name: str = 'Unnamed' + rating: int = field(default_factory=int) + rating_count: int = field() # E: Attributes without a default cannot follow attributes with one + +[builtins fixtures/list.pyi] + + +[case testDataclassesDefaultFactoryTypeChecking] +# flags: --python-version 3.6 +from dataclasses import dataclass, field + + +@dataclass +class Application: + name: str = 'Unnamed' + rating: int = field(default_factory=str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +[builtins fixtures/list.pyi] + + +[case testDataclassesDefaultOrdering] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Application: + name: str = 'Unnamed' + rating: int # E: Attributes without a default cannot follow attributes with one + +[builtins fixtures/list.pyi] + + +[case testDataclassesClassmethods] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Application: + name: str + + @classmethod + def parse(cls, request: str) -> "Application": + return cls(name='...') + +app = Application.parse('') + +[builtins fixtures/list.pyi] +[builtins fixtures/classmethod.pyi] + + +[case testDataclassesClassVars] +# flags: --python-version 3.6 +from dataclasses import dataclass +from typing import ClassVar + + +@dataclass +class Application: + name: str + + COUNTER: ClassVar[int] = 0 + +reveal_type(Application) # E: Revealed type is 'def (name: builtins.str) -> __main__.Application' +application = Application("example") +application.COUNTER = 1 # E: Cannot assign to class variable "COUNTER" via instance +Application.COUNTER = 1 + +[builtins fixtures/list.pyi] + + +[case testDataclassEquality] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Application: + name: str + rating: int + +app1 = Application("example-1", 5) +app2 = Application("example-2", 5) +app1 == app2 +app1 != app2 +app1 == None # E: Unsupported operand types for == ("Application" and "None") + +[builtins fixtures/list.pyi] + + +[case testDataclassCustomEquality] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass +class Application: + name: str + rating: int + + def __eq__(self, other: 'Application') -> bool: + ... + +app1 = Application("example-1", 5) +app2 = Application("example-2", 5) +app1 == app2 +app1 != app2 # E: Unsupported left operand type for != ("Application") +app1 == None # E: Unsupported operand types for == ("Application" and "None") + + +class SpecializedApplication(Application): + ... + +app1 == SpecializedApplication("example-3", 5) + +[builtins fixtures/list.pyi] + + +[case testDataclassOrdering] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass(order=True) +class Application: + name: str + rating: int + +app1 = Application('example-1', 5) +app2 = Application('example-2', 5) +app1 < app2 +app1 > app2 +app1 <= app2 +app1 >= app2 +app1 < 5 # E: Unsupported operand types for < ("Application" and "int") +app1 > 5 # E: Unsupported operand types for > ("Application" and "int") +app1 <= 5 # E: Unsupported operand types for <= ("Application" and "int") +app1 >= 5 # E: Unsupported operand types for >= ("Application" and "int") + + +class SpecializedApplication(Application): + ... + +app3 = SpecializedApplication('example-3', 5) +app1 < app3 +app1 > app3 +app1 <= app3 +app1 >= app3 + +[builtins fixtures/list.pyi] + + +[case testDataclassOrderingWithoutEquality] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass(eq=False, order=True) # E: eq must be True if order is True +class Application: + ... + +[builtins fixtures/list.pyi] + + +[case testDataclassOrderingWithCustomMethods] +# flags: --python-version 3.6 +from dataclasses import dataclass + + +@dataclass(order=True) +class Application: + def __lt__(self, other: 'Application') -> bool: # E: You may not have a custom __lt__ method when order=True + ... + +[builtins fixtures/list.pyi] + + +[case testDataclassDefaultsInheritance] +# flags: --python-version 3.6 +from dataclasses import dataclass +from typing import Optional + + +@dataclass(order=True) +class Application: + id: Optional[int] + name: str + + +@dataclass +class SpecializedApplication(Application): + rating: int = 0 + + +reveal_type(SpecializedApplication) # E: Revealed type is 'def (id: Union[builtins.int, None], name: builtins.str, rating: builtins.int =) -> __main__.SpecializedApplication' + +[builtins fixtures/list.pyi] \ No newline at end of file diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi new file mode 100644 index 000000000000..a5675a939234 --- /dev/null +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -0,0 +1,36 @@ +from typing import Any, Callable, Optional, TypeVar, overload + +_T = TypeVar('_T') +_C = TypeVar('_C', bound=type) + + +@overload +def dataclass(_cls: _C, + *, + init: bool = ..., + repr: bool = ..., + eq: bool = ..., + order: bool = ..., + unsafe_hash: bool = ..., + frozen: bool = ...) -> _C: ... + + +@overload +def dataclass(_cls: None = ..., + *, + init: bool = ..., + repr: bool = ..., + eq: bool = ..., + order: bool = ..., + unsafe_hash: bool = ..., + frozen: bool = ...) -> Callable[[_C], _C]: ... + + +def field(*, + default: Optional[_T] = ..., + default_factory: Optional[Callable[..., _T]] = ..., + init: bool = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + compare: bool = ..., + metadata: Any = ...) -> _T: ... From 98becd6be09b4cf7126eb6f501bcf9c0750c1bbc Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 27 May 2018 13:32:20 +0300 Subject: [PATCH 02/21] clean up code comments --- mypy/plugins/dataclasses.py | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index e9ebec4135e5..fd543d48480e 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -11,7 +11,7 @@ from mypy.types import CallableType, NoneTyp, Type, TypeVarDef, TypeVarType from mypy.typevars import fill_typevars -#: The set of decorators that generate dataclasses. +# The set of decorators that generate dataclasses. dataclass_makers = { 'dataclass', 'dataclasses.dataclass', @@ -53,7 +53,7 @@ def serialize(self) -> JsonDict: } @classmethod - def deserialize(cls, data: JsonDict) -> "DataclassAttribute": + def deserialize(cls, data: JsonDict) -> 'DataclassAttribute': return cls(**data) @@ -62,6 +62,10 @@ def __init__(self, ctx: ClassDefContext) -> None: self._ctx = ctx def transform(self) -> None: + """Apply all the necessary transformations to the underlying + dataclass so as to ensure it is fully type checked according + to the rules in PEP 557. + """ ctx = self._ctx info = self._ctx.cls.info attributes = self.collect_attributes() @@ -80,6 +84,8 @@ def transform(self) -> None: return_type=NoneTyp(), ) for stmt in self._ctx.cls.defs.body: + # Fix up the types of classmethods since, by default, + # they will be based on the parent class' init. if isinstance(stmt, Decorator) and stmt.func.is_class: func_type = stmt.func.type if isinstance(func_type, CallableType): @@ -166,22 +172,22 @@ def collect_attributes(self) -> List[DataclassAttribute]: if not isinstance(lhs, NameExpr): continue - try: - node = cls.info.names[lhs.name].node - assert isinstance(node, Var) - - # x: ClassVar[int] is ignored by dataclasses. - if node.is_classvar: - continue + node = cls.info.names[lhs.name].node + assert isinstance(node, Var) - # Treat the assignment as an instance-level assignment. - node.is_initialized_in_class = False - except KeyError: + # x: ClassVar[int] is ignored by dataclasses. + if node.is_classvar: continue + # Treat the assignment as an instance-level assignment + # even though it looks like a class-level assignment. + node.is_initialized_in_class = False + has_field_call, field_args = _collect_field_args(stmt.rvalue) try: + # parse_bool returns an optional bool, so we corece it + # to a bool here in order to appease the type checker. is_in_init = bool(ctx.api.parse_bool(field_args['init'])) except KeyError: is_in_init = True @@ -192,7 +198,7 @@ def collect_attributes(self) -> List[DataclassAttribute]: if has_field_call: has_default = 'default' in field_args or 'default_factory' in field_args - # All other assignments are type checked. + # All other assignments are already type checked. elif not isinstance(stmt.rvalue, TempNode): has_default = True @@ -225,9 +231,12 @@ def collect_attributes(self) -> List[DataclassAttribute]: # arguments that have a default. found_default = False for attr in all_attrs: + # If we find any attribute that is_in_init but that + # doesn't have a default after one that does have one, + # then that's an error. if found_default and attr.is_in_init and not attr.has_default: ctx.api.fail( - "Attributes without a default cannot follow attributes with one", + 'Attributes without a default cannot follow attributes with one', Context(line=attr.line, column=attr.column), ) @@ -268,7 +277,7 @@ def _collect_field_args(expr: Expression) -> Tuple[bool, Dict[str, Expression]]: if ( isinstance(expr, CallExpr) and isinstance(expr.callee, NameExpr) and - expr.callee.fullname == "dataclasses.field" + expr.callee.fullname == 'dataclasses.field' ): # field() only takes keyword arguments. args = {} From 3979713c6f73a4b24b24b8caebeef9f36d2eebbf Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 27 May 2018 13:32:59 +0300 Subject: [PATCH 03/21] avoid reusing types for comparison methods --- mypy/plugins/dataclasses.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index fd543d48480e..3ea521a17bc9 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -93,11 +93,15 @@ def transform(self) -> None: # Add an eq method, but only if the class doesn't already have one. if decorator_arguments['eq'] and info.get('__eq__') is None: - cmp_tvar_def = TypeVarDef('T', 'T', 1, [], ctx.api.named_type('__builtins__.object')) - cmp_other_type = TypeVarType(cmp_tvar_def) - cmp_return_type = ctx.api.named_type('__builtins__.bool') - for method_name in ['__eq__', '__ne__']: + # The TVar is used to enforce that "other" must have + # the same type as self (covariant). Note the + # "self_type" parameter to _add_method. + obj_type = ctx.api.named_type('__builtins__.object') + cmp_tvar_def = TypeVarDef('T', 'T', 1, [], obj_type) + cmp_other_type = TypeVarType(cmp_tvar_def) + cmp_return_type = ctx.api.named_type('__builtins__.bool') + _add_method( ctx, method_name, @@ -107,19 +111,22 @@ def transform(self) -> None: tvar_def=cmp_tvar_def, ) - # Add <,>,<=,>=, but only if the class has an eq method. + # Add <, >, <=, >=, but only if the class has an eq method. if decorator_arguments['order']: if not decorator_arguments['eq']: ctx.api.fail('eq must be True if order is True', ctx.cls) - order_tvar_def = TypeVarDef('T', 'T', 1, [], ctx.api.named_type('__builtins__.object')) - order_other_type = TypeVarType(order_tvar_def) - order_return_type = ctx.api.named_type('__builtins__.bool') - order_args = [ - Argument(Var('other', order_other_type), order_other_type, None, ARG_POS) - ] - for method_name in ['__lt__', '__gt__', '__le__', '__ge__']: + # Like for __eq__ and __ne__, we want "other" to match + # the self type. + obj_type = ctx.api.named_type('__builtins__.object') + order_tvar_def = TypeVarDef('T', 'T', 1, [], obj_type) + order_other_type = TypeVarType(order_tvar_def) + order_return_type = ctx.api.named_type('__builtins__.bool') + order_args = [ + Argument(Var('other', order_other_type), order_other_type, None, ARG_POS) + ] + existing_method = info.get(method_name) if existing_method is not None: assert existing_method.node From 4ab42a158b0fb28bdb94306f866306b0e0ae2533 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 27 May 2018 13:36:29 +0300 Subject: [PATCH 04/21] drop extra newlines from tests --- test-data/unit/check-dataclasses.test | 44 +-------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index df9f73082d35..2ccf9fac9668 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -16,17 +16,14 @@ Person('Jonh', 21, None) # E: Too many arguments for "Person" [builtins fixtures/list.pyi] - [case testDataclassesBasicInheritance] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Mammal: age: int - @dataclass class Person(Mammal): name: str @@ -41,27 +38,22 @@ Person(21, 'Jonh', None) # E: Too many arguments for "Person" [builtins fixtures/list.pyi] - [case testDataclassesDeepInheritance] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class A: a: int - @dataclass class B(A): b: int - @dataclass class C(B): c: int - @dataclass class D(C): d: int @@ -73,17 +65,14 @@ reveal_type(D) # E: Revealed type is 'def (a: builtins.int, b: builtins.int, c: [builtins fixtures/list.pyi] - [case testDataclassesOverriding] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Mammal: age: int - @dataclass class Person(Mammal): name: str @@ -95,7 +84,6 @@ Person('John', 21, None) # E: Too many arguments for "Person" [builtins fixtures/list.pyi] - [case testDataclassesFreezing] # flags: --python-version 3.6 from dataclasses import dataclass @@ -109,7 +97,6 @@ john.name = 'Ben' # E: Property "name" defined in "Person" is read-only [builtins fixtures/list.pyi] - [case testDataclassesFields] # flags: --python-version 3.6 from dataclasses import dataclass, field @@ -126,7 +113,6 @@ john.age = 24 [builtins fixtures/list.pyi] - [case testDataclassesBadInit] # flags: --python-version 3.6 from dataclasses import dataclass, field @@ -138,12 +124,10 @@ class Person: [builtins fixtures/list.pyi] - [case testDataclassesDefaults] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Application: name: str = 'Unnamed' @@ -154,12 +138,10 @@ app = Application() [builtins fixtures/list.pyi] - [case testDataclassesDefaultFactories] # flags: --python-version 3.6 from dataclasses import dataclass, field - @dataclass class Application: name: str = 'Unnamed' @@ -168,12 +150,10 @@ class Application: [builtins fixtures/list.pyi] - [case testDataclassesDefaultFactoryTypeChecking] # flags: --python-version 3.6 from dataclasses import dataclass, field - @dataclass class Application: name: str = 'Unnamed' @@ -181,12 +161,10 @@ class Application: [builtins fixtures/list.pyi] - [case testDataclassesDefaultOrdering] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Application: name: str = 'Unnamed' @@ -194,12 +172,10 @@ class Application: [builtins fixtures/list.pyi] - [case testDataclassesClassmethods] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Application: name: str @@ -213,13 +189,11 @@ app = Application.parse('') [builtins fixtures/list.pyi] [builtins fixtures/classmethod.pyi] - [case testDataclassesClassVars] # flags: --python-version 3.6 from dataclasses import dataclass from typing import ClassVar - @dataclass class Application: name: str @@ -233,12 +207,10 @@ Application.COUNTER = 1 [builtins fixtures/list.pyi] - [case testDataclassEquality] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Application: name: str @@ -252,12 +224,10 @@ app1 == None # E: Unsupported operand types for == ("Application" and "None") [builtins fixtures/list.pyi] - [case testDataclassCustomEquality] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass class Application: name: str @@ -272,7 +242,6 @@ app1 == app2 app1 != app2 # E: Unsupported left operand type for != ("Application") app1 == None # E: Unsupported operand types for == ("Application" and "None") - class SpecializedApplication(Application): ... @@ -280,12 +249,10 @@ app1 == SpecializedApplication("example-3", 5) [builtins fixtures/list.pyi] - [case testDataclassOrdering] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass(order=True) class Application: name: str @@ -302,7 +269,6 @@ app1 > 5 # E: Unsupported operand types for > ("Application" and "int") app1 <= 5 # E: Unsupported operand types for <= ("Application" and "int") app1 >= 5 # E: Unsupported operand types for >= ("Application" and "int") - class SpecializedApplication(Application): ... @@ -314,24 +280,20 @@ app1 >= app3 [builtins fixtures/list.pyi] - [case testDataclassOrderingWithoutEquality] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass(eq=False, order=True) # E: eq must be True if order is True class Application: ... [builtins fixtures/list.pyi] - [case testDataclassOrderingWithCustomMethods] # flags: --python-version 3.6 from dataclasses import dataclass - @dataclass(order=True) class Application: def __lt__(self, other: 'Application') -> bool: # E: You may not have a custom __lt__ method when order=True @@ -339,24 +301,20 @@ class Application: [builtins fixtures/list.pyi] - [case testDataclassDefaultsInheritance] # flags: --python-version 3.6 from dataclasses import dataclass from typing import Optional - @dataclass(order=True) class Application: id: Optional[int] name: str - @dataclass class SpecializedApplication(Application): rating: int = 0 - reveal_type(SpecializedApplication) # E: Revealed type is 'def (id: Union[builtins.int, None], name: builtins.str, rating: builtins.int =) -> __main__.SpecializedApplication' -[builtins fixtures/list.pyi] \ No newline at end of file +[builtins fixtures/list.pyi] From 58ae57714b9e413969b222f08e9ef91a2bf415c7 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 27 May 2018 13:40:29 +0300 Subject: [PATCH 05/21] add tests for multiple init arg combos --- setup.cfg | 3 +++ test-data/unit/check-dataclasses.test | 33 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/setup.cfg b/setup.cfg index cfc72fbd89a1..086aa8b951eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,3 +48,6 @@ parallel = true [coverage:report] show_missing = true + +[isort] +multi_line_output = 5 \ No newline at end of file diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 2ccf9fac9668..0491ca8165c1 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -124,6 +124,39 @@ class Person: [builtins fixtures/list.pyi] +[case testDataclassesMultiInit] +# flags: --python-version 3.6 +from dataclasses import dataclass, field +from typing import List + +@dataclass +class Person: + name: str + age: int = field(init=False) + friend_names: List[str] = field(init=True) + enemy_names: List[str] + +reveal_type(Person) # E: Revealed type is 'def (name: builtins.str, friend_names: builtins.list[builtins.str], enemy_names: builtins.list[builtins.str]) -> __main__.Person' + +[builtins fixtures/list.pyi] + +[case testDataclassesMultiInitDefaults] +# flags: --python-version 3.6 +from dataclasses import dataclass, field +from typing import List, Optional + +@dataclass +class Person: + name: str + age: int = field(init=False) + friend_names: List[str] = field(init=True) + enemy_names: List[str] + nickname: Optional[str] = None + +reveal_type(Person) # E: Revealed type is 'def (name: builtins.str, friend_names: builtins.list[builtins.str], enemy_names: builtins.list[builtins.str], nickname: Union[builtins.str, None] =) -> __main__.Person' + +[builtins fixtures/list.pyi] + [case testDataclassesDefaults] # flags: --python-version 3.6 from dataclasses import dataclass From 7f7415e33faa58afae757236b35295dcdcd5e3ff Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 27 May 2018 14:42:53 +0300 Subject: [PATCH 06/21] add InitVar to lib stub --- test-data/unit/check-dataclasses.test | 2 +- test-data/unit/lib-stub/dataclasses.pyi | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 0491ca8165c1..489efb46ba4d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -350,4 +350,4 @@ class SpecializedApplication(Application): reveal_type(SpecializedApplication) # E: Revealed type is 'def (id: Union[builtins.int, None], name: builtins.str, rating: builtins.int =) -> __main__.SpecializedApplication' -[builtins fixtures/list.pyi] +[builtins fixtures/list.pyi] \ No newline at end of file diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi index a5675a939234..1332964f96a6 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -1,8 +1,11 @@ -from typing import Any, Callable, Optional, TypeVar, overload +from typing import Any, Callable, Generic, Optional, TypeVar, overload _T = TypeVar('_T') _C = TypeVar('_C', bound=type) +class InitVar(Generic[_T]): + ... + @overload def dataclass(_cls: _C, From addde6e323498b7c175ede344b35eac1a88e8c97 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Sun, 27 May 2018 14:53:53 +0300 Subject: [PATCH 07/21] add generic dataclass test --- test-data/unit/check-dataclasses.test | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 489efb46ba4d..95f256669851 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -350,4 +350,35 @@ class SpecializedApplication(Application): reveal_type(SpecializedApplication) # E: Revealed type is 'def (id: Union[builtins.int, None], name: builtins.str, rating: builtins.int =) -> __main__.SpecializedApplication' -[builtins fixtures/list.pyi] \ No newline at end of file +[builtins fixtures/list.pyi] + +[case testDataclassGenerics] +# flags: --python-version 3.6 +from dataclasses import dataclass +from typing import Generic, List, Optional, TypeVar + +T = TypeVar('T') + +@dataclass +class A(Generic[T]): + x: T + y: T + z: List[T] + + def foo(self) -> List[T]: + return [self.x, self.y] + + def bar(self) -> T: + return self.z[0] + + def problem(self) -> T: + return self.z # E: Incompatible return value type (got "List[T]", expected "T") + +reveal_type(A) # E: Revealed type is 'def [T] (x: T`1, y: T`1, z: builtins.list[T`1]) -> __main__.A[T`1]' +a = A(1, 2, [1, 2]) +reveal_type(a) # E: Revealed type is '__main__.A[builtins.int*]' +reveal_type(a.x) # E: Revealed type is 'builtins.int*' +reveal_type(a.y) # E: Revealed type is 'builtins.int*' +reveal_type(a.z) # E: Revealed type is 'builtins.list[builtins.int*]' + +[builtins fixtures/list.pyi] From 3004fac4d938e933b9a31f056fdc2d4c36d098c6 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 28 May 2018 10:01:23 +0300 Subject: [PATCH 08/21] drop is_initialized_in_class block since it serves no purpose --- mypy/plugins/dataclasses.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 3ea521a17bc9..6a40e344b6a9 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -186,10 +186,6 @@ def collect_attributes(self) -> List[DataclassAttribute]: if node.is_classvar: continue - # Treat the assignment as an instance-level assignment - # even though it looks like a class-level assignment. - node.is_initialized_in_class = False - has_field_call, field_args = _collect_field_args(stmt.rvalue) try: From f5f52a835dc9f4cbf61a66a8af4b328737e11af6 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 28 May 2018 10:03:26 +0300 Subject: [PATCH 09/21] add more checks to generic dataclass test --- test-data/unit/check-dataclasses.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 95f256669851..0a63452956a6 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -375,10 +375,12 @@ class A(Generic[T]): return self.z # E: Incompatible return value type (got "List[T]", expected "T") reveal_type(A) # E: Revealed type is 'def [T] (x: T`1, y: T`1, z: builtins.list[T`1]) -> __main__.A[T`1]' +A(1, 2, ["a", "b"]) # E: Cannot infer type argument 1 of "A" a = A(1, 2, [1, 2]) reveal_type(a) # E: Revealed type is '__main__.A[builtins.int*]' reveal_type(a.x) # E: Revealed type is 'builtins.int*' reveal_type(a.y) # E: Revealed type is 'builtins.int*' reveal_type(a.z) # E: Revealed type is 'builtins.list[builtins.int*]' +s: str = a.bar() # E: Incompatible types in assignment (expression has type "int", variable has type "str") [builtins fixtures/list.pyi] From 74436ea335342af33f864f3ef3daa26760985f79 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 28 May 2018 10:10:51 +0300 Subject: [PATCH 10/21] refactor KeyError blocks --- mypy/plugins/dataclasses.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 6a40e344b6a9..04874b3cffdf 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -188,12 +188,11 @@ def collect_attributes(self) -> List[DataclassAttribute]: has_field_call, field_args = _collect_field_args(stmt.rvalue) - try: - # parse_bool returns an optional bool, so we corece it - # to a bool here in order to appease the type checker. - is_in_init = bool(ctx.api.parse_bool(field_args['init'])) - except KeyError: + is_in_init_param = ctx.api.parse_bool(field_args.get('init')) + if is_in_init_param is None: is_in_init = True + else: + is_in_init = is_in_init_param has_default = False # Ensure that something like x: int = field() is rejected @@ -253,11 +252,12 @@ def _freeze(self, attributes: List[DataclassAttribute]) -> None: """ info = self._ctx.cls.info for attr in attributes: - try: - node = info.names[attr.name].node - assert isinstance(node, Var) - node.is_property = True - except KeyError: + sym_node = info.names.get(attr.name) + if sym_node is not None: + var = sym_node.node + assert isinstance(var, Var) + var.is_property = True + else: var = attr.to_var(info) var.info = info var.is_property = True From 2918ebd2829a48d6e332556570753c1d1c06c315 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 28 May 2018 10:36:26 +0300 Subject: [PATCH 11/21] add incremental tests for dataclasses --- test-data/unit/check-dataclasses.test | 17 ++ test-data/unit/check-incremental.test | 291 ++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 0a63452956a6..09271fc6a5e1 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -384,3 +384,20 @@ reveal_type(a.z) # E: Revealed type is 'builtins.list[builtins.int*]' s: str = a.bar() # E: Incompatible types in assignment (expression has type "int", variable has type "str") [builtins fixtures/list.pyi] + +[case testDataclassesForwardRefs] +from dataclasses import dataclass + +@dataclass +class A: + b: 'B' + +@dataclass +class B: + x: int + +reveal_type(A) # E: Revealed type is 'def (b: __main__.B) -> __main__.A' +A(b=B(42)) +A(b=42) # E: Argument "b" to "A" has incompatible type "int"; expected "B" + +[builtins fixtures/list.pyi] \ No newline at end of file diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 1080cf511985..d7427656aed0 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4361,3 +4361,294 @@ import b -- since the interface didn't change [stale] [rechecked b] + +[case testIncrementalDataclassesSubclassingCached] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + e: str = 'e' + +a = B(5, [5], 'foo') +a.a = 6 +a._b = [2] +a.c = 'yo' +a._d = 22 +a.e = 'hi' + +[file a.py] +from dataclasses import dataclass, field +from typing import ClassVar, List + +@dataclass +class A: + a: int + _b: List[int] + c: str = '18' + _d: int = field(default=False) + E = 7 + F: ClassVar[int] = 22 + +[builtins fixtures/list.pyi] +[out1] +[out2] + +[case testIncrementalDataclassesSubclassingCachedType] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + pass +reveal_type(B) + +[file a.py] +from dataclasses import dataclass + +@dataclass +class A: + x: int + +[builtins fixtures/list.pyi] +[out1] +main:7: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +[out2] +main:7: error: Revealed type is 'def (x: builtins.int) -> __main__.B' + +[case testIncrementalDataclassesArguments] +from a import Frozen, NoInit, NoCmp + +f = Frozen(5) +f.x = 6 + +g = NoInit() + +Frozen(1) < Frozen(2) +Frozen(1) <= Frozen(2) +Frozen(1) > Frozen(2) +Frozen(1) >= Frozen(2) + +NoCmp(1) < NoCmp(2) +NoCmp(1) <= NoCmp(2) +NoCmp(1) > NoCmp(2) +NoCmp(1) >= NoCmp(2) + +[file a.py] +from dataclasses import dataclass + +@dataclass(frozen=True, order=True) +class Frozen: + x: int + +@dataclass(init=False) +class NoInit: + x: int + +@dataclass(order=False) +class NoCmp: + x: int + +[builtins fixtures/list.pyi] +[rechecked] +[stale] +[out1] +main:4: error: Property "x" defined in "Frozen" is read-only +main:13: error: Unsupported left operand type for < ("NoCmp") +main:14: error: Unsupported left operand type for <= ("NoCmp") +main:15: error: Unsupported left operand type for > ("NoCmp") +main:16: error: Unsupported left operand type for >= ("NoCmp") + +[out2] +main:4: error: Property "x" defined in "Frozen" is read-only +main:13: error: Unsupported left operand type for < ("NoCmp") +main:14: error: Unsupported left operand type for <= ("NoCmp") +main:15: error: Unsupported left operand type for > ("NoCmp") +main:16: error: Unsupported left operand type for >= ("NoCmp") + +[case testIncrementalDataclassesDunder] +from a import A +reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> a.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +reveal_type(A.__ne__) # E: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +reveal_type(A.__le__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +reveal_type(A.__gt__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +reveal_type(A.__ge__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' + +A(1) < A(2) +A(1) <= A(2) +A(1) > A(2) +A(1) >= A(2) +A(1) == A(2) +A(1) != A(2) + +A(1) < 1 # E: Unsupported operand types for < ("A" and "int") +A(1) <= 1 # E: Unsupported operand types for <= ("A" and "int") +A(1) > 1 # E: Unsupported operand types for > ("A" and "int") +A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int") +A(1) == 1 +A(1) != 1 + +1 < A(1) # E: Unsupported operand types for > ("A" and "int") +1 <= A(1) # E: Unsupported operand types for >= ("A" and "int") +1 > A(1) # E: Unsupported operand types for < ("A" and "int") +1 >= A(1) # E: Unsupported operand types for <= ("A" and "int") +1 == A(1) +1 != A(1) + +[file a.py] +from dataclasses import dataclass + +@dataclass(order=True) +class A: + a: int + +[builtins fixtures/attr.pyi] +[rechecked] +[stale] +[out2] +main:2: error: Revealed type is 'def (a: builtins.int) -> a.A' +main:3: error: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +main:4: error: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +main:5: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +main:6: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +main:7: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +main:8: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +main:17: error: Unsupported operand types for < ("A" and "int") +main:18: error: Unsupported operand types for <= ("A" and "int") +main:19: error: Unsupported operand types for > ("A" and "int") +main:20: error: Unsupported operand types for >= ("A" and "int") +main:24: error: Unsupported operand types for > ("A" and "int") +main:25: error: Unsupported operand types for >= ("A" and "int") +main:26: error: Unsupported operand types for < ("A" and "int") +main:27: error: Unsupported operand types for <= ("A" and "int") + +[case testIncrementalDataclassesSubclassModified] +from b import B +B(5, 'foo') + +[file a.py] +from dataclasses import dataclass + +@dataclass +class A: + x: int + +[file b.py] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + y: str + +[file b.py.2] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + y: int + +[builtins fixtures/list.pyi] +[out1] +[out2] +main:2: error: Argument 2 to "B" has incompatible type "str"; expected "int" +[rechecked b] + +[case testIncrementalDataclassesSubclassModifiedErrorFirst] +from b import B +B(5, 'foo') + +[file a.py] +from dataclasses import dataclass + +@dataclass +class A: + x: int + +[file b.py] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + y: int + +[file b.py.2] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + y: str + +[builtins fixtures/list.pyi] +[out1] +main:2: error: Argument 2 to "B" has incompatible type "str"; expected "int" + +[out2] +[rechecked b] + +[case testIncrementalDataclassesThreeFiles] +from c import C +C(5, 'foo', True) + +[file a.py] +from dataclasses import dataclass + +@dataclass +class A: + a: int + +[file b.py] +from dataclasses import dataclass + +@dataclass +class B: + b: str + +[file c.py] +from a import A +from b import B +from dataclasses import dataclass + +@dataclass +class C(A, B): + c: bool + +[builtins fixtures/list.pyi] +[out1] +[out2] + +[case testIncrementalDataclassesThreeRuns] +from a import A +A(5) + +[file a.py] +from dataclasses import dataclass + +@dataclass +class A: + a: int + +[file a.py.2] +from dataclasses import dataclass + +@dataclass +class A: + a: str + +[file a.py.3] +from dataclasses import dataclass + +@dataclass +class A: + a: int = 6 + +[builtins fixtures/list.pyi] +[out1] +[out2] +main:2: error: Argument 1 to "A" has incompatible type "int"; expected "str" +[out3] \ No newline at end of file From 7c857cb9cce56a95b46e211b5fb80cdc75807cbf Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 28 May 2018 11:09:32 +0300 Subject: [PATCH 12/21] fix failing type check --- mypy/plugins/dataclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 04874b3cffdf..253c4b756c1c 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -188,11 +188,11 @@ def collect_attributes(self) -> List[DataclassAttribute]: has_field_call, field_args = _collect_field_args(stmt.rvalue) - is_in_init_param = ctx.api.parse_bool(field_args.get('init')) + is_in_init_param = field_args.get('init') if is_in_init_param is None: is_in_init = True else: - is_in_init = is_in_init_param + is_in_init = bool(ctx.api.parse_bool(is_in_init_param)) has_default = False # Ensure that something like x: int = field() is rejected From 37eaae82a441fd1fa404f5c6bb6632fba81c5ae0 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 28 May 2018 11:09:59 +0300 Subject: [PATCH 13/21] remove accidental modification to setup.cfg --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 086aa8b951eb..cfc72fbd89a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,3 @@ parallel = true [coverage:report] show_missing = true - -[isort] -multi_line_output = 5 \ No newline at end of file From 8d2dd6d9514460ae214f8f1c459c3d686758ab1b Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 4 Jun 2018 19:12:44 +0300 Subject: [PATCH 14/21] drop now-unnecessary "or" clause on dataclass plugin init --- mypy/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 5aba636fa7b4..f6bd02f92342 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -311,8 +311,7 @@ def get_class_decorator_hook(self, fullname: str attrs.attr_class_maker_callback, auto_attribs_default=True ) - # TODO: Drop the or clause once dataclasses lands in typeshed. - elif fullname in dataclasses.dataclass_makers or fullname.endswith('.dataclass'): + elif fullname in dataclasses.dataclass_makers: return dataclasses.dataclass_class_maker_callback return None From 9d08b7a156f8b32febb6ab8dc10dfd6f9c3c7a00 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 4 Jun 2018 19:23:03 +0300 Subject: [PATCH 15/21] implement InitVar support --- mypy/plugins/dataclasses.py | 47 +++++++++++++++++++++++---- test-data/unit/check-dataclasses.test | 31 +++++++++++++++++- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 253c4b756c1c..a2c19f6f5145 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -2,13 +2,16 @@ from typing import Dict, List, Optional, Set, Tuple, cast from mypy.nodes import ( - ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, Block, CallExpr, Context, - Decorator, Expression, JsonDict, NameExpr, SymbolTableNode, TempNode, - TypeInfo, Var + ARG_OPT, ARG_POS, MDEF, Argument, AssignmentStmt, Block, CallExpr, + Context, Decorator, Expression, FuncDef, JsonDict, NameExpr, + SymbolTableNode, TempNode, TypeInfo, Var, ) from mypy.plugin import ClassDefContext from mypy.plugins.common import _add_method, _get_decorator_bool_argument -from mypy.types import CallableType, NoneTyp, Type, TypeVarDef, TypeVarType +from mypy.types import ( + CallableType, Instance, NoneTyp, Type, TypeVarDef, TypeVarType, + deserialize_type +) from mypy.typevars import fill_typevars # The set of decorators that generate dataclasses. @@ -23,11 +26,14 @@ def __init__( self, name: str, is_in_init: bool, + is_init_var: bool, has_default: bool, - line: int, column: int, + line: int, + column: int, ) -> 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 @@ -47,13 +53,14 @@ def serialize(self) -> JsonDict: 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, } @classmethod - def deserialize(cls, data: JsonDict) -> 'DataclassAttribute': + def deserialize(cls, info: TypeInfo, data: JsonDict) -> 'DataclassAttribute': return cls(**data) @@ -147,6 +154,11 @@ def transform(self) -> None: if decorator_arguments['frozen']: self._freeze(attributes) + # Remove init-only vars from the class. + for attr in attributes: + if attr.is_init_var: + del info.names[attr.name] + info.metadata['dataclass'] = { 'attributes': OrderedDict((attr.name, attr.serialize()) for attr in attributes), 'frozen': decorator_arguments['frozen'], @@ -186,6 +198,15 @@ def collect_attributes(self) -> List[DataclassAttribute]: if node.is_classvar: continue + # x: InitVar[int] is turned into x: int and is removed from the class. + is_init_var = False + if ( + isinstance(node.type, Instance) and + node.type.type.fullname() == 'dataclasses.InitVar' + ): + is_init_var = True + node.type = node.type.args[0] + has_field_call, field_args = _collect_field_args(stmt.rvalue) is_in_init_param = field_args.get('init') @@ -208,6 +229,7 @@ def collect_attributes(self) -> List[DataclassAttribute]: attrs.append(DataclassAttribute( name=lhs.name, is_in_init=is_in_init, + is_init_var=is_init_var, has_default=has_default, line=stmt.line, column=stmt.column, @@ -217,13 +239,24 @@ def collect_attributes(self) -> List[DataclassAttribute]: # as long as those attributes weren't already collected. This # makes it possible to overwrite attributes in subclasses. super_attrs = [] + init_method = cls.info.get_method('__init__') for info in cls.info.mro[1:-1]: if 'dataclass' not in info.metadata: continue for name, data in info.metadata['dataclass']['attributes'].items(): if name not in known_attrs: - attr = DataclassAttribute.deserialize(data) + attr = DataclassAttribute.deserialize(info, data) + if attr.is_init_var and isinstance(init_method, FuncDef): + # 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. + for arg, arg_name in zip(init_method.arguments, init_method.arg_names): + if arg_name == attr.name: + cls.info.names[attr.name] = SymbolTableNode(MDEF, arg.variable) + known_attrs.add(name) super_attrs.append(attr) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 09271fc6a5e1..4cb8cee45b53 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -400,4 +400,33 @@ reveal_type(A) # E: Revealed type is 'def (b: __main__.B) -> __main__.A' A(b=B(42)) A(b=42) # E: Argument "b" to "A" has incompatible type "int"; expected "B" -[builtins fixtures/list.pyi] \ No newline at end of file +[builtins fixtures/list.pyi] + + +[case testDataclassesInitVars] +from dataclasses import InitVar, dataclass + +@dataclass +class Application: + name: str + database_name: InitVar[str] + +reveal_type(Application) # E: Revealed type is 'def (name: builtins.str, database_name: builtins.str) -> __main__.Application' +app = Application("example", 42) # E: Argument 2 to "Application" has incompatible type "int"; expected "str" +app = Application("example", "apps") +app.name +app.database_name # E: "Application" has no attribute "database_name" + + +@dataclass +class SpecializedApplication(Application): + rating: int + +reveal_type(SpecializedApplication) # E: Revealed type is 'def (name: builtins.str, database_name: builtins.str, rating: builtins.int) -> __main__.SpecializedApplication' +app = SpecializedApplication("example", "apps", "five") # E: Argument 3 to "SpecializedApplication" has incompatible type "str"; expected "int" +app = SpecializedApplication("example", "apps", 5) +app.name +app.rating +app.database_name # E: "SpecializedApplication" has no attribute "database_name" + +[builtins fixtures/list.pyi] From 8b20ceb838a5630acca413642da858496210c807 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 4 Jun 2018 20:04:36 +0300 Subject: [PATCH 16/21] update dataclasses.field --- test-data/unit/check-dataclasses.test | 2 +- test-data/unit/lib-stub/dataclasses.pyi | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 4cb8cee45b53..aa8bad16f505 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -120,7 +120,7 @@ from dataclasses import dataclass, field @dataclass class Person: name: str - age: int = field(init=None) # E: Argument "init" to "field" has incompatible type "None"; expected "bool" + age: int = field(init=None) # E: No overload variant of "field" matches argument type "None" [builtins fixtures/list.pyi] diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi index 1332964f96a6..881f9461f136 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Generic, Optional, TypeVar, overload +from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload _T = TypeVar('_T') _C = TypeVar('_C', bound=type) @@ -29,11 +29,17 @@ def dataclass(_cls: None = ..., frozen: bool = ...) -> Callable[[_C], _C]: ... +@overload +def field(*, default: _T, + init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., + metadata: Optional[Mapping[str, Any]] = ...) -> _T: ... + +@overload +def field(*, default_factory: Callable[[], _T], + init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., + metadata: Optional[Mapping[str, Any]] = ...) -> _T: ... + +@overload def field(*, - default: Optional[_T] = ..., - default_factory: Optional[Callable[..., _T]] = ..., - init: bool = ..., - repr: bool = ..., - hash: Optional[bool] = ..., - compare: bool = ..., - metadata: Any = ...) -> _T: ... + init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., + metadata: Optional[Mapping[str, Any]] = ...) -> Any: ... \ No newline at end of file From 5b78ad7a345d5d19614793e9862972d463b4fe1e Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 4 Jun 2018 20:13:03 +0300 Subject: [PATCH 17/21] update typeshed --- typeshed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typeshed b/typeshed index 2ba90a65c0cf..9ec6d476c447 160000 --- a/typeshed +++ b/typeshed @@ -1 +1 @@ -Subproject commit 2ba90a65c0cf4d196a971e1c0b0362bb735e8e6d +Subproject commit 9ec6d476c44729ea84e9cafde9c5a77610451b0c From 3b9c215100a0555c916a27668979963d91f1e861 Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 4 Jun 2018 20:13:23 +0300 Subject: [PATCH 18/21] Revert "drop now-unnecessary "or" clause on dataclass plugin init" This reverts commit 8d2dd6d9514460ae214f8f1c459c3d686758ab1b. --- mypy/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index f6bd02f92342..5aba636fa7b4 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -311,7 +311,8 @@ def get_class_decorator_hook(self, fullname: str attrs.attr_class_maker_callback, auto_attribs_default=True ) - elif fullname in dataclasses.dataclass_makers: + # TODO: Drop the or clause once dataclasses lands in typeshed. + elif fullname in dataclasses.dataclass_makers or fullname.endswith('.dataclass'): return dataclasses.dataclass_class_maker_callback return None From 55dfc7e6ff3e2c37eb54937830105d27df53255c Mon Sep 17 00:00:00 2001 From: Bogdan Popa Date: Mon, 4 Jun 2018 20:39:22 +0300 Subject: [PATCH 19/21] update incremental tests --- test-data/unit/check-incremental.test | 109 +++++++++++++++----------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index bbb0168725e8..578920bd8315 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4395,12 +4395,24 @@ class A: [out2] [case testIncrementalDataclassesSubclassingCachedType] +import b + +[file b.py] from a import A from dataclasses import dataclass @dataclass class B(A): pass + +[file b.py.2] +from a import A +from dataclasses import dataclass + +@dataclass +class B(A): + pass + reveal_type(B) [file a.py] @@ -4412,11 +4424,16 @@ class A: [builtins fixtures/list.pyi] [out1] -main:7: error: Revealed type is 'def (x: builtins.int) -> __main__.B' [out2] -main:7: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +tmp/b.py:8: error: Revealed type is 'def (x: builtins.int) -> b.B' [case testIncrementalDataclassesArguments] +import b + +[file b.py] +from a import Frozen, NoInit, NoCmp + +[file b.py.2] from a import Frozen, NoInit, NoCmp f = Frozen(5) @@ -4450,31 +4467,30 @@ class NoCmp: x: int [builtins fixtures/list.pyi] -[rechecked] -[stale] [out1] -main:4: error: Property "x" defined in "Frozen" is read-only -main:13: error: Unsupported left operand type for < ("NoCmp") -main:14: error: Unsupported left operand type for <= ("NoCmp") -main:15: error: Unsupported left operand type for > ("NoCmp") -main:16: error: Unsupported left operand type for >= ("NoCmp") - [out2] -main:4: error: Property "x" defined in "Frozen" is read-only -main:13: error: Unsupported left operand type for < ("NoCmp") -main:14: error: Unsupported left operand type for <= ("NoCmp") -main:15: error: Unsupported left operand type for > ("NoCmp") -main:16: error: Unsupported left operand type for >= ("NoCmp") +tmp/b.py:4: error: Property "x" defined in "Frozen" is read-only +tmp/b.py:13: error: Unsupported left operand type for < ("NoCmp") +tmp/b.py:14: error: Unsupported left operand type for <= ("NoCmp") +tmp/b.py:15: error: Unsupported left operand type for > ("NoCmp") +tmp/b.py:16: error: Unsupported left operand type for >= ("NoCmp") [case testIncrementalDataclassesDunder] +import b + +[file b.py] from a import A -reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> a.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' -reveal_type(A.__ne__) # E: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -reveal_type(A.__le__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -reveal_type(A.__gt__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -reveal_type(A.__ge__) # E: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' + +[file b.py.2] +from a import A + +reveal_type(A) +reveal_type(A.__eq__) +reveal_type(A.__ne__) +reveal_type(A.__lt__) +reveal_type(A.__le__) +reveal_type(A.__gt__) +reveal_type(A.__ge__) A(1) < A(2) A(1) <= A(2) @@ -4483,17 +4499,17 @@ A(1) >= A(2) A(1) == A(2) A(1) != A(2) -A(1) < 1 # E: Unsupported operand types for < ("A" and "int") -A(1) <= 1 # E: Unsupported operand types for <= ("A" and "int") -A(1) > 1 # E: Unsupported operand types for > ("A" and "int") -A(1) >= 1 # E: Unsupported operand types for >= ("A" and "int") +A(1) < 1 +A(1) <= 1 +A(1) > 1 +A(1) >= 1 A(1) == 1 A(1) != 1 -1 < A(1) # E: Unsupported operand types for > ("A" and "int") -1 <= A(1) # E: Unsupported operand types for >= ("A" and "int") -1 > A(1) # E: Unsupported operand types for < ("A" and "int") -1 >= A(1) # E: Unsupported operand types for <= ("A" and "int") +1 < A(1) +1 <= A(1) +1 > A(1) +1 >= A(1) 1 == A(1) 1 != A(1) @@ -4505,24 +4521,23 @@ class A: a: int [builtins fixtures/attr.pyi] -[rechecked] -[stale] +[out1] [out2] -main:2: error: Revealed type is 'def (a: builtins.int) -> a.A' -main:3: error: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' -main:4: error: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' -main:5: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -main:6: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -main:7: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -main:8: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' -main:17: error: Unsupported operand types for < ("A" and "int") -main:18: error: Unsupported operand types for <= ("A" and "int") -main:19: error: Unsupported operand types for > ("A" and "int") -main:20: error: Unsupported operand types for >= ("A" and "int") -main:24: error: Unsupported operand types for > ("A" and "int") -main:25: error: Unsupported operand types for >= ("A" and "int") -main:26: error: Unsupported operand types for < ("A" and "int") -main:27: error: Unsupported operand types for <= ("A" and "int") +tmp/b.py:3: error: Revealed type is 'def (a: builtins.int) -> a.A' +tmp/b.py:4: error: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +tmp/b.py:5: error: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +tmp/b.py:6: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +tmp/b.py:7: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +tmp/b.py:8: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +tmp/b.py:9: error: Revealed type is 'def [T] (self: T`1, other: T`1) -> builtins.bool' +tmp/b.py:18: error: Unsupported operand types for < ("A" and "int") +tmp/b.py:19: error: Unsupported operand types for <= ("A" and "int") +tmp/b.py:20: error: Unsupported operand types for > ("A" and "int") +tmp/b.py:21: error: Unsupported operand types for >= ("A" and "int") +tmp/b.py:25: error: Unsupported operand types for > ("A" and "int") +tmp/b.py:26: error: Unsupported operand types for >= ("A" and "int") +tmp/b.py:27: error: Unsupported operand types for < ("A" and "int") +tmp/b.py:28: error: Unsupported operand types for <= ("A" and "int") [case testIncrementalDataclassesSubclassModified] from b import B From 988c6e8fb61a6738657faae1be2ff3c982e371c1 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 5 Jun 2018 17:00:30 +0100 Subject: [PATCH 20/21] Some polish --- mypy/plugin.py | 3 +-- test-data/unit/check-incremental.test | 9 +++++++++ test-data/unit/lib-stub/dataclasses.pyi | 23 ++++------------------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 5aba636fa7b4..f6bd02f92342 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -311,8 +311,7 @@ def get_class_decorator_hook(self, fullname: str attrs.attr_class_maker_callback, auto_attribs_default=True ) - # TODO: Drop the or clause once dataclasses lands in typeshed. - elif fullname in dataclasses.dataclass_makers or fullname.endswith('.dataclass'): + elif fullname in dataclasses.dataclass_makers: return dataclasses.dataclass_class_maker_callback return None diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 578920bd8315..3f537922d9e3 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -4624,6 +4624,14 @@ from dataclasses import dataclass class B: b: str +[file b.py.2] +from dataclasses import dataclass + +@dataclass +class B: + b: str + c: str + [file c.py] from a import A from b import B @@ -4636,6 +4644,7 @@ class C(A, B): [builtins fixtures/list.pyi] [out1] [out2] +tmp/c.py:7: error: Incompatible types in assignment (expression has type "bool", base class "B" defined the type as "str") [case testIncrementalDataclassesThreeRuns] from a import A diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi index 881f9461f136..44e8f06b1807 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -1,32 +1,17 @@ -from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload +from typing import Any, Callable, Generic, Mapping, Optional, TypeVar, overload, Type _T = TypeVar('_T') -_C = TypeVar('_C', bound=type) class InitVar(Generic[_T]): ... @overload -def dataclass(_cls: _C, - *, - init: bool = ..., - repr: bool = ..., - eq: bool = ..., - order: bool = ..., - unsafe_hash: bool = ..., - frozen: bool = ...) -> _C: ... - +def dataclass(_cls: Type[_T]) -> Type[_T]: ... @overload -def dataclass(_cls: None = ..., - *, - init: bool = ..., - repr: bool = ..., - eq: bool = ..., - order: bool = ..., - unsafe_hash: bool = ..., - frozen: bool = ...) -> Callable[[_C], _C]: ... +def dataclass(*, init: bool = ..., repr: bool = ..., eq: bool = ..., order: bool = ..., + unsafe_hash: bool = ..., frozen: bool = ...) -> Callable[[Type[_T]], Type[_T]]: ... @overload From 747473e89a8735e74851a8cc489e2d0284414526 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 5 Jun 2018 17:38:22 +0100 Subject: [PATCH 21/21] Fix newline --- test-data/unit/lib-stub/dataclasses.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/lib-stub/dataclasses.pyi b/test-data/unit/lib-stub/dataclasses.pyi index 44e8f06b1807..160cfcd066ba 100644 --- a/test-data/unit/lib-stub/dataclasses.pyi +++ b/test-data/unit/lib-stub/dataclasses.pyi @@ -27,4 +27,4 @@ def field(*, default_factory: Callable[[], _T], @overload def field(*, init: bool = ..., repr: bool = ..., hash: Optional[bool] = ..., compare: bool = ..., - metadata: Optional[Mapping[str, Any]] = ...) -> Any: ... \ No newline at end of file + metadata: Optional[Mapping[str, Any]] = ...) -> Any: ...