From 005547ac28da528aa503fbb5d9ffa055a9184790 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 01/81] WIP: attrs_plugin_real --- mypy/plugin.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index 4ffa9395afc5..d35cd8440e53 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -379,3 +379,112 @@ def int_pow_callback(ctx: MethodContext) -> Type: else: return ctx.api.named_generic_type('builtins.float', []) return ctx.default_return_type + + +def add_method( + info: TypeInfo, + method_name: str, + args: List[Argument], + ret_type: Type, + self_type: Type, + function_type: Instance) -> None: + from mypy.semanal import set_callable_name + + first = [Argument(Var('self'), self_type, None, ARG_POS)] + args = first + args + + arg_types = [arg.type_annotation for arg in args] + arg_names = [arg.variable.name() for arg in args] + arg_kinds = [arg.kind for arg in args] + assert None not in arg_types + signature = CallableType(arg_types, arg_kinds, arg_names, + ret_type, function_type) + func = FuncDef(method_name, args, Block([])) + func.info = info + func.is_class = False + func.type = set_callable_name(signature, func) + func._fullname = info.fullname() + '.' + method_name + info.names[method_name] = SymbolTableNode(MDEF, func) + + + +def attr_s_callback(ctx: ClassDefContext) -> None: + """Add an __init__ method to classes decorated with attr.s.""" + # TODO: Add __cmp__ methods. + + def get_bool_argument(call: CallExpr, name: str, default: bool): + for arg_name, arg_value in zip(call.arg_names, call.args): + if arg_name == name: + # TODO: Handle None being returned here. + return ctx.api.parse_bool(arg_value) + return default + + def called_function(expr: Expression): + if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): + return expr.callee.fullname + + decorator = ctx.context + if isinstance(decorator, CallExpr): + # Update init and auto_attrib if this was a call. + init = get_bool_argument(decorator, "init", True) + auto_attribs = get_bool_argument(decorator, "auto_attribs", False) + else: + # Default values of attr.s() + init = True + auto_attribs = False + + if not init: + print("Nothing to do", init) + return + + print(f"{ctx.cls.info.fullname()} init={init} auto={auto_attribs}") + + info = ctx.cls.info + + # Walk the body looking for assignments. + items = [] # type: List[str] + types = [] # type: List[Type] + rhs = {} # type: Dict[str, Expression] + for stmt in ctx.cls.defs.body: + if isinstance(stmt, AssignmentStmt): + name = stmt.lvalues[0].name + # print(name, stmt.type, stmt.rvalue) + items.append(name) + types.append(None + if stmt.type is None + else ctx.api.anal_type(stmt.type)) + + + if isinstance(stmt.rvalue, TempNode): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + if rhs: + print("DEFAULT ISSUE") + elif called_function(stmt.rvalue) == 'attr.ib': + # Look for a default value in the call. + expr = stmt.rvalue + print(f"{name} = attr.ib(...)") + else: + print(f"{name} = {stmt.rvalue}") + rhs[name] = stmt.rvalue + + any_type = AnyType(TypeOfAny.unannotated) + + import pdb; pdb.set_trace() + + has_default = {} # type: Dict[str, Expression] + args = [] + for name, table in info.names.items(): + if isinstance(table.node, Var) and table.type: + var = Var(name.lstrip("_"), table.type) + default = has_default.get(var.name(), None) + kind = ARG_POS if default is None else ARG_OPT + args.append(Argument(var, var.type, default, kind)) + + add_method( + info=info, + method_name='__init__', + args=args, + ret_type=NoneTyp(), + self_type=ctx.api.named_type(info.name()), + function_type=ctx.api.named_type('__builtins__.function'), + ) \ No newline at end of file From ecd0164268b58ab9008c67243ca0dcfa07944bc7 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 02/81] Handle untyped attrs --- mypy/plugin.py | 65 +++++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index d35cd8440e53..ced5ce207ef8 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -4,7 +4,10 @@ from abc import abstractmethod from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar -from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef +from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ + DictExpr, ClassDef, Argument, Var, TypeInfo, FuncDef, Block, \ + SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, \ + ARG_POS, ARG_OPT, EllipsisExpr from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny @@ -262,6 +265,11 @@ def get_method_hook(self, fullname: str return int_pow_callback return None + def get_class_decorator_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + if fullname == 'attr.s': + return attr_s_callback + def open_callback(ctx: FunctionContext) -> Type: """Infer a better return type for 'open'. @@ -407,7 +415,6 @@ def add_method( info.names[method_name] = SymbolTableNode(MDEF, func) - def attr_s_callback(ctx: ClassDefContext) -> None: """Add an __init__ method to classes decorated with attr.s.""" # TODO: Add __cmp__ methods. @@ -419,11 +426,19 @@ def get_bool_argument(call: CallExpr, name: str, default: bool): return ctx.api.parse_bool(arg_value) return default + def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]): + for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): + if num is not None and i == num: + return attr_value + if name and attr_name == name: + return attr_value + return None + def called_function(expr: Expression): if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): return expr.callee.fullname - decorator = ctx.context + decorator = ctx.reason if isinstance(decorator, CallExpr): # Update init and auto_attrib if this was a call. init = get_bool_argument(decorator, "init", True) @@ -442,43 +457,45 @@ def called_function(expr: Expression): info = ctx.cls.info # Walk the body looking for assignments. - items = [] # type: List[str] + names = [] # type: List[str] types = [] # type: List[Type] - rhs = {} # type: Dict[str, Expression] + has_default = set() # type: Set[str] for stmt in ctx.cls.defs.body: if isinstance(stmt, AssignmentStmt): - name = stmt.lvalues[0].name - # print(name, stmt.type, stmt.rvalue) - items.append(name) - types.append(None - if stmt.type is None - else ctx.api.anal_type(stmt.type)) - + name = stmt.lvalues[0].name.lstrip("_") + typ = (AnyType(TypeOfAny.unannotated) if stmt.type is None + else ctx.api.anal_type(stmt.type)) if isinstance(stmt.rvalue, TempNode): + print(f"{name}: {typ}") # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - if rhs: + if has_default: print("DEFAULT ISSUE") elif called_function(stmt.rvalue) == 'attr.ib': # Look for a default value in the call. - expr = stmt.rvalue - print(f"{name} = attr.ib(...)") + if get_argument(stmt.rvalue, "default", 0): + has_default.add(name) + print(f"{name} = attr.ib(default=...)") + else: + if has_default: + ctx.api.fail("Non-default attributes not allowed after default attributes.", stmt.rvalue) + print(f"{name} = attr.ib()") + + names.append(name) + types.append(typ) else: print(f"{name} = {stmt.rvalue}") - rhs[name] = stmt.rvalue + # rhs[name] = stmt.rvalue any_type = AnyType(TypeOfAny.unannotated) - import pdb; pdb.set_trace() + print(names, types, has_default) - has_default = {} # type: Dict[str, Expression] args = [] - for name, table in info.names.items(): - if isinstance(table.node, Var) and table.type: - var = Var(name.lstrip("_"), table.type) - default = has_default.get(var.name(), None) - kind = ARG_POS if default is None else ARG_OPT - args.append(Argument(var, var.type, default, kind)) + for (name, typ) in zip(names, types): + var = Var(name, typ) + kind = ARG_OPT if name in has_default else ARG_POS + args.append(Argument(var, var.type, EllipsisExpr(), kind)) add_method( info=info, From 8f4dbd7c6de3f3017d844dfe765f18e8f2e2142b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 03/81] Handle auto_attribs and refactor --- mypy/plugin.py | 95 ++++++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index ced5ce207ef8..7f86890c390a 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,12 +2,14 @@ from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, \ + cast +from mypy import messages from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ DictExpr, ClassDef, Argument, Var, TypeInfo, FuncDef, Block, \ SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, \ - ARG_POS, ARG_OPT, EllipsisExpr + ARG_POS, ARG_OPT, EllipsisExpr, NameExpr from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny @@ -269,6 +271,7 @@ def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: if fullname == 'attr.s': return attr_s_callback + return None def open_callback(ctx: FunctionContext) -> Type: @@ -405,7 +408,7 @@ def add_method( arg_names = [arg.variable.name() for arg in args] arg_kinds = [arg.kind for arg in args] assert None not in arg_types - signature = CallableType(arg_types, arg_kinds, arg_names, + signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, ret_type, function_type) func = FuncDef(method_name, args, Block([])) func.info = info @@ -419,24 +422,25 @@ def attr_s_callback(ctx: ClassDefContext) -> None: """Add an __init__ method to classes decorated with attr.s.""" # TODO: Add __cmp__ methods. - def get_bool_argument(call: CallExpr, name: str, default: bool): + def get_bool_argument(call: CallExpr, name: str, default: Optional[bool]) -> Optional[bool]: for arg_name, arg_value in zip(call.arg_names, call.args): if arg_name == name: # TODO: Handle None being returned here. return ctx.api.parse_bool(arg_value) return default - def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]): + def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): - if num is not None and i == num: + if num is not None and not attr_name and i == num: return attr_value if name and attr_name == name: return attr_value return None - def called_function(expr: Expression): + def called_function(expr: Expression) -> Optional[str]: if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): return expr.callee.fullname + return None decorator = ctx.reason if isinstance(decorator, CallExpr): @@ -449,7 +453,6 @@ def called_function(expr: Expression): auto_attribs = False if not init: - print("Nothing to do", init) return print(f"{ctx.cls.info.fullname()} init={init} auto={auto_attribs}") @@ -460,48 +463,56 @@ def called_function(expr: Expression): names = [] # type: List[str] types = [] # type: List[Type] has_default = set() # type: Set[str] + + def add_init_argument(name: str, typ:Optional[Type], default: bool, context:Context) -> None: + if not default and has_default: + ctx.api.fail( + "Non-default attributes not allowed after default attributes.", + context) + if not typ: + ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) + typ = AnyType(TypeOfAny.unannotated) + + names.append(name) + assert typ is not None + types.append(typ) + if default: + has_default.add(name) + + def is_class_var(expr: NameExpr) -> bool: + # import pdb; pdb.set_trace() + if isinstance(expr.node, Var): + return expr.node.is_classvar + return False + for stmt in ctx.cls.defs.body: - if isinstance(stmt, AssignmentStmt): - name = stmt.lvalues[0].name.lstrip("_") - typ = (AnyType(TypeOfAny.unannotated) if stmt.type is None - else ctx.api.anal_type(stmt.type)) - - if isinstance(stmt.rvalue, TempNode): - print(f"{name}: {typ}") - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - if has_default: - print("DEFAULT ISSUE") - elif called_function(stmt.rvalue) == 'attr.ib': + if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): + lhs = stmt.lvalues[0] + name = lhs.name.lstrip("_") + typ = stmt.type + print(name, typ, is_class_var(lhs)) + + if called_function(stmt.rvalue) == 'attr.ib': # Look for a default value in the call. - if get_argument(stmt.rvalue, "default", 0): - has_default.add(name) - print(f"{name} = attr.ib(default=...)") - else: - if has_default: - ctx.api.fail("Non-default attributes not allowed after default attributes.", stmt.rvalue) - print(f"{name} = attr.ib()") - - names.append(name) - types.append(typ) + assert isinstance(stmt.rvalue, CallExpr) + add_init_argument(name, typ, bool(get_argument(stmt.rvalue, "default", 0)), stmt) else: - print(f"{name} = {stmt.rvalue}") - # rhs[name] = stmt.rvalue - - any_type = AnyType(TypeOfAny.unannotated) - - print(names, types, has_default) + if auto_attribs and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(stmt.rvalue, TempNode) + add_init_argument(name, typ, has_rhs, stmt) - args = [] - for (name, typ) in zip(names, types): - var = Var(name, typ) - kind = ARG_OPT if name in has_default else ARG_POS - args.append(Argument(var, var.type, EllipsisExpr(), kind)) + init_args = [ + Argument(Var(name, typ), typ, EllipsisExpr(), + ARG_OPT if name in has_default else ARG_POS) + for (name, typ) in zip(names, types) + ] add_method( info=info, method_name='__init__', - args=args, + args=init_args, ret_type=NoneTyp(), self_type=ctx.api.named_type(info.name()), function_type=ctx.api.named_type('__builtins__.function'), - ) \ No newline at end of file + ) From 2b55334c7b057156faaba6bfc046a6a0b12d7649 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 04/81] WIP: attrs_plugin --- attr.pyi | 83 ++++++++++++++++++++++++++++++++++++++++++ foo.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 attr.pyi create mode 100644 foo.py diff --git a/attr.pyi b/attr.pyi new file mode 100644 index 000000000000..48d5a884ec61 --- /dev/null +++ b/attr.pyi @@ -0,0 +1,83 @@ +from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, \ + Mapping, Tuple, Type, TypeVar, Union, overload, Sequence + +# `import X as X` is required to expose these to mypy. otherwise they are invisible +#from . import exceptions as exceptions +#from . import filters as filters +#from . import converters as converters +#from . import validators as validators + +# typing -- + +_T = TypeVar('_T') +_C = TypeVar('_C', bound=type) +_M = TypeVar('_M', bound=Mapping) +_I = TypeVar('_I', bound=Sequence) + +_ValidatorType = Callable[[Any, 'Attribute', _T], Any] +_ConverterType = Callable[[Any], _T] +_FilterType = Callable[['Attribute', Any], bool] + +# _make -- + +NOTHING : object + +# Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` +def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... + +class Attribute(Generic[_T]): + __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") + name: str + default: Any + validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] + repr: bool + cmp: bool + hash: Optional[bool] + init: bool + convert: Optional[_ConverterType[_T]] + metadata: Mapping + type: Optional[Type[_T]] + +@overload +def attr(default: _T = ..., validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., type: Any = ...) -> _T: ... +@overload +def attr(*, validator: Optional[Union[_ValidatorType[Any], List[_ValidatorType[Any]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[Any]] = ..., metadata: Mapping = ...): ... + + +@overload +def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... +@overload +def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... + +def fields(cls: type) -> Tuple[Attribute, ...]: ... +def validate(inst: Any) -> None: ... + +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid +def make_class(name: str, attrs: Union[List[Any], Dict[str, Any]], bases: Tuple[type, ...] = ..., + these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> type: ... + +# _funcs -- +# FIXME: Overloads don't work. +#@overload +#def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... +#@overload +#def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... + +#@overload +#def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... +#@overload +#def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... + +def has(cls: type) -> bool: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases +s = attrs = attributes +ib = attrib = attr + diff --git a/foo.py b/foo.py new file mode 100644 index 000000000000..94d19a07fac7 --- /dev/null +++ b/foo.py @@ -0,0 +1,107 @@ +import attr +import typing +from attr import attributes + +# from typing import Optional +from typing import List + +@attr.s(auto_attribs=True) +class Auto: + normal: int + _private: int + def_arg: int = 18 + _def_kwarg: int = attr.ib(validator=None, default=18) + + FOO: typing.ClassVar[int] = 18 + +reveal_type(Auto) +Auto(1, 2, 3) + + +@attr.s +class Typed: + normal: int = attr.ib() + _private: int = attr.ib() + def_arg: int = attr.ib(18) + _def_kwarg: int = attr.ib(validator=None, default=18) + + FOO: int = 18 + +reveal_type(Typed) +Typed(1, 2, 3) + + +@attr.s +class UnTyped: + normal = attr.ib() + _private = attr.ib() + def_arg = attr.ib(18) + _def_kwarg = attr.ib(validator=None, default=18) + + FOO = 18 + +reveal_type(UnTyped) +UnTyped(1, 2, 3) + +# @attr.s +# class UnTyped: +# pass +# +# +# @attributes +# class UnTyped: +# pass +# +# +# class Bar(UnTyped, List[str], typing.Mapping, List()): +# pass +# +# +# UnTyped(6, '17') +# + + + +# @attr.s +# class NonAuto: +# a: int = attr.ib() +# b: int = attr.ib(default=18) +# +# cls_var = 18 +# +# def stuff(self) -> int: +# return self.b + self.c + + + +# @attr.s(auto_attribs=True) +# class Auto: +# b: int +# _parent: Optional['Auto'] = None +# +# def stuff(self) -> int: +# return self.b +# +# +# @attr.s(init=False, auto_attribs=True) +# class NoInit: +# b: int +# +# def __init__(self, b: int = 18) -> None: +# pass +# +# +# @attr.s +# class UnTyped: +# b = attr.ib() +# c = attr.ib(default=18) +# +# +# if __name__ == "__main__": +# Auto(7, None) +# Auto(18, Auto(7, None)) +# a = Auto(7, None) +# b = Auto(8, None) +# +# if a > b: +# print("yay") From 83b008c2c11f84859687f01f8fe7148fd17c79fc Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 05/81] WIP: attrs_plugin --- foo.py | 65 -------------------------------------------------- mypy/plugin.py | 1 - 2 files changed, 66 deletions(-) diff --git a/foo.py b/foo.py index 94d19a07fac7..a2935dcf9afa 100644 --- a/foo.py +++ b/foo.py @@ -1,9 +1,6 @@ import attr import typing -from attr import attributes -# from typing import Optional -from typing import List @attr.s(auto_attribs=True) class Auto: @@ -43,65 +40,3 @@ class UnTyped: reveal_type(UnTyped) UnTyped(1, 2, 3) -# @attr.s -# class UnTyped: -# pass -# -# -# @attributes -# class UnTyped: -# pass -# -# -# class Bar(UnTyped, List[str], typing.Mapping, List()): -# pass -# -# -# UnTyped(6, '17') -# - - - -# @attr.s -# class NonAuto: -# a: int = attr.ib() -# b: int = attr.ib(default=18) -# -# cls_var = 18 -# -# def stuff(self) -> int: -# return self.b + self.c - - - -# @attr.s(auto_attribs=True) -# class Auto: -# b: int -# _parent: Optional['Auto'] = None -# -# def stuff(self) -> int: -# return self.b -# -# -# @attr.s(init=False, auto_attribs=True) -# class NoInit: -# b: int -# -# def __init__(self, b: int = 18) -> None: -# pass -# -# -# @attr.s -# class UnTyped: -# b = attr.ib() -# c = attr.ib(default=18) -# -# -# if __name__ == "__main__": -# Auto(7, None) -# Auto(18, Auto(7, None)) -# a = Auto(7, None) -# b = Auto(8, None) -# -# if a > b: -# print("yay") diff --git a/mypy/plugin.py b/mypy/plugin.py index 7f86890c390a..15f09b17ca6b 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -490,7 +490,6 @@ def is_class_var(expr: NameExpr) -> bool: lhs = stmt.lvalues[0] name = lhs.name.lstrip("_") typ = stmt.type - print(name, typ, is_class_var(lhs)) if called_function(stmt.rvalue) == 'attr.ib': # Look for a default value in the call. From 3bc5c1161c201970b83213ca3946365bbc02820e Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 06/81] WIP: attrs_plugin --- mypy/semanal.py | 2 + mypy/test/testcheck.py | 1 + test-data/unit/check-attr.test | 86 ++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 test-data/unit/check-attr.test diff --git a/mypy/semanal.py b/mypy/semanal.py index f94b836d783c..e0d8d248b255 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1684,6 +1684,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.analyze_lvalue(lval, explicit_type=s.type is not None) self.check_classvar(s) s.rvalue.accept(self) + #if s.lvalues[0].name == 'FOO': + # import pdb; pdb.set_trace() if s.type: allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr)) s.type = self.anal_type(s.type, allow_tuple_literal=allow_tuple_literal) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 0bdf645e25c7..26d058423413 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -75,6 +75,7 @@ 'check-incomplete-fixture.test', 'check-custom-plugin.test', 'check-default-plugin.test', + 'check-attr.test', ] diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test new file mode 100644 index 000000000000..565098766b91 --- /dev/null +++ b/test-data/unit/check-attr.test @@ -0,0 +1,86 @@ +[case testUntypedAttrS] +import attr + +@attr.s +class UnTyped: + normal = attr.ib() # E: Need type annotation for variable + _private = attr.ib() # E: Need type annotation for variable + def_arg = attr.ib(18) # E: Need type annotation for variable + _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable + + CLASS_VAR = 18 + +reveal_type(UnTyped) # E: Revealed type is 'def (normal: Any, private: Any, def_arg: Any =, def_kwarg: Any =) -> __main__.UnTyped' + +UnTyped(1, 2) +UnTyped('1', '2', '3') +UnTyped('1', '2', '3', 4) +UnTyped('1', '2', def_kwarg='5') +UnTyped('1', '2', def_kwarg='5') +UnTyped(normal=1, private=2, def_arg=3, def_kwarg=4) +UnTyped() # E: Too few arguments for "UnTyped" + +[file attr.pyi] +from typing import TypeVar +_T = TypeVar('_T') +def ib(default: _T = ..., validator = ...) -> _T: ... +def s(cls: _T) -> _T: ... + +[case testTypedAttrS] +import attr + +@attr.s +class Typed: + normal: int = attr.ib() + _private: int = attr.ib() + def_arg: int = attr.ib(18) + _def_kwarg: int = attr.ib(validator=None, default=18) + + FOO: int = 18 + +reveal_type(Typed) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Typed' + +Typed(1, 2) +Typed(1, 2, 3) +Typed(1, 2, 3, 4) +Typed(1, 2, def_kwarg=5) +Typed(1, 2, def_kwarg=5) +Typed(normal=1, private=2, def_arg=3, def_kwarg=4) +Typed() # E: Too few arguments for "Typed" + +[file attr.pyi] +from typing import TypeVar +_T = TypeVar('_T') +def ib(default: _T = ..., validator = ...) -> _T: ... +def s(cls: _T) -> _T: ... + +[case testAutoAttribAttrS] +import attr +import typing + +@attr.s(auto_attribs=True) +class Auto: + normal: int + _private: int + def_arg: int = 18 + _def_kwarg: int = attr.ib(validator=None, default=18) + + FOO: typing.ClassVar[int] = 18 + +reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' + +Auto(1, 2) +Auto(1, 2, 3) +Auto(1, 2, 3, 4) +Auto(1, 2, def_kwarg=5) +Auto(1, 2, def_kwarg=5) +Auto(normal=1, private=2, def_arg=3, def_kwarg=4) +Auto() # E: Too few arguments for "Auto" + + +[file attr.pyi] +from typing import TypeVar +_T = TypeVar('_T') +def ib(default: _T = ..., validator = ...) -> _T: ... +def s(cls: _T) -> _T: ... + From aa55fe57f92a133adf9af555ca8d4324124cca60 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 07/81] WIP: attrs_plugin --- foo.py | 1 + mypy/plugin.py | 12 ++++++++---- mypy/semanal.py | 2 -- test-data/unit/check-attr.test | 7 +++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/foo.py b/foo.py index a2935dcf9afa..016a0a3248cc 100644 --- a/foo.py +++ b/foo.py @@ -38,5 +38,6 @@ class UnTyped: FOO = 18 reveal_type(UnTyped) +reveal_type(UnTyped.FOO) UnTyped(1, 2, 3) diff --git a/mypy/plugin.py b/mypy/plugin.py index 15f09b17ca6b..8efadc8afee9 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -464,7 +464,8 @@ def called_function(expr: Expression) -> Optional[str]: types = [] # type: List[Type] has_default = set() # type: Set[str] - def add_init_argument(name: str, typ:Optional[Type], default: bool, context:Context) -> None: + def add_init_argument(name: str, typ: Optional[Type], default: bool, + context: Context) -> None: if not default and has_default: ctx.api.fail( "Non-default attributes not allowed after default attributes.", @@ -480,7 +481,6 @@ def add_init_argument(name: str, typ:Optional[Type], default: bool, context:Cont has_default.add(name) def is_class_var(expr: NameExpr) -> bool: - # import pdb; pdb.set_trace() if isinstance(expr.node, Var): return expr.node.is_classvar return False @@ -494,9 +494,13 @@ def is_class_var(expr: NameExpr) -> bool: if called_function(stmt.rvalue) == 'attr.ib': # Look for a default value in the call. assert isinstance(stmt.rvalue, CallExpr) - add_init_argument(name, typ, bool(get_argument(stmt.rvalue, "default", 0)), stmt) + add_init_argument( + name, + typ, + bool(get_argument(stmt.rvalue, "default", 0)), + stmt) else: - if auto_attribs and not is_class_var(lhs): + if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(stmt.rvalue, TempNode) add_init_argument(name, typ, has_rhs, stmt) diff --git a/mypy/semanal.py b/mypy/semanal.py index e0d8d248b255..f94b836d783c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1684,8 +1684,6 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.analyze_lvalue(lval, explicit_type=s.type is not None) self.check_classvar(s) s.rvalue.accept(self) - #if s.lvalues[0].name == 'FOO': - # import pdb; pdb.set_trace() if s.type: allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr)) s.type = self.anal_type(s.type, allow_tuple_literal=allow_tuple_literal) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 565098766b91..97208681c0e7 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -28,6 +28,7 @@ def s(cls: _T) -> _T: ... [case testTypedAttrS] import attr +import typing @attr.s class Typed: @@ -36,7 +37,8 @@ class Typed: def_arg: int = attr.ib(18) _def_kwarg: int = attr.ib(validator=None, default=18) - FOO: int = 18 + UNTYPED_CLASS_VAR = 5+2 + TYPED_CLASS_VAR: typing.ClassVar[int] = 22 reveal_type(Typed) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Typed' @@ -65,7 +67,8 @@ class Auto: def_arg: int = 18 _def_kwarg: int = attr.ib(validator=None, default=18) - FOO: typing.ClassVar[int] = 18 + UNTYPED_CLASS_VAR = 18 + TYPED_CLASS_VAR: typing.ClassVar[int] = 18 reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' From 5f897b756d186e6333500dbd7544b3da36512b4c Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 08/81] WIP: attrs_plugin --- mypy/plugin.py | 31 ++++++++++++++++++++++--------- test-data/unit/check-attr.test | 30 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 8efadc8afee9..4951499b75a7 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -269,8 +269,8 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - if fullname == 'attr.s': - return attr_s_callback + if fullname in attr_class_makers: + return attr_class_maker_callback return None @@ -418,10 +418,21 @@ def add_method( info.names[method_name] = SymbolTableNode(MDEF, func) -def attr_s_callback(ctx: ClassDefContext) -> None: - """Add an __init__ method to classes decorated with attr.s.""" - # TODO: Add __cmp__ methods. +attr_class_makers = { + 'attr.s', + 'attr.attrs', + 'attr.attributes', +} + +attr_attrib_makers = { + 'attr.ib', + 'attr.attrib', + 'attr.attr' +} + +def attr_class_maker_callback(ctx: ClassDefContext) -> None: + """Add an __init__ method to classes decorated with attr.s.""" def get_bool_argument(call: CallExpr, name: str, default: Optional[bool]) -> Optional[bool]: for arg_name, arg_value in zip(call.arg_names, call.args): if arg_name == name: @@ -455,7 +466,7 @@ def called_function(expr: Expression) -> Optional[str]: if not init: return - print(f"{ctx.cls.info.fullname()} init={init} auto={auto_attribs}") + # print(f"{ctx.cls.info.fullname()} init={init} auto={auto_attribs}") info = ctx.cls.info @@ -491,13 +502,14 @@ def is_class_var(expr: NameExpr) -> bool: name = lhs.name.lstrip("_") typ = stmt.type - if called_function(stmt.rvalue) == 'attr.ib': - # Look for a default value in the call. + if called_function(stmt.rvalue) in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) + # Look for default= in the call. + default = get_argument(stmt.rvalue, "default", 0) add_init_argument( name, typ, - bool(get_argument(stmt.rvalue, "default", 0)), + bool(default), stmt) else: if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): @@ -519,3 +531,4 @@ def is_class_var(expr: NameExpr) -> bool: self_type=ctx.api.named_type(info.name()), function_type=ctx.api.named_type('__builtins__.function'), ) + # TODO: Add __cmp__ methods. diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 97208681c0e7..4b3ad61e7d32 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -87,3 +87,33 @@ _T = TypeVar('_T') def ib(default: _T = ..., validator = ...) -> _T: ... def s(cls: _T) -> _T: ... +[case testSeriousNamesAttrS] +import typing +from attr import attrib, attrs + +@attrs(auto_attribs=True) +class Auto: + normal: int + _private: int + def_arg: int = 18 + _def_kwarg: int = attrib(validator=None, default=18) + + UNTYPED_CLASS_VAR = 18 + TYPED_CLASS_VAR: typing.ClassVar[int] = 18 + +reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' + +Auto(1, 2) +Auto(1, 2, 3) +Auto(1, 2, 3, 4) +Auto(1, 2, def_kwarg=5) +Auto(1, 2, def_kwarg=5) +Auto(normal=1, private=2, def_arg=3, def_kwarg=4) +Auto() # E: Too few arguments for "Auto" + + +[file attr.pyi] +from typing import TypeVar +_T = TypeVar('_T') +def attrib(default: _T = ..., validator = ...) -> _T: ... +def attrs(cls: _T) -> _T: ... From 8867278286219da68215a67c6804cac8ab4bce3d Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 09/81] Support cmp --- foo.py | 20 +++- mypy/plugin.py | 151 +++++++++++++++++-------------- test-data/unit/check-attr.test | 28 ++---- test-data/unit/fixtures/bool.pyi | 2 + 4 files changed, 110 insertions(+), 91 deletions(-) diff --git a/foo.py b/foo.py index 016a0a3248cc..9b5a75446901 100644 --- a/foo.py +++ b/foo.py @@ -11,7 +11,7 @@ class Auto: FOO: typing.ClassVar[int] = 18 -reveal_type(Auto) +# reveal_type(Auto) Auto(1, 2, 3) @@ -24,7 +24,7 @@ class Typed: FOO: int = 18 -reveal_type(Typed) +# reveal_type(Typed) Typed(1, 2, 3) @@ -37,7 +37,19 @@ class UnTyped: FOO = 18 -reveal_type(UnTyped) -reveal_type(UnTyped.FOO) + def __cmp__(self, other): + ... + + def __lt__(self, other): + ... + + +# reveal_type(UnTyped) +# reveal_type(UnTyped.FOO) UnTyped(1, 2, 3) +x = UnTyped(1, 2, 3) +y = UnTyped(2, 3, 4) + +print(UnTyped(1, 2, 3) == UnTyped(1, 2, 3)) +print(x < y) diff --git a/mypy/plugin.py b/mypy/plugin.py index 4951499b75a7..11334930235f 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -458,77 +458,96 @@ def called_function(expr: Expression) -> Optional[str]: # Update init and auto_attrib if this was a call. init = get_bool_argument(decorator, "init", True) auto_attribs = get_bool_argument(decorator, "auto_attribs", False) + cmp = get_bool_argument(decorator, "cmp", True) else: # Default values of attr.s() init = True + cmp = True auto_attribs = False - if not init: + if not init and not cmp: + # Nothing to add. return - # print(f"{ctx.cls.info.fullname()} init={init} auto={auto_attribs}") - info = ctx.cls.info - - # Walk the body looking for assignments. - names = [] # type: List[str] - types = [] # type: List[Type] - has_default = set() # type: Set[str] - - def add_init_argument(name: str, typ: Optional[Type], default: bool, - context: Context) -> None: - if not default and has_default: - ctx.api.fail( - "Non-default attributes not allowed after default attributes.", - context) - if not typ: - ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) - typ = AnyType(TypeOfAny.unannotated) - - names.append(name) - assert typ is not None - types.append(typ) - if default: - has_default.add(name) - - def is_class_var(expr: NameExpr) -> bool: - if isinstance(expr.node, Var): - return expr.node.is_classvar - return False - - for stmt in ctx.cls.defs.body: - if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): - lhs = stmt.lvalues[0] - name = lhs.name.lstrip("_") - typ = stmt.type - - if called_function(stmt.rvalue) in attr_attrib_makers: - assert isinstance(stmt.rvalue, CallExpr) - # Look for default= in the call. - default = get_argument(stmt.rvalue, "default", 0) - add_init_argument( - name, - typ, - bool(default), - stmt) - else: - if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(stmt.rvalue, TempNode) - add_init_argument(name, typ, has_rhs, stmt) - - init_args = [ - Argument(Var(name, typ), typ, EllipsisExpr(), - ARG_OPT if name in has_default else ARG_POS) - for (name, typ) in zip(names, types) - ] - - add_method( - info=info, - method_name='__init__', - args=init_args, - ret_type=NoneTyp(), - self_type=ctx.api.named_type(info.name()), - function_type=ctx.api.named_type('__builtins__.function'), - ) - # TODO: Add __cmp__ methods. + self_type = ctx.api.named_type(info.name()) + function_type = ctx.api.named_type('__builtins__.function') + + if init: + # Walk the body looking for assignments. + names = [] # type: List[str] + types = [] # type: List[Type] + has_default = set() # type: Set[str] + + def add_init_argument(name: str, typ: Optional[Type], default: bool, + context: Context) -> None: + if not default and has_default: + ctx.api.fail( + "Non-default attributes not allowed after default attributes.", + context) + if not typ: + ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) + typ = AnyType(TypeOfAny.unannotated) + + names.append(name) + assert typ is not None + types.append(typ) + if default: + has_default.add(name) + + def is_class_var(expr: NameExpr) -> bool: + if isinstance(expr.node, Var): + return expr.node.is_classvar + return False + + for stmt in ctx.cls.defs.body: + if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): + lhs = stmt.lvalues[0] + name = lhs.name.lstrip("_") + typ = stmt.type + + if called_function(stmt.rvalue) in attr_attrib_makers: + assert isinstance(stmt.rvalue, CallExpr) + # Look for default= in the call. + default = get_argument(stmt.rvalue, "default", 0) + add_init_argument( + name, + typ, + bool(default), + stmt) + else: + if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(stmt.rvalue, TempNode) + add_init_argument(name, typ, has_rhs, stmt) + + init_args = [ + Argument(Var(name, typ), typ, + EllipsisExpr() if name in has_default else None, + ARG_OPT if name in has_default else ARG_POS) + for (name, typ) in zip(names, types) + ] + + add_method( + info=info, + method_name='__init__', + args=init_args, + ret_type=NoneTyp(), + self_type=self_type, + function_type=function_type, + ) + + if cmp: + bool_type = ctx.api.named_type('__builtins__.bool') + args = [Argument(Var('other', self_type), self_type, None, ARG_POS)] + for method in ['__ne__', '__eq__', + '__lt__', '__le__', + '__gt__', '__ge__']: + add_method( + info=info, + method_name=method, + args=args, + ret_type=bool_type, + self_type=self_type, + function_type=function_type, + ) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 4b3ad61e7d32..1de40dc34ae5 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1,6 +1,5 @@ [case testUntypedAttrS] import attr - @attr.s class UnTyped: normal = attr.ib() # E: Need type annotation for variable @@ -9,9 +8,7 @@ class UnTyped: _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable CLASS_VAR = 18 - reveal_type(UnTyped) # E: Revealed type is 'def (normal: Any, private: Any, def_arg: Any =, def_kwarg: Any =) -> __main__.UnTyped' - UnTyped(1, 2) UnTyped('1', '2', '3') UnTyped('1', '2', '3', 4) @@ -19,7 +16,9 @@ UnTyped('1', '2', def_kwarg='5') UnTyped('1', '2', def_kwarg='5') UnTyped(normal=1, private=2, def_arg=3, def_kwarg=4) UnTyped() # E: Too few arguments for "UnTyped" - +UnTyped(1, 2) == UnTyped(2, 3) +UnTyped(1, 2) >= UnTyped(2, 3) +[builtins fixtures/bool.pyi] [file attr.pyi] from typing import TypeVar _T = TypeVar('_T') @@ -27,21 +26,18 @@ def ib(default: _T = ..., validator = ...) -> _T: ... def s(cls: _T) -> _T: ... [case testTypedAttrS] +[builtins fixtures/bool.pyi] import attr import typing - @attr.s class Typed: normal: int = attr.ib() _private: int = attr.ib() def_arg: int = attr.ib(18) _def_kwarg: int = attr.ib(validator=None, default=18) - - UNTYPED_CLASS_VAR = 5+2 + UNTYPED_CLASS_VAR = 7 TYPED_CLASS_VAR: typing.ClassVar[int] = 22 - reveal_type(Typed) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Typed' - Typed(1, 2) Typed(1, 2, 3) Typed(1, 2, 3, 4) @@ -49,7 +45,6 @@ Typed(1, 2, def_kwarg=5) Typed(1, 2, def_kwarg=5) Typed(normal=1, private=2, def_arg=3, def_kwarg=4) Typed() # E: Too few arguments for "Typed" - [file attr.pyi] from typing import TypeVar _T = TypeVar('_T') @@ -57,9 +52,9 @@ def ib(default: _T = ..., validator = ...) -> _T: ... def s(cls: _T) -> _T: ... [case testAutoAttribAttrS] +[builtins fixtures/bool.pyi] import attr import typing - @attr.s(auto_attribs=True) class Auto: normal: int @@ -69,9 +64,7 @@ class Auto: UNTYPED_CLASS_VAR = 18 TYPED_CLASS_VAR: typing.ClassVar[int] = 18 - reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' - Auto(1, 2) Auto(1, 2, 3) Auto(1, 2, 3, 4) @@ -79,8 +72,6 @@ Auto(1, 2, def_kwarg=5) Auto(1, 2, def_kwarg=5) Auto(normal=1, private=2, def_arg=3, def_kwarg=4) Auto() # E: Too few arguments for "Auto" - - [file attr.pyi] from typing import TypeVar _T = TypeVar('_T') @@ -88,21 +79,18 @@ def ib(default: _T = ..., validator = ...) -> _T: ... def s(cls: _T) -> _T: ... [case testSeriousNamesAttrS] +[builtins fixtures/bool.pyi] import typing from attr import attrib, attrs - @attrs(auto_attribs=True) class Auto: normal: int _private: int def_arg: int = 18 _def_kwarg: int = attrib(validator=None, default=18) - UNTYPED_CLASS_VAR = 18 TYPED_CLASS_VAR: typing.ClassVar[int] = 18 - reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' - Auto(1, 2) Auto(1, 2, 3) Auto(1, 2, 3, 4) @@ -110,8 +98,6 @@ Auto(1, 2, def_kwarg=5) Auto(1, 2, def_kwarg=5) Auto(normal=1, private=2, def_arg=3, def_kwarg=4) Auto() # E: Too few arguments for "Auto" - - [file attr.pyi] from typing import TypeVar _T = TypeVar('_T') diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index a1d1b9c1fdf5..5a15ae249dea 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -12,3 +12,5 @@ class bool: pass class int: pass class str: pass class unicode: pass +class ellipsis: pass + From cd2aded01daadc624e7f360c141e61ace76c349b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 10/81] WIP: attrs_plugin --- foo.py | 10 ++++ mypy/plugin.py | 5 ++ mypy/test/data.py | 7 +++ test-data/unit/check-attr.test | 61 ++++++++++++++--------- test-data/unit/fixtures/attr_builtins.pyi | 12 +++++ 5 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 test-data/unit/fixtures/attr_builtins.pyi diff --git a/foo.py b/foo.py index 9b5a75446901..deae60ad9da9 100644 --- a/foo.py +++ b/foo.py @@ -2,6 +2,16 @@ import typing +@attr.s(cmp=False) +class A: + x = attr.ib() + +a = A() + +A() == A() + +import pdb; pdb.set_trace() + @attr.s(auto_attribs=True) class Auto: normal: int diff --git a/mypy/plugin.py b/mypy/plugin.py index 11334930235f..834f3f52b844 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -508,6 +508,11 @@ def is_class_var(expr: NameExpr) -> bool: if called_function(stmt.rvalue) in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) + # Is it an init=False argument? + attr_init = get_argument(stmt.rvalue, "init", 5) + if attr_init and ctx.api.parse_bool(attr_init) is False: + continue + # Look for default= in the call. default = get_argument(stmt.rvalue, "default", 0) add_init_argument( diff --git a/mypy/test/data.py b/mypy/test/data.py index 0be3be2e3fc9..55f041647ff4 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -101,6 +101,13 @@ def parse_test_cases( src_path = join(os.path.dirname(path), arg) with open(src_path) as f: files.append((join(base_path, 'typing.pyi'), f.read())) + elif p[i].id == 'add-module': + arg = p[i].arg + assert arg is not None + src_path = join(os.path.dirname(path), arg) + name = os.path.basename(src_path) + with open(src_path) as f: + files.append((join(base_path, name), f.read())) elif re.match(r'stale[0-9]*$', p[i].id): if p[i].id == 'stale': passnum = 1 diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 1de40dc34ae5..df7c152ffbe0 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -18,15 +18,10 @@ UnTyped(normal=1, private=2, def_arg=3, def_kwarg=4) UnTyped() # E: Too few arguments for "UnTyped" UnTyped(1, 2) == UnTyped(2, 3) UnTyped(1, 2) >= UnTyped(2, 3) -[builtins fixtures/bool.pyi] -[file attr.pyi] -from typing import TypeVar -_T = TypeVar('_T') -def ib(default: _T = ..., validator = ...) -> _T: ... -def s(cls: _T) -> _T: ... +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] [case testTypedAttrS] -[builtins fixtures/bool.pyi] import attr import typing @attr.s @@ -45,14 +40,10 @@ Typed(1, 2, def_kwarg=5) Typed(1, 2, def_kwarg=5) Typed(normal=1, private=2, def_arg=3, def_kwarg=4) Typed() # E: Too few arguments for "Typed" -[file attr.pyi] -from typing import TypeVar -_T = TypeVar('_T') -def ib(default: _T = ..., validator = ...) -> _T: ... -def s(cls: _T) -> _T: ... +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] [case testAutoAttribAttrS] -[builtins fixtures/bool.pyi] import attr import typing @attr.s(auto_attribs=True) @@ -64,6 +55,7 @@ class Auto: UNTYPED_CLASS_VAR = 18 TYPED_CLASS_VAR: typing.ClassVar[int] = 18 + reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' Auto(1, 2) Auto(1, 2, 3) @@ -72,14 +64,10 @@ Auto(1, 2, def_kwarg=5) Auto(1, 2, def_kwarg=5) Auto(normal=1, private=2, def_arg=3, def_kwarg=4) Auto() # E: Too few arguments for "Auto" -[file attr.pyi] -from typing import TypeVar -_T = TypeVar('_T') -def ib(default: _T = ..., validator = ...) -> _T: ... -def s(cls: _T) -> _T: ... +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] [case testSeriousNamesAttrS] -[builtins fixtures/bool.pyi] import typing from attr import attrib, attrs @attrs(auto_attribs=True) @@ -98,8 +86,33 @@ Auto(1, 2, def_kwarg=5) Auto(1, 2, def_kwarg=5) Auto(normal=1, private=2, def_arg=3, def_kwarg=4) Auto() # E: Too few arguments for "Auto" -[file attr.pyi] -from typing import TypeVar -_T = TypeVar('_T') -def attrib(default: _T = ..., validator = ...) -> _T: ... -def attrs(cls: _T) -> _T: ... +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] + +[case testNoInitAttrS] +from attr import attrib, attrs +@attrs(auto_attribs=True, init=False) +class Auto: + normal: int + _private: int + def_arg: int = 18 + _def_kwarg: int = attrib(validator=None, default=18) +reveal_type(Auto) # E: Revealed type is 'def () -> __main__.Auto' +Auto() +Auto() == Auto() +Auto() >= Auto() +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] + +[case testNoCmpAttrS] +from attr import attrib, attrs +@attrs(auto_attribs=True, cmp=False) +class A: + b: int +reveal_type(A) # E: Revealed type is 'def (b: builtins.int) -> __main__.A' +A(5) +A(5) == A(5) +A(5) >= A(5) # E: Unsupported left operand type for >= ("A") +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] + diff --git a/test-data/unit/fixtures/attr_builtins.pyi b/test-data/unit/fixtures/attr_builtins.pyi new file mode 100644 index 000000000000..403e3ae2780a --- /dev/null +++ b/test-data/unit/fixtures/attr_builtins.pyi @@ -0,0 +1,12 @@ +class object: + def __init__(self) -> None: pass + def __eq__(self, o: object) -> bool: pass + def __ne__(self, o: object) -> bool: pass + +class type: pass +class function: pass +class bool: pass +class int: pass +class str: pass +class unicode: pass +class ellipsis: pass From 104f9f41c1970d29b23dad9dc8425ece062f6590 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 11/81] WIP: attrs_plugin --- attr.pyi | 83 -------------------------------- foo.py | 65 ------------------------- mypy/plugin.py | 6 ++- test-data/unit/fixtures/attr.pyi | 15 ++++++ test-data/unit/fixtures/bool.pyi | 2 - 5 files changed, 19 insertions(+), 152 deletions(-) delete mode 100644 attr.pyi delete mode 100644 foo.py create mode 100644 test-data/unit/fixtures/attr.pyi diff --git a/attr.pyi b/attr.pyi deleted file mode 100644 index 48d5a884ec61..000000000000 --- a/attr.pyi +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, \ - Mapping, Tuple, Type, TypeVar, Union, overload, Sequence - -# `import X as X` is required to expose these to mypy. otherwise they are invisible -#from . import exceptions as exceptions -#from . import filters as filters -#from . import converters as converters -#from . import validators as validators - -# typing -- - -_T = TypeVar('_T') -_C = TypeVar('_C', bound=type) -_M = TypeVar('_M', bound=Mapping) -_I = TypeVar('_I', bound=Sequence) - -_ValidatorType = Callable[[Any, 'Attribute', _T], Any] -_ConverterType = Callable[[Any], _T] -_FilterType = Callable[['Attribute', Any], bool] - -# _make -- - -NOTHING : object - -# Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` -def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... - -class Attribute(Generic[_T]): - __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") - name: str - default: Any - validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] - repr: bool - cmp: bool - hash: Optional[bool] - init: bool - convert: Optional[_ConverterType[_T]] - metadata: Mapping - type: Optional[Type[_T]] - -@overload -def attr(default: _T = ..., validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., type: Any = ...) -> _T: ... -@overload -def attr(*, validator: Optional[Union[_ValidatorType[Any], List[_ValidatorType[Any]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[Any]] = ..., metadata: Mapping = ...): ... - - -@overload -def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... -@overload -def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... - -def fields(cls: type) -> Tuple[Attribute, ...]: ... -def validate(inst: Any) -> None: ... - -# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name: str, attrs: Union[List[Any], Dict[str, Any]], bases: Tuple[type, ...] = ..., - these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> type: ... - -# _funcs -- -# FIXME: Overloads don't work. -#@overload -#def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... -#@overload -#def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... - -#@overload -#def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... -#@overload -#def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... - -def has(cls: type) -> bool: ... -def assoc(inst: _T, **changes: Any) -> _T: ... -def evolve(inst: _T, **changes: Any) -> _T: ... - -# _config -- - -def set_run_validators(run: bool) -> None: ... -def get_run_validators() -> bool: ... - -# aliases -s = attrs = attributes -ib = attrib = attr - diff --git a/foo.py b/foo.py deleted file mode 100644 index deae60ad9da9..000000000000 --- a/foo.py +++ /dev/null @@ -1,65 +0,0 @@ -import attr -import typing - - -@attr.s(cmp=False) -class A: - x = attr.ib() - -a = A() - -A() == A() - -import pdb; pdb.set_trace() - -@attr.s(auto_attribs=True) -class Auto: - normal: int - _private: int - def_arg: int = 18 - _def_kwarg: int = attr.ib(validator=None, default=18) - - FOO: typing.ClassVar[int] = 18 - -# reveal_type(Auto) -Auto(1, 2, 3) - - -@attr.s -class Typed: - normal: int = attr.ib() - _private: int = attr.ib() - def_arg: int = attr.ib(18) - _def_kwarg: int = attr.ib(validator=None, default=18) - - FOO: int = 18 - -# reveal_type(Typed) -Typed(1, 2, 3) - - -@attr.s -class UnTyped: - normal = attr.ib() - _private = attr.ib() - def_arg = attr.ib(18) - _def_kwarg = attr.ib(validator=None, default=18) - - FOO = 18 - - def __cmp__(self, other): - ... - - def __lt__(self, other): - ... - - -# reveal_type(UnTyped) -# reveal_type(UnTyped.FOO) -UnTyped(1, 2, 3) - -x = UnTyped(1, 2, 3) -y = UnTyped(2, 3, 4) - -print(UnTyped(1, 2, 3) == UnTyped(1, 2, 3)) -print(x < y) diff --git a/mypy/plugin.py b/mypy/plugin.py index 834f3f52b844..4e817bd8343a 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -433,14 +433,16 @@ def add_method( def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add an __init__ method to classes decorated with attr.s.""" - def get_bool_argument(call: CallExpr, name: str, default: Optional[bool]) -> Optional[bool]: + def get_bool_argument(call: CallExpr, name: str, + default: Optional[bool]) -> Optional[bool]: for arg_name, arg_value in zip(call.arg_names, call.args): if arg_name == name: # TODO: Handle None being returned here. return ctx.api.parse_bool(arg_value) return default - def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: + def get_argument(call: CallExpr, name: Optional[str], + num: Optional[int]) -> Optional[Expression]: for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): if num is not None and not attr_name and i == num: return attr_value diff --git a/test-data/unit/fixtures/attr.pyi b/test-data/unit/fixtures/attr.pyi new file mode 100644 index 000000000000..3aa3e87a6555 --- /dev/null +++ b/test-data/unit/fixtures/attr.pyi @@ -0,0 +1,15 @@ +from typing import TypeVar, overload, Callable + +_T = TypeVar('_T') + +def attr(default: _T = ..., validator = ...) -> _T: ... + +@overload +def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ...) -> _T: ... + +@overload +def attributes(maybe_cls: None = ..., cmp: bool = ..., init: bool = ...) -> Callable[[_T], _T]: ... + +# aliases +s = attrs = attributes +ib = attrib = attr diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index 5a15ae249dea..a1d1b9c1fdf5 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -12,5 +12,3 @@ class bool: pass class int: pass class str: pass class unicode: pass -class ellipsis: pass - From 6856500d78f7f47b903fba2d5caff474409e5c4b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 12/81] WIP: attrs_plugin --- mypy/checker.py | 1 + mypy/plugin.py | 7 ++++++- test-data/unit/check-attr.test | 24 ++++++++++++++++++++---- test-data/unit/fixtures/attr.pyi | 9 ++++++++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ba3cb543e70b..b7f0cf4bac38 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1940,6 +1940,7 @@ def infer_variable_type(self, name: Var, lvalue: Lvalue, # partial type which will be made more specific later. A partial type # gets generated in assignment like 'x = []' where item type is not known. if not self.infer_partial_type(name, lvalue, init_type): + # import pdb; pdb.set_trace() self.fail(messages.NEED_ANNOTATION_FOR_VAR, context) self.set_inference_error_fallback_type(name, lvalue, init_type, context) elif (isinstance(lvalue, MemberExpr) and self.inferred_attribute_types is not None diff --git a/mypy/plugin.py b/mypy/plugin.py index 4e817bd8343a..ec3774e61146 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -488,7 +488,6 @@ def add_init_argument(name: str, typ: Optional[Type], default: bool, "Non-default attributes not allowed after default attributes.", context) if not typ: - ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) typ = AnyType(TypeOfAny.unannotated) names.append(name) @@ -510,6 +509,9 @@ def is_class_var(expr: NameExpr) -> bool: if called_function(stmt.rvalue) in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) + if not stmt.type: + stmt.type = AnyType(TypeOfAny.explicit) + # Is it an init=False argument? attr_init = get_argument(stmt.rvalue, "init", 5) if attr_init and ctx.api.parse_bool(attr_init) is False: @@ -517,6 +519,9 @@ def is_class_var(expr: NameExpr) -> bool: # Look for default= in the call. default = get_argument(stmt.rvalue, "default", 0) + attr_typ = get_argument(stmt.rvalue, "type", 15) + if attr_typ: + import pdb; pdb.set_trace() add_init_argument( name, typ, diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index df7c152ffbe0..327746be8db7 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1,11 +1,27 @@ +[case testStuffAttrS] +import attr + +a = attr.ib() # E: Need type annotation for variable +b = attr.ib(7) +c = attr.ib(validator=22) # E: Need type annotation for variable +d = attr.ib(7, validator=22) + +reveal_type(a) # E: Revealed type is 'Any' # E: Cannot determine type of 'a' +reveal_type(b) # E: Revealed type is 'Any' +reveal_type(c) # E: Revealed type is 'Any' # E: Cannot determine type of 'c' +reveal_type(d) # E: Revealed type is 'builtins.int*' + +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] + [case testUntypedAttrS] import attr @attr.s class UnTyped: - normal = attr.ib() # E: Need type annotation for variable - _private = attr.ib() # E: Need type annotation for variable - def_arg = attr.ib(18) # E: Need type annotation for variable - _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable + normal = attr.ib() + _private = attr.ib() + def_arg = attr.ib(18) + _def_kwarg = attr.ib(validator=None, default=18) CLASS_VAR = 18 reveal_type(UnTyped) # E: Revealed type is 'def (normal: Any, private: Any, def_arg: Any =, def_kwarg: Any =) -> __main__.UnTyped' diff --git a/test-data/unit/fixtures/attr.pyi b/test-data/unit/fixtures/attr.pyi index 3aa3e87a6555..3351d59a33f3 100644 --- a/test-data/unit/fixtures/attr.pyi +++ b/test-data/unit/fixtures/attr.pyi @@ -1,8 +1,15 @@ -from typing import TypeVar, overload, Callable +from typing import TypeVar, overload, Callable, Any _T = TypeVar('_T') +@overload +def attr() -> Any: ... +@overload +def attr(default: _T, validator = ...) -> _T: ... +@overload def attr(default: _T = ..., validator = ...) -> _T: ... +@overload +def attr(validator= ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ...) -> _T: ... From 0e359a8125bf8d3c2bddbb5e58c64f82c2533776 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:56 -0800 Subject: [PATCH 13/81] WIP: attr_pyi --- attr.pyi | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ foo.py | 20 ++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 attr.pyi create mode 100644 foo.py diff --git a/attr.pyi b/attr.pyi new file mode 100644 index 000000000000..e24b81d1b12a --- /dev/null +++ b/attr.pyi @@ -0,0 +1,97 @@ +from typing import Any, Callable, Collection, Dict, Generic, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload, Sequence +# `import X as X` is required to expose these to mypy. otherwise they are invisible +#from . import exceptions as exceptions +#from . import filters as filters +#from . import converters as converters +#from . import validators as validators + +# typing -- + +_T = TypeVar('_T') +_C = TypeVar('_C', bound=type) +_M = TypeVar('_M', bound=Mapping) +_I = TypeVar('_I', bound=Collection) + +_ValidatorType = Callable[[Any, 'Attribute', _T], Any] +_ConverterType = Callable[[Any], _T] +_FilterType = Callable[['Attribute', Any], bool] + +# _make -- + +NOTHING : object + +# Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` +def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... + +class Attribute(Generic[_T]): + __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") + name: str + default: Any + validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] + repr: bool + cmp: bool + hash: Optional[bool] + init: bool + convert: Optional[_ConverterType[_T]] + metadata: Mapping + type: Optional[Type[_T]] + + +# FIXME: if no type arg or annotation is provided when using `attr` it will result in an error: +# error: Need type annotation for variable +# See discussion here: https://github.com/python/mypy/issues/4227 +# tl;dr: Waiting on a fix to https://github.com/python/typing/issues/253 +@overload +def attr(default: _T, validator: Optional[Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., + type: Optional[Type[_T]] = ...) -> _T: ... +@overload +def attr(default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... + + +@overload +def attributes(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... +@overload +def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... + +def fields(cls: type) -> Tuple[Attribute, ...]: ... +def validate(inst: Any) -> None: ... + +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid +def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... + +# _funcs -- + +# FIXME: asdict/astuple do not honor their factory args. waiting on one of these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... + +# @overload +# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... +# @overload +# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... + +def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Collection] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... + +# @overload +# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... +# @overload +# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> tuple: ... + +def has(cls: type) -> bool: ... +def assoc(inst: _T, **changes) -> _T: ... +def evolve(inst: _T, **changes) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases +s = attrs = attributes +ib = attrib = attr diff --git a/foo.py b/foo.py new file mode 100644 index 000000000000..87b05f5a4c5e --- /dev/null +++ b/foo.py @@ -0,0 +1,20 @@ +from typing import Any + +import attr + + +def validator(inst: Any, attr: attr.Attribute, value: bool): + pass + + +a = attr.ib() # E: Need type annotation for variable +b = attr.ib(7) +c = attr.ib(validator=validator) # E: Need type annotation for variable +d = attr.ib(True, validator=validator) +e = attr.ib(type=str) + +reveal_type(a) # E: Revealed type is 'Any' # E: Cannot determine type of 'a' +reveal_type(b) # E: Revealed type is 'Any' +reveal_type(c) # E: Revealed type is 'Any' # E: Cannot determine type of 'c' +reveal_type(d) # E: Revealed type is 'builtins.int*' +reveal_type(e) From ebb98347a19ddda1329486ba01c1328315589174 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 14/81] WIP: attr_pyi --- attr.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/attr.pyi b/attr.pyi index e24b81d1b12a..67e4020f3073 100644 --- a/attr.pyi +++ b/attr.pyi @@ -42,10 +42,10 @@ class Attribute(Generic[_T]): # See discussion here: https://github.com/python/mypy/issues/4227 # tl;dr: Waiting on a fix to https://github.com/python/typing/issues/253 @overload -def attr(default: _T, validator: Optional[Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]]] = ..., +def attr(default: _T, validator: Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., - type: Optional[Type[_T]] = ...) -> _T: ... + convert: _ConverterType[_T] = ..., metadata: Mapping = ..., + type: Type[_T] = ...) -> _T: ... @overload def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., From 001a16a4b653f2d0eaa43874b20fe45132574b13 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 15/81] WIP: attr_pyi --- attr.pyi | 21 ++++++++++++++++----- foo.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/attr.pyi b/attr.pyi index 67e4020f3073..cb4e06937aae 100644 --- a/attr.pyi +++ b/attr.pyi @@ -15,6 +15,7 @@ _I = TypeVar('_I', bound=Collection) _ValidatorType = Callable[[Any, 'Attribute', _T], Any] _ConverterType = Callable[[Any], _T] _FilterType = Callable[['Attribute', Any], bool] +_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] # _make -- @@ -37,16 +38,26 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] -# FIXME: if no type arg or annotation is provided when using `attr` it will result in an error: -# error: Need type annotation for variable -# See discussion here: https://github.com/python/mypy/issues/4227 -# tl;dr: Waiting on a fix to https://github.com/python/typing/issues/253 +# `attr` also lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# 1st form catches a default value set. Can't use = ... or you get "overloaded overlap" error. @overload -def attr(default: _T, validator: Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] = ..., +def attr(default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: _ConverterType[_T] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... @overload +def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: _ConverterType[_T] = ..., metadata: Mapping = ..., + type: Type[_T] = ...) -> _T: ... +# 3rd form catches nothing set. So returns Any. +@overload def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: None = ..., metadata: Mapping = ..., diff --git a/foo.py b/foo.py index 87b05f5a4c5e..8edc059de7dd 100644 --- a/foo.py +++ b/foo.py @@ -7,12 +7,22 @@ def validator(inst: Any, attr: attr.Attribute, value: bool): pass +def converter(val: Any) -> bool: + return bool(val) + + a = attr.ib() # E: Need type annotation for variable b = attr.ib(7) c = attr.ib(validator=validator) # E: Need type annotation for variable d = attr.ib(True, validator=validator) e = attr.ib(type=str) +attr.ib(validator=None) + +f = attr.ib(validator=validator, convert=converter) # E: Need type annotation for variable + + + reveal_type(a) # E: Revealed type is 'Any' # E: Cannot determine type of 'a' reveal_type(b) # E: Revealed type is 'Any' reveal_type(c) # E: Revealed type is 'Any' # E: Cannot determine type of 'c' From 453c8aef6602bce85180e8cfd90589c004361b82 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 16/81] WIP: attrs_plugin --- mypy/plugin.py | 8 ++++++-- test-data/unit/check-attr.test | 30 ++++++++++++++---------------- test-data/unit/fixtures/attr.pyi | 10 +--------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index ec3774e61146..329db74881b3 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -488,6 +488,12 @@ def add_init_argument(name: str, typ: Optional[Type], default: bool, "Non-default attributes not allowed after default attributes.", context) if not typ: + if ctx.api.options.disallow_untyped_defs: + # This is a compromise. If you don't have a type here then the init will be untyped. + # But since the __init__ method doesn't have a line number it's difficult to point + # to the correct line number. So instead we just show the error in the assignment. + # Which is where you would fix the issue. + ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) typ = AnyType(TypeOfAny.unannotated) names.append(name) @@ -509,8 +515,6 @@ def is_class_var(expr: NameExpr) -> bool: if called_function(stmt.rvalue) in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) - if not stmt.type: - stmt.type = AnyType(TypeOfAny.explicit) # Is it an init=False argument? attr_init = get_argument(stmt.rvalue, "init", 5) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 327746be8db7..936f750d315b 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1,19 +1,3 @@ -[case testStuffAttrS] -import attr - -a = attr.ib() # E: Need type annotation for variable -b = attr.ib(7) -c = attr.ib(validator=22) # E: Need type annotation for variable -d = attr.ib(7, validator=22) - -reveal_type(a) # E: Revealed type is 'Any' # E: Cannot determine type of 'a' -reveal_type(b) # E: Revealed type is 'Any' -reveal_type(c) # E: Revealed type is 'Any' # E: Cannot determine type of 'c' -reveal_type(d) # E: Revealed type is 'builtins.int*' - -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] - [case testUntypedAttrS] import attr @attr.s @@ -37,6 +21,20 @@ UnTyped(1, 2) >= UnTyped(2, 3) [builtins fixtures/attr_builtins.pyi] [add-module fixtures/attr.pyi] +[case testUntypedNoUntypedAttrS] +# flags: --disallow-untyped-defs +import attr +@attr.s +class UnTyped: + normal = attr.ib() # E: Need type annotation for variable + _private = attr.ib() # E: Need type annotation for variable + def_arg = attr.ib(18) # E: Need type annotation for variable + _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable + + CLASS_VAR = 18 +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] + [case testTypedAttrS] import attr import typing diff --git a/test-data/unit/fixtures/attr.pyi b/test-data/unit/fixtures/attr.pyi index 3351d59a33f3..ab2809648512 100644 --- a/test-data/unit/fixtures/attr.pyi +++ b/test-data/unit/fixtures/attr.pyi @@ -2,18 +2,10 @@ from typing import TypeVar, overload, Callable, Any _T = TypeVar('_T') -@overload -def attr() -> Any: ... -@overload -def attr(default: _T, validator = ...) -> _T: ... -@overload -def attr(default: _T = ..., validator = ...) -> _T: ... -@overload -def attr(validator= ...) -> Any: ... +def attr(default: Any = ..., validator: Any = ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ...) -> _T: ... - @overload def attributes(maybe_cls: None = ..., cmp: bool = ..., init: bool = ...) -> Callable[[_T], _T]: ... From d26bdb659511d1c11b47d0ef467fc89d688f2a07 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 17/81] WIP: attrs_plugin --- mypy/plugin.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 329db74881b3..9ecbb2b6136f 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -433,13 +433,6 @@ def add_method( def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add an __init__ method to classes decorated with attr.s.""" - def get_bool_argument(call: CallExpr, name: str, - default: Optional[bool]) -> Optional[bool]: - for arg_name, arg_value in zip(call.arg_names, call.args): - if arg_name == name: - # TODO: Handle None being returned here. - return ctx.api.parse_bool(arg_value) - return default def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: @@ -450,22 +443,32 @@ def get_argument(call: CallExpr, name: Optional[str], return attr_value return None + def get_bool_argument(call: CallExpr, name: str, + default: Optional[bool]) -> Optional[bool]: + for arg_name, arg_value in zip(call.arg_names, call.args): + if arg_name == name: + # TODO: Handle None being returned here. + return ctx.api.parse_bool(arg_value) + return default + def called_function(expr: Expression) -> Optional[str]: if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): return expr.callee.fullname return None decorator = ctx.reason + + # Default values of attr.s() + init = True + cmp = True + auto_attribs = False + if isinstance(decorator, CallExpr): - # Update init and auto_attrib if this was a call. - init = get_bool_argument(decorator, "init", True) - auto_attribs = get_bool_argument(decorator, "auto_attribs", False) - cmp = get_bool_argument(decorator, "cmp", True) - else: - # Default values of attr.s() - init = True - cmp = True - auto_attribs = False + # Read call arguments. + init = get_bool_argument(decorator, "init", init) + cmp = get_bool_argument(decorator, "cmp", cmp) + auto_attribs = get_bool_argument(decorator, "auto_attribs", + auto_attribs) if not init and not cmp: # Nothing to add. From e78c0408999931a08a64120beb60b232dcc5b9f8 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 18/81] WIP: attr_pyi --- mypy/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 9ecbb2b6136f..e34fb225da39 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -528,7 +528,9 @@ def is_class_var(expr: NameExpr) -> bool: default = get_argument(stmt.rvalue, "default", 0) attr_typ = get_argument(stmt.rvalue, "type", 15) if attr_typ: - import pdb; pdb.set_trace() + # TODO: Can we do something useful with this? + pass + add_init_argument( name, typ, From fd1a24b9ad18a7b615d610194cc4e1851d975556 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 19/81] WIP: attr_pyi --- attr.pyi | 108 ------------------------------------------------------- foo.py | 30 ---------------- 2 files changed, 138 deletions(-) delete mode 100644 attr.pyi delete mode 100644 foo.py diff --git a/attr.pyi b/attr.pyi deleted file mode 100644 index cb4e06937aae..000000000000 --- a/attr.pyi +++ /dev/null @@ -1,108 +0,0 @@ -from typing import Any, Callable, Collection, Dict, Generic, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload, Sequence -# `import X as X` is required to expose these to mypy. otherwise they are invisible -#from . import exceptions as exceptions -#from . import filters as filters -#from . import converters as converters -#from . import validators as validators - -# typing -- - -_T = TypeVar('_T') -_C = TypeVar('_C', bound=type) -_M = TypeVar('_M', bound=Mapping) -_I = TypeVar('_I', bound=Collection) - -_ValidatorType = Callable[[Any, 'Attribute', _T], Any] -_ConverterType = Callable[[Any], _T] -_FilterType = Callable[['Attribute', Any], bool] -_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] - -# _make -- - -NOTHING : object - -# Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` -def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... - -class Attribute(Generic[_T]): - __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") - name: str - default: Any - validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] - repr: bool - cmp: bool - hash: Optional[bool] - init: bool - convert: Optional[_ConverterType[_T]] - metadata: Mapping - type: Optional[Type[_T]] - - -# `attr` also lies about its return type to make the following possible: -# attr() -> Any -# attr(8) -> int -# attr(validator=) -> Whatever the callable expects. -# This makes this type of assignments possible: -# x: int = attr(8) -# -# 1st form catches a default value set. Can't use = ... or you get "overloaded overlap" error. -@overload -def attr(default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: _ConverterType[_T] = ..., metadata: Mapping = ..., - type: Type[_T] = ...) -> _T: ... -@overload -def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: _ConverterType[_T] = ..., metadata: Mapping = ..., - type: Type[_T] = ...) -> _T: ... -# 3rd form catches nothing set. So returns Any. -@overload -def attr(default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... - - -@overload -def attributes(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... -@overload -def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... - -def fields(cls: type) -> Tuple[Attribute, ...]: ... -def validate(inst: Any) -> None: ... - -# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... - -# _funcs -- - -# FIXME: asdict/astuple do not honor their factory args. waiting on one of these: -# https://github.com/python/mypy/issues/4236 -# https://github.com/python/typing/issues/253 -def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... - -# @overload -# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... -# @overload -# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... - -def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Collection] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... - -# @overload -# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... -# @overload -# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> tuple: ... - -def has(cls: type) -> bool: ... -def assoc(inst: _T, **changes) -> _T: ... -def evolve(inst: _T, **changes) -> _T: ... - -# _config -- - -def set_run_validators(run: bool) -> None: ... -def get_run_validators() -> bool: ... - -# aliases -s = attrs = attributes -ib = attrib = attr diff --git a/foo.py b/foo.py deleted file mode 100644 index 8edc059de7dd..000000000000 --- a/foo.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Any - -import attr - - -def validator(inst: Any, attr: attr.Attribute, value: bool): - pass - - -def converter(val: Any) -> bool: - return bool(val) - - -a = attr.ib() # E: Need type annotation for variable -b = attr.ib(7) -c = attr.ib(validator=validator) # E: Need type annotation for variable -d = attr.ib(True, validator=validator) -e = attr.ib(type=str) - -attr.ib(validator=None) - -f = attr.ib(validator=validator, convert=converter) # E: Need type annotation for variable - - - -reveal_type(a) # E: Revealed type is 'Any' # E: Cannot determine type of 'a' -reveal_type(b) # E: Revealed type is 'Any' -reveal_type(c) # E: Revealed type is 'Any' # E: Cannot determine type of 'c' -reveal_type(d) # E: Revealed type is 'builtins.int*' -reveal_type(e) From 1680e8f397cdd829145a24346314daf30298419a Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 20/81] Support inheritance --- mypy/plugin.py | 71 +++++++++++++++++----------------- test-data/unit/check-attr.test | 19 +++++++++ 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index e34fb225da39..71cd77a5a81c 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -474,8 +474,7 @@ def called_function(expr: Expression) -> Optional[str]: # Nothing to add. return - info = ctx.cls.info - self_type = ctx.api.named_type(info.name()) + self_type = ctx.api.named_type(ctx.cls.info.name()) function_type = ctx.api.named_type('__builtins__.function') if init: @@ -510,37 +509,39 @@ def is_class_var(expr: NameExpr) -> bool: return expr.node.is_classvar return False - for stmt in ctx.cls.defs.body: - if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): - lhs = stmt.lvalues[0] - name = lhs.name.lstrip("_") - typ = stmt.type - - if called_function(stmt.rvalue) in attr_attrib_makers: - assert isinstance(stmt.rvalue, CallExpr) - - # Is it an init=False argument? - attr_init = get_argument(stmt.rvalue, "init", 5) - if attr_init and ctx.api.parse_bool(attr_init) is False: - continue - - # Look for default= in the call. - default = get_argument(stmt.rvalue, "default", 0) - attr_typ = get_argument(stmt.rvalue, "type", 15) - if attr_typ: - # TODO: Can we do something useful with this? - pass - - add_init_argument( - name, - typ, - bool(default), - stmt) - else: - if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(stmt.rvalue, TempNode) - add_init_argument(name, typ, has_rhs, stmt) + # Walk the mro in reverse looking for those yummy attributes. + for info in reversed(ctx.cls.info.mro): + for stmt in info.defn.defs.body: + if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): + lhs = stmt.lvalues[0] + name = lhs.name.lstrip("_") + typ = stmt.type + + if called_function(stmt.rvalue) in attr_attrib_makers: + assert isinstance(stmt.rvalue, CallExpr) + + # Is it an init=False argument? + attr_init = get_argument(stmt.rvalue, "init", 5) + if attr_init and ctx.api.parse_bool(attr_init) is False: + continue + + # Look for default= in the call. + default = get_argument(stmt.rvalue, "default", 0) + attr_typ = get_argument(stmt.rvalue, "type", 15) + if attr_typ: + # TODO: Can we do something useful with this? + pass + + add_init_argument( + name, + typ, + bool(default), + stmt) + else: + if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(stmt.rvalue, TempNode) + add_init_argument(name, typ, has_rhs, stmt) init_args = [ Argument(Var(name, typ), typ, @@ -550,7 +551,7 @@ def is_class_var(expr: NameExpr) -> bool: ] add_method( - info=info, + info=ctx.cls.info, method_name='__init__', args=init_args, ret_type=NoneTyp(), @@ -565,7 +566,7 @@ def is_class_var(expr: NameExpr) -> bool: '__lt__', '__le__', '__gt__', '__ge__']: add_method( - info=info, + info=ctx.cls.info, method_name=method, args=args, ret_type=bool_type, diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 936f750d315b..bbdfb03054e5 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -130,3 +130,22 @@ A(5) >= A(5) # E: Unsupported left operand type for >= ("A") [builtins fixtures/attr_builtins.pyi] [add-module fixtures/attr.pyi] + +[case testInheritanceAttrS] +import attr + +@attr.s +class A: + a: int = attr.ib() + +@attr.s +class B: + b: str = attr.ib() + +@attr.s # type: ignore # Incompatible base classes in re cmp methods +class C(B, A): + c: bool = attr.ib() + +reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool) -> __main__.C' +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] From 521d2150c0e798cd2e460873b4eac5e6f45c65f4 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 21/81] Fix lint and mypy --- mypy/plugin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 71cd77a5a81c..3f9835df76b9 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -61,6 +61,8 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance: class SemanticAnalyzerPluginInterface: """Interface for accessing semantic analyzer functionality in plugins.""" + options = None # type: Options + @abstractmethod def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance: raise NotImplementedError @@ -491,10 +493,10 @@ def add_init_argument(name: str, typ: Optional[Type], default: bool, context) if not typ: if ctx.api.options.disallow_untyped_defs: - # This is a compromise. If you don't have a type here then the init will be untyped. - # But since the __init__ method doesn't have a line number it's difficult to point - # to the correct line number. So instead we just show the error in the assignment. - # Which is where you would fix the issue. + # This is a compromise. If you don't have a type here then the __init__ will + # be untyped. But since the __init__ method doesn't have a line number it's + # difficult to point to the correct line number. So instead we just show the + # error in the assignment, which is where you would fix the issue. ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) typ = AnyType(TypeOfAny.unannotated) From 071c0fdf428df9e49e34c46fe5b49ad5b273890c Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 22/81] Optionals --- mypy/checker.py | 1 - mypy/plugin.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b7f0cf4bac38..ba3cb543e70b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1940,7 +1940,6 @@ def infer_variable_type(self, name: Var, lvalue: Lvalue, # partial type which will be made more specific later. A partial type # gets generated in assignment like 'x = []' where item type is not known. if not self.infer_partial_type(name, lvalue, init_type): - # import pdb; pdb.set_trace() self.fail(messages.NEED_ANNOTATION_FOR_VAR, context) self.set_inference_error_fallback_type(name, lvalue, init_type, context) elif (isinstance(lvalue, MemberExpr) and self.inferred_attribute_types is not None diff --git a/mypy/plugin.py b/mypy/plugin.py index 3f9835df76b9..e9fc3e0b7d60 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -461,12 +461,13 @@ def called_function(expr: Expression) -> Optional[str]: decorator = ctx.reason # Default values of attr.s() - init = True - cmp = True - auto_attribs = False + init: Optional[bool] = True + cmp: Optional[bool] = True + auto_attribs: Optional[bool] = False if isinstance(decorator, CallExpr): # Read call arguments. + # TODO handle these returning None (e.g. bool=SOMETHING_ELSE) init = get_bool_argument(decorator, "init", init) cmp = get_bool_argument(decorator, "cmp", cmp) auto_attribs = get_bool_argument(decorator, "auto_attribs", From 6986547d9e99bee22003a772658e04069a9310f5 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 23/81] Never forget Python 3.5 --- mypy/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index e9fc3e0b7d60..f77197a4d5e7 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -461,9 +461,9 @@ def called_function(expr: Expression) -> Optional[str]: decorator = ctx.reason # Default values of attr.s() - init: Optional[bool] = True - cmp: Optional[bool] = True - auto_attribs: Optional[bool] = False + init = True # type: Optional[bool] + cmp: Optional[bool] = True # type: Optional[bool] + auto_attribs = False # type: Optional[bool] if isinstance(decorator, CallExpr): # Read call arguments. From 5d60d991d0d3eb51b200b9112d40f2e64ec7a3e4 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 24/81] Sigh --- mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index f77197a4d5e7..fe56e9d450f7 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -462,7 +462,7 @@ def called_function(expr: Expression) -> Optional[str]: # Default values of attr.s() init = True # type: Optional[bool] - cmp: Optional[bool] = True # type: Optional[bool] + cmp = True # type: Optional[bool] auto_attribs = False # type: Optional[bool] if isinstance(decorator, CallExpr): From 2284797fd4f63e8dcd385307991a4a5e98ab56f5 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 25/81] Support nested classes --- mypy/plugin.py | 4 +++- test-data/unit/check-attr.test | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index fe56e9d450f7..f6557f0b84a5 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -477,7 +477,9 @@ def called_function(expr: Expression) -> Optional[str]: # Nothing to add. return - self_type = ctx.api.named_type(ctx.cls.info.name()) + # To support nested classes we use fullname(). But fullname is . + # and named_type() expects only the name. + self_type = ctx.api.named_type(ctx.cls.info.fullname().split(".", 1)[1]) function_type = ctx.api.named_type('__builtins__.function') if init: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index bbdfb03054e5..237fc6e0de7b 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -133,19 +133,28 @@ A(5) >= A(5) # E: Unsupported left operand type for >= ("A") [case testInheritanceAttrS] import attr - @attr.s class A: a: int = attr.ib() - @attr.s class B: b: str = attr.ib() - @attr.s # type: ignore # Incompatible base classes in re cmp methods class C(B, A): c: bool = attr.ib() - reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool) -> __main__.C' [builtins fixtures/attr_builtins.pyi] [add-module fixtures/attr.pyi] + +[case testNestedClassAttrS] +import attr +@attr.s +class C: + y = attr.ib() + @attr.s + class D: + x: int = attr.ib() +reveal_type(C) # E: Revealed type is 'def (y: Any) -> __main__.C' +reveal_type(C.D) # E: Revealed type is 'def (x: builtins.int) -> __main__.C.D' +[builtins fixtures/attr_builtins.pyi] +[add-module fixtures/attr.pyi] From 89f3537d82575bc7fe72159c020944a442b28560 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 26/81] WIP: attrs_plugin --- mypy/plugin.py | 2 +- mypy/test/data.py | 7 ------- test-data/unit/{fixtures => lib-stub}/attr.pyi | 0 3 files changed, 1 insertion(+), 8 deletions(-) rename test-data/unit/{fixtures => lib-stub}/attr.pyi (100%) diff --git a/mypy/plugin.py b/mypy/plugin.py index f6557f0b84a5..076f7aac25e2 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -434,7 +434,7 @@ def add_method( def attr_class_maker_callback(ctx: ClassDefContext) -> None: - """Add an __init__ method to classes decorated with attr.s.""" + """Add __init__ and __cmp__ methods to classes decorated with attr.s.""" def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: diff --git a/mypy/test/data.py b/mypy/test/data.py index 55f041647ff4..0be3be2e3fc9 100644 --- a/mypy/test/data.py +++ b/mypy/test/data.py @@ -101,13 +101,6 @@ def parse_test_cases( src_path = join(os.path.dirname(path), arg) with open(src_path) as f: files.append((join(base_path, 'typing.pyi'), f.read())) - elif p[i].id == 'add-module': - arg = p[i].arg - assert arg is not None - src_path = join(os.path.dirname(path), arg) - name = os.path.basename(src_path) - with open(src_path) as f: - files.append((join(base_path, name), f.read())) elif re.match(r'stale[0-9]*$', p[i].id): if p[i].id == 'stale': passnum = 1 diff --git a/test-data/unit/fixtures/attr.pyi b/test-data/unit/lib-stub/attr.pyi similarity index 100% rename from test-data/unit/fixtures/attr.pyi rename to test-data/unit/lib-stub/attr.pyi From 353319cd6dcffbf23d180d334297dcb0cb5d7029 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 27/81] Simplify stubs --- test-data/unit/check-attr.test | 27 +++++++------------ .../fixtures/{attr_builtins.pyi => attr.pyi} | 2 ++ 2 files changed, 11 insertions(+), 18 deletions(-) rename test-data/unit/fixtures/{attr_builtins.pyi => attr.pyi} (85%) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 237fc6e0de7b..801f2f158688 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -18,8 +18,7 @@ UnTyped(normal=1, private=2, def_arg=3, def_kwarg=4) UnTyped() # E: Too few arguments for "UnTyped" UnTyped(1, 2) == UnTyped(2, 3) UnTyped(1, 2) >= UnTyped(2, 3) -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testUntypedNoUntypedAttrS] # flags: --disallow-untyped-defs @@ -32,8 +31,7 @@ class UnTyped: _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable CLASS_VAR = 18 -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testTypedAttrS] import attr @@ -54,8 +52,7 @@ Typed(1, 2, def_kwarg=5) Typed(1, 2, def_kwarg=5) Typed(normal=1, private=2, def_arg=3, def_kwarg=4) Typed() # E: Too few arguments for "Typed" -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testAutoAttribAttrS] import attr @@ -78,8 +75,7 @@ Auto(1, 2, def_kwarg=5) Auto(1, 2, def_kwarg=5) Auto(normal=1, private=2, def_arg=3, def_kwarg=4) Auto() # E: Too few arguments for "Auto" -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testSeriousNamesAttrS] import typing @@ -100,8 +96,7 @@ Auto(1, 2, def_kwarg=5) Auto(1, 2, def_kwarg=5) Auto(normal=1, private=2, def_arg=3, def_kwarg=4) Auto() # E: Too few arguments for "Auto" -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testNoInitAttrS] from attr import attrib, attrs @@ -115,8 +110,7 @@ reveal_type(Auto) # E: Revealed type is 'def () -> __main__.Auto' Auto() Auto() == Auto() Auto() >= Auto() -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testNoCmpAttrS] from attr import attrib, attrs @@ -127,8 +121,7 @@ reveal_type(A) # E: Revealed type is 'def (b: builtins.int) -> __main__.A' A(5) A(5) == A(5) A(5) >= A(5) # E: Unsupported left operand type for >= ("A") -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testInheritanceAttrS] @@ -143,8 +136,7 @@ class B: class C(B, A): c: bool = attr.ib() reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool) -> __main__.C' -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] [case testNestedClassAttrS] import attr @@ -156,5 +148,4 @@ class C: x: int = attr.ib() reveal_type(C) # E: Revealed type is 'def (y: Any) -> __main__.C' reveal_type(C.D) # E: Revealed type is 'def (x: builtins.int) -> __main__.C.D' -[builtins fixtures/attr_builtins.pyi] -[add-module fixtures/attr.pyi] +[builtins fixtures/attr.pyi] diff --git a/test-data/unit/fixtures/attr_builtins.pyi b/test-data/unit/fixtures/attr.pyi similarity index 85% rename from test-data/unit/fixtures/attr_builtins.pyi rename to test-data/unit/fixtures/attr.pyi index 403e3ae2780a..01a118046f46 100644 --- a/test-data/unit/fixtures/attr_builtins.pyi +++ b/test-data/unit/fixtures/attr.pyi @@ -1,3 +1,5 @@ +# Builtins stub used to support @attr.s tests. + class object: def __init__(self) -> None: pass def __eq__(self, o: object) -> bool: pass From 72cc0a7ee24a14d5c7efd1847335e0f0e162315a Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:57 -0800 Subject: [PATCH 28/81] Cleanup tests --- mypy/plugin.py | 37 +++++-- test-data/unit/check-attr.test | 183 ++++++++++++++----------------- test-data/unit/fixtures/bool.pyi | 1 + 3 files changed, 111 insertions(+), 110 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 076f7aac25e2..f99fd980b790 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,14 +2,14 @@ from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, \ - cast +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, cast from mypy import messages -from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ - DictExpr, ClassDef, Argument, Var, TypeInfo, FuncDef, Block, \ - SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, \ - ARG_POS, ARG_OPT, EllipsisExpr, NameExpr +from mypy.nodes import ( + Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, TypeInfo, + FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_POS, + ARG_OPT, EllipsisExpr, NameExpr +) from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny @@ -431,10 +431,29 @@ def add_method( 'attr.attrib', 'attr.attr' } +# These are the argument to the functions with their defaults in their correct order. +attrib_arguments = OrderedDict([ + ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), + ('init', True), ('convert', None), ('metadata', {}), ('type', None) +]) +attrs_arguments = OrderedDict([ + ('maybe_cls', None), ('these', None), ('repr_ns', None), ('repr', True), ('cmp', True), + ('hash', None), ('init', True), ('slots', False), ('frozen', False), ('str', False), + ('auto_attribs', False) +]) def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add __init__ and __cmp__ methods to classes decorated with attr.s.""" + # TODO(David): + # o Comment explaining how attrs work so people know what this is doing. + # o Figure out Self Type + # o Explain strip("_") + # o Remove magic numbers + # o Figure out what to do with type=... + # o Cleanup builtins in tests. + # o Fix inheritance with attribute override. + # o Handle None from get_bool_argument def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: @@ -537,11 +556,7 @@ def is_class_var(expr: NameExpr) -> bool: # TODO: Can we do something useful with this? pass - add_init_argument( - name, - typ, - bool(default), - stmt) + add_init_argument(name, typ, bool(default), stmt) else: if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 801f2f158688..39a29f37d165 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1,130 +1,115 @@ -[case testUntypedAttrS] +[case testAttrsSimple] +# TODO(David): +# Add tests for: +# o type aliases (both inside and outside of a class) +# o type variables +# o decorated generic classes +# o type inference +# o forward references (both inside and outside of class) +# o importing decorated classes +# o instance and class methods in decorated types + import attr @attr.s -class UnTyped: - normal = attr.ib() - _private = attr.ib() - def_arg = attr.ib(18) - _def_kwarg = attr.ib(validator=None, default=18) - +class A: + a = attr.ib() + _b = attr.ib() + c = attr.ib(18) + _d = attr.ib(validator=None, default=18) CLASS_VAR = 18 -reveal_type(UnTyped) # E: Revealed type is 'def (normal: Any, private: Any, def_arg: Any =, def_kwarg: Any =) -> __main__.UnTyped' -UnTyped(1, 2) -UnTyped('1', '2', '3') -UnTyped('1', '2', '3', 4) -UnTyped('1', '2', def_kwarg='5') -UnTyped('1', '2', def_kwarg='5') -UnTyped(normal=1, private=2, def_arg=3, def_kwarg=4) -UnTyped() # E: Too few arguments for "UnTyped" -UnTyped(1, 2) == UnTyped(2, 3) -UnTyped(1, 2) >= UnTyped(2, 3) -[builtins fixtures/attr.pyi] +reveal_type(A) # E: Revealed type is 'def (a: Any, b: Any, c: Any =, d: Any =) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +A(1,2) +[builtins fixtures/bool.pyi] [case testUntypedNoUntypedAttrS] # flags: --disallow-untyped-defs import attr @attr.s -class UnTyped: +class A: normal = attr.ib() # E: Need type annotation for variable _private = attr.ib() # E: Need type annotation for variable def_arg = attr.ib(18) # E: Need type annotation for variable _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable CLASS_VAR = 18 -[builtins fixtures/attr.pyi] +[builtins fixtures/bool.pyi] -[case testTypedAttrS] +[case testAttrsAnnotated] import attr -import typing +from typing import List, ClassVar @attr.s -class Typed: - normal: int = attr.ib() - _private: int = attr.ib() - def_arg: int = attr.ib(18) - _def_kwarg: int = attr.ib(validator=None, default=18) - UNTYPED_CLASS_VAR = 7 - TYPED_CLASS_VAR: typing.ClassVar[int] = 22 -reveal_type(Typed) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Typed' -Typed(1, 2) -Typed(1, 2, 3) -Typed(1, 2, 3, 4) -Typed(1, 2, def_kwarg=5) -Typed(1, 2, def_kwarg=5) -Typed(normal=1, private=2, def_arg=3, def_kwarg=4) -Typed() # E: Too few arguments for "Typed" -[builtins fixtures/attr.pyi] +class A: + a: int = attr.ib() + _b: List[str] = attr.ib() + c: str = attr.ib('18') + _d: int = attr.ib(validator=None, default=18) + E = 7 + F: ClassVar[int] = 22 +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +A(1, ['2']) +[builtins fixtures/list.pyi] -[case testAutoAttribAttrS] +[case testAttrsAutoAttribs] import attr -import typing +from typing import List, ClassVar @attr.s(auto_attribs=True) -class Auto: - normal: int - _private: int - def_arg: int = 18 - _def_kwarg: int = attr.ib(validator=None, default=18) - - UNTYPED_CLASS_VAR = 18 - TYPED_CLASS_VAR: typing.ClassVar[int] = 18 - -reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' -Auto(1, 2) -Auto(1, 2, 3) -Auto(1, 2, 3, 4) -Auto(1, 2, def_kwarg=5) -Auto(1, 2, def_kwarg=5) -Auto(normal=1, private=2, def_arg=3, def_kwarg=4) -Auto() # E: Too few arguments for "Auto" -[builtins fixtures/attr.pyi] +class A: + a: int = attr.ib() + _b: List[str] = attr.ib() + c: str = attr.ib('18') + _d: int = attr.ib(validator=None, default=18) + E = 7 + F: ClassVar[int] = 22 +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +A(1, ['2']) +[builtins fixtures/list.pyi] -[case testSeriousNamesAttrS] -import typing +[case testAttrsSeriousNames] from attr import attrib, attrs -@attrs(auto_attribs=True) -class Auto: - normal: int - _private: int - def_arg: int = 18 - _def_kwarg: int = attrib(validator=None, default=18) - UNTYPED_CLASS_VAR = 18 - TYPED_CLASS_VAR: typing.ClassVar[int] = 18 -reveal_type(Auto) # E: Revealed type is 'def (normal: builtins.int, private: builtins.int, def_arg: builtins.int =, def_kwarg: builtins.int =) -> __main__.Auto' -Auto(1, 2) -Auto(1, 2, 3) -Auto(1, 2, 3, 4) -Auto(1, 2, def_kwarg=5) -Auto(1, 2, def_kwarg=5) -Auto(normal=1, private=2, def_arg=3, def_kwarg=4) -Auto() # E: Too few arguments for "Auto" -[builtins fixtures/attr.pyi] +@attrs +class A: + a = attrib() + _b: int = attrib() + c = attrib(18) + _d = attrib(validator=None, default=18) + CLASS_VAR = 18 +reveal_type(A) # E: Revealed type is 'def (a: Any, b: builtins.int, c: Any =, d: Any =) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +A(1,2) +[builtins fixtures/bool.pyi] -[case testNoInitAttrS] +[case testAttrsInitFalse] from attr import attrib, attrs @attrs(auto_attribs=True, init=False) -class Auto: - normal: int - _private: int - def_arg: int = 18 - _def_kwarg: int = attrib(validator=None, default=18) -reveal_type(Auto) # E: Revealed type is 'def () -> __main__.Auto' -Auto() -Auto() == Auto() -Auto() >= Auto() -[builtins fixtures/attr.pyi] +class A: + a: int + _b: int + c: int = 18 + _d: int = attrib(validator=None, default=18) +reveal_type(A) # E: Revealed type is 'def () -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +[builtins fixtures/bool.pyi] -[case testNoCmpAttrS] +[case testAttrsCmpFalse] from attr import attrib, attrs @attrs(auto_attribs=True, cmp=False) class A: - b: int -reveal_type(A) # E: Revealed type is 'def (b: builtins.int) -> __main__.A' -A(5) -A(5) == A(5) -A(5) >= A(5) # E: Unsupported left operand type for >= ("A") + a: int +reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (builtins.object, builtins.object) -> builtins.bool' +A(1) < A(2) # E: Unsupported left operand type for < ("A") [builtins fixtures/attr.pyi] -[case testInheritanceAttrS] +[case testAttrsInheritance] import attr @attr.s class A: @@ -132,13 +117,13 @@ class A: @attr.s class B: b: str = attr.ib() -@attr.s # type: ignore # Incompatible base classes in re cmp methods +@attr.s # type: ignore # Incompatible base classes because of __cmp__ methods. class C(B, A): c: bool = attr.ib() reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool) -> __main__.C' -[builtins fixtures/attr.pyi] +[builtins fixtures/bool.pyi] -[case testNestedClassAttrS] +[case testAttrsNestedInClasses] import attr @attr.s class C: @@ -148,4 +133,4 @@ class C: x: int = attr.ib() reveal_type(C) # E: Revealed type is 'def (y: Any) -> __main__.C' reveal_type(C.D) # E: Revealed type is 'def (x: builtins.int) -> __main__.C.D' -[builtins fixtures/attr.pyi] +[builtins fixtures/bool.pyi] diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index a1d1b9c1fdf5..bf506d97312f 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -12,3 +12,4 @@ class bool: pass class int: pass class str: pass class unicode: pass +class ellipsis: pass From e48091fdae345029656413c63c218676183ae2a7 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 29/81] Get rid of self type. --- mypy/plugin.py | 74 +++++++++++++--------------------- test-data/unit/check-attr.test | 2 + 2 files changed, 29 insertions(+), 47 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index f99fd980b790..a2ef37de00a1 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -394,32 +394,6 @@ def int_pow_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -def add_method( - info: TypeInfo, - method_name: str, - args: List[Argument], - ret_type: Type, - self_type: Type, - function_type: Instance) -> None: - from mypy.semanal import set_callable_name - - first = [Argument(Var('self'), self_type, None, ARG_POS)] - args = first + args - - arg_types = [arg.type_annotation for arg in args] - arg_names = [arg.variable.name() for arg in args] - arg_kinds = [arg.kind for arg in args] - assert None not in arg_types - signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, - ret_type, function_type) - func = FuncDef(method_name, args, Block([])) - func.info = info - func.is_class = False - func.type = set_callable_name(signature, func) - func._fullname = info.fullname() + '.' + method_name - info.names[method_name] = SymbolTableNode(MDEF, func) - - attr_class_makers = { 'attr.s', 'attr.attrs', @@ -454,6 +428,16 @@ def attr_class_maker_callback(ctx: ClassDefContext) -> None: # o Cleanup builtins in tests. # o Fix inheritance with attribute override. # o Handle None from get_bool_argument + # o Support frozen=True + + # attrs is a package that lets you define classes without writing dull boilerplate code. + # + # At a quick glance, the decorator searches the class object for instances of attr.ibs (or + # annotated variables if auto_attribs=True) and tries to add a bunch of helpful methods to + # the object. The most important for type checking purposes are __init__ and all the cmp + # methods. + # + # See http://www.attrs.org/en/stable/how-does-it-work.html for information on how this works. def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: @@ -496,11 +480,21 @@ def called_function(expr: Expression) -> Optional[str]: # Nothing to add. return - # To support nested classes we use fullname(). But fullname is . - # and named_type() expects only the name. - self_type = ctx.api.named_type(ctx.cls.info.fullname().split(".", 1)[1]) function_type = ctx.api.named_type('__builtins__.function') + def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: + """Create a method: def (self, ) -> ): ...""" + args = [Argument(Var('self'), AnyType(TypeOfAny.unannotated), None, ARG_POS)] + args + arg_types = [arg.type_annotation for arg in args] + arg_names = [arg.variable.name() for arg in args] + arg_kinds = [arg.kind for arg in args] + assert None not in arg_types + signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, + ret_type, function_type) + func = FuncDef(method_name, args, Block([]), signature) + ctx.api.accept(func) + ctx.cls.info.names[method_name] = SymbolTableNode(MDEF, func) + if init: # Walk the body looking for assignments. names = [] # type: List[str] @@ -569,27 +563,13 @@ def is_class_var(expr: NameExpr) -> bool: ARG_OPT if name in has_default else ARG_POS) for (name, typ) in zip(names, types) ] - - add_method( - info=ctx.cls.info, - method_name='__init__', - args=init_args, - ret_type=NoneTyp(), - self_type=self_type, - function_type=function_type, - ) + add_method('__init__', init_args, NoneTyp()) if cmp: bool_type = ctx.api.named_type('__builtins__.bool') - args = [Argument(Var('other', self_type), self_type, None, ARG_POS)] + other_type = UnboundType(ctx.cls.info.fullname().split(".", 1)[1]) + args = [Argument(Var('other', other_type), other_type, None, ARG_POS)] for method in ['__ne__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__']: - add_method( - info=ctx.cls.info, - method_name=method, - args=args, - ret_type=bool_type, - self_type=self_type, - function_type=function_type, - ) + add_method(method, args, bool_type) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 39a29f37d165..e6926b368875 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -17,6 +17,8 @@ class A: c = attr.ib(18) _d = attr.ib(validator=None, default=18) CLASS_VAR = 18 + + reveal_type(A) # E: Revealed type is 'def (a: Any, b: Any, c: Any =, d: Any =) -> __main__.A' reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' From 2d0f5dd1029f807d64238d15ba92ff2749aeb69c Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 30/81] Comments --- mypy/plugin.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index a2ef37de00a1..830a4bef6b6b 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -418,17 +418,14 @@ def int_pow_callback(ctx: MethodContext) -> Type: def attr_class_maker_callback(ctx: ClassDefContext) -> None: - """Add __init__ and __cmp__ methods to classes decorated with attr.s.""" + """Add necessary dunder methods to classes decorated with attr.s.""" # TODO(David): - # o Comment explaining how attrs work so people know what this is doing. - # o Figure out Self Type - # o Explain strip("_") # o Remove magic numbers # o Figure out what to do with type=... # o Cleanup builtins in tests. # o Fix inheritance with attribute override. # o Handle None from get_bool_argument - # o Support frozen=True + # o Support frozen=True? # attrs is a package that lets you define classes without writing dull boilerplate code. # @@ -532,6 +529,7 @@ def is_class_var(expr: NameExpr) -> bool: for stmt in info.defn.defs.body: if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): lhs = stmt.lvalues[0] + # Attrs removes leading underscores when creating the __init__ arguments. name = lhs.name.lstrip("_") typ = stmt.type @@ -566,8 +564,11 @@ def is_class_var(expr: NameExpr) -> bool: add_method('__init__', init_args, NoneTyp()) if cmp: - bool_type = ctx.api.named_type('__builtins__.bool') + # Generate cmp methods that look like this: + # def __ne__(self, other: '') -> bool: ... + # We use fullname to handle nested classes. other_type = UnboundType(ctx.cls.info.fullname().split(".", 1)[1]) + bool_type = ctx.api.named_type('__builtins__.bool') args = [Argument(Var('other', other_type), other_type, None, ARG_POS)] for method in ['__ne__', '__eq__', '__lt__', '__le__', From f7f6033322f6a75622752d432bc16cfb7edf237e Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 31/81] Remove magic numbers --- mypy/plugin.py | 88 ++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 830a4bef6b6b..76ba121a365b 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -394,17 +394,6 @@ def int_pow_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -attr_class_makers = { - 'attr.s', - 'attr.attrs', - 'attr.attributes', -} - -attr_attrib_makers = { - 'attr.ib', - 'attr.attrib', - 'attr.attr' -} # These are the argument to the functions with their defaults in their correct order. attrib_arguments = OrderedDict([ ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), @@ -415,17 +404,26 @@ def int_pow_callback(ctx: MethodContext) -> Type: ('hash', None), ('init', True), ('slots', False), ('frozen', False), ('str', False), ('auto_attribs', False) ]) - +attr_class_makers = { + 'attr.s': attrs_arguments, + 'attr.attrs': attrs_arguments, + 'attr.attributes': attrs_arguments, +} +attr_attrib_makers = { + 'attr.ib': attrib_arguments, + 'attr.attrib': attrib_arguments, + 'attr.attr': attrib_arguments, +} def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add necessary dunder methods to classes decorated with attr.s.""" # TODO(David): - # o Remove magic numbers # o Figure out what to do with type=... - # o Cleanup builtins in tests. # o Fix inheritance with attribute override. # o Handle None from get_bool_argument # o Support frozen=True? + # o Support @dataclass + # o Moar Tests! # attrs is a package that lets you define classes without writing dull boilerplate code. # @@ -436,27 +434,32 @@ def attr_class_maker_callback(ctx: ClassDefContext) -> None: # # See http://www.attrs.org/en/stable/how-does-it-work.html for information on how this works. - def get_argument(call: CallExpr, name: Optional[str], - num: Optional[int]) -> Optional[Expression]: + def called_function(expr: Expression) -> Optional[str]: + """Return the full name of the function being called by the expr, or None.""" + if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): + return expr.callee.fullname + return None + + def get_argument(call: CallExpr, arg_name: str, + func_args: OrderedDict) -> Optional[Expression]: + """Return the """ + arg_num = list(func_args).index(arg_name) + assert arg_num >= 0, "Function doesn't have arg {}".format(arg_name) for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): - if num is not None and not attr_name and i == num: + if not attr_name and i == arg_num: return attr_value - if name and attr_name == name: + if attr_name == arg_name: return attr_value return None - def get_bool_argument(call: CallExpr, name: str, - default: Optional[bool]) -> Optional[bool]: - for arg_name, arg_value in zip(call.arg_names, call.args): - if arg_name == name: - # TODO: Handle None being returned here. - return ctx.api.parse_bool(arg_value) - return default - - def called_function(expr: Expression) -> Optional[str]: - if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): - return expr.callee.fullname - return None + def get_bool_argument(call: CallExpr, arg_name: str, func_args: OrderedDict) -> Optional[bool]: + attr_value = get_argument(call, arg_name, func_args) + if attr_value: + # TODO: Handle None being returned here. + return ctx.api.parse_bool(attr_value) + ret = func_args[arg_name] + assert isinstance(ret, bool), "Default value for {} isn't boolean".format(arg_name) + return ret decorator = ctx.reason @@ -468,10 +471,9 @@ def called_function(expr: Expression) -> Optional[str]: if isinstance(decorator, CallExpr): # Read call arguments. # TODO handle these returning None (e.g. bool=SOMETHING_ELSE) - init = get_bool_argument(decorator, "init", init) - cmp = get_bool_argument(decorator, "cmp", cmp) - auto_attribs = get_bool_argument(decorator, "auto_attribs", - auto_attribs) + init = get_bool_argument(decorator, "init", attrs_arguments) + cmp = get_bool_argument(decorator, "cmp", attrs_arguments) + auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) if not init and not cmp: # Nothing to add. @@ -536,19 +538,15 @@ def is_class_var(expr: NameExpr) -> bool: if called_function(stmt.rvalue) in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) - # Is it an init=False argument? - attr_init = get_argument(stmt.rvalue, "init", 5) - if attr_init and ctx.api.parse_bool(attr_init) is False: + if get_bool_argument(stmt.rvalue, "init", attrib_arguments) is False: + # This attribute doesn't go in init. continue - # Look for default= in the call. - default = get_argument(stmt.rvalue, "default", 0) - attr_typ = get_argument(stmt.rvalue, "type", 15) - if attr_typ: - # TODO: Can we do something useful with this? - pass - - add_init_argument(name, typ, bool(default), stmt) + # Look for default= in the call. Note: This fails if someone + # passes the _NOTHING sentinel object into attrs. + attr_has_default = bool(get_argument(stmt.rvalue, "default", + attrib_arguments)) + add_init_argument(name, typ, attr_has_default, stmt) else: if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) From 2ff3a190b080a58a5c425cf2a7e3fd86150ca9c9 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 32/81] Cleanup argument pulling --- mypy/plugin.py | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 76ba121a365b..0b275719f241 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -442,7 +442,7 @@ def called_function(expr: Expression) -> Optional[str]: def get_argument(call: CallExpr, arg_name: str, func_args: OrderedDict) -> Optional[Expression]: - """Return the """ + """Return the expression for the specific argument.""" arg_num = list(func_args).index(arg_name) assert arg_num >= 0, "Function doesn't have arg {}".format(arg_name) for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): @@ -452,28 +452,32 @@ def get_argument(call: CallExpr, arg_name: str, return attr_value return None - def get_bool_argument(call: CallExpr, arg_name: str, func_args: OrderedDict) -> Optional[bool]: - attr_value = get_argument(call, arg_name, func_args) - if attr_value: - # TODO: Handle None being returned here. - return ctx.api.parse_bool(attr_value) - ret = func_args[arg_name] - assert isinstance(ret, bool), "Default value for {} isn't boolean".format(arg_name) - return ret + def get_bool_argument(expr: Expression, arg_name: str, func_args: OrderedDict) -> bool: + """Return the value of an argument name in the give Expression. + + If it's a CallExpr and the argument is one of the args then return it. + Otherwise return the default value for the argument. + """ + default = func_args[arg_name] + assert isinstance(default, bool), "Default value for {} isn't boolean".format(arg_name) + + if isinstance(expr, CallExpr): + attr_value = get_argument(expr, arg_name, func_args) + if attr_value: + # TODO: Handle None being returned here. + ret = ctx.api.parse_bool(attr_value) + if ret is None: + ctx.api.fail('"{}" argument must be True or False.'.format(arg_name), expr) + return default + return ret + return default decorator = ctx.reason - # Default values of attr.s() - init = True # type: Optional[bool] - cmp = True # type: Optional[bool] - auto_attribs = False # type: Optional[bool] - - if isinstance(decorator, CallExpr): - # Read call arguments. - # TODO handle these returning None (e.g. bool=SOMETHING_ELSE) - init = get_bool_argument(decorator, "init", attrs_arguments) - cmp = get_bool_argument(decorator, "cmp", attrs_arguments) - auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) + # Read the arguments off of attr.s() or the defaults of attr.s + init = get_bool_argument(decorator, "init", attrs_arguments) + cmp = get_bool_argument(decorator, "cmp", attrs_arguments) + auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) if not init and not cmp: # Nothing to add. From d06bd744a1329f7d23dbaf8008d250979dc5516e Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 33/81] Fix multiple inheritance override --- mypy/plugin.py | 48 ++++++++++++++++++---------------- test-data/unit/check-attr.test | 23 ++++++++++++++++ 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 0b275719f241..d2ab4cdfbcf1 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,13 +2,13 @@ from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, cast +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, cast, Dict from mypy import messages from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, TypeInfo, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_POS, - ARG_OPT, EllipsisExpr, NameExpr + ARG_OPT, EllipsisExpr, NameExpr, Node ) from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, @@ -76,6 +76,10 @@ def fail(self, msg: str, ctx: Context, serious: bool = False, *, blocker: bool = False) -> None: raise NotImplementedError + @abstractmethod + def accept(self, node: Node) -> None: + raise NotImplementedError + # A context for a function hook that infers the return type of a function with # a special signature. @@ -419,7 +423,6 @@ def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add necessary dunder methods to classes decorated with attr.s.""" # TODO(David): # o Figure out what to do with type=... - # o Fix inheritance with attribute override. # o Handle None from get_bool_argument # o Support frozen=True? # o Support @dataclass @@ -500,16 +503,11 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: if init: # Walk the body looking for assignments. - names = [] # type: List[str] - types = [] # type: List[Type] - has_default = set() # type: Set[str] + init_args = OrderedDict() # type: : OrderedDict[str, Argument] + init_arg_sources = {} # type: Dict[str, Context] def add_init_argument(name: str, typ: Optional[Type], default: bool, context: Context) -> None: - if not default and has_default: - ctx.api.fail( - "Non-default attributes not allowed after default attributes.", - context) if not typ: if ctx.api.options.disallow_untyped_defs: # This is a compromise. If you don't have a type here then the __init__ will @@ -519,11 +517,13 @@ def add_init_argument(name: str, typ: Optional[Type], default: bool, ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) typ = AnyType(TypeOfAny.unannotated) - names.append(name) - assert typ is not None - types.append(typ) - if default: - has_default.add(name) + init_arg = Argument(Var(name, typ), typ, + EllipsisExpr() if default else None, + ARG_OPT if default else ARG_POS) + if name in init_args: + del init_args[name] + init_args[name] = init_arg + init_arg_sources[name] = Expression def is_class_var(expr: NameExpr) -> bool: if isinstance(expr.node, Var): @@ -557,13 +557,17 @@ def is_class_var(expr: NameExpr) -> bool: has_rhs = not isinstance(stmt.rvalue, TempNode) add_init_argument(name, typ, has_rhs, stmt) - init_args = [ - Argument(Var(name, typ), typ, - EllipsisExpr() if name in has_default else None, - ARG_OPT if name in has_default else ARG_POS) - for (name, typ) in zip(names, types) - ] - add_method('__init__', init_args, NoneTyp()) + # Check the init args for correct default-ness. Note: Has to be done after all the + # attributes for all classes have been read, because subclasses can override parents. + last_default = False + for name, argument in init_args.items(): + if not argument.initializer and last_default: + ctx.api.fail( + "Non-default attributes not allowed after default attributes.", + init_arg_sources[name]) + last_default = bool(argument.initializer) + + add_method('__init__', list(init_args.values()), NoneTyp()) if cmp: # Generate cmp methods that look like this: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index e6926b368875..d40733f67e50 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -136,3 +136,26 @@ class C: reveal_type(C) # E: Revealed type is 'def (y: Any) -> __main__.C' reveal_type(C.D) # E: Revealed type is 'def (x: builtins.int) -> __main__.C.D' [builtins fixtures/bool.pyi] + +[case testAttrsInheritanceOverride] +import attr + +@attr.s +class A: + a: int = attr.ib() + x: int = attr.ib() + +@attr.s +class B(A): + b: str = attr.ib() + x: int = attr.ib(default='value') + +@attr.s +class C(B): + c: bool = attr.ib() # No error here because the x below overwrite the x above. + x: int = attr.ib() + +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, x: builtins.int) -> __main__.A' +reveal_type(B) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, x: builtins.int =) -> __main__.B' +reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool, x: builtins.int) -> __main__.C' +[builtins fixtures/bool.pyi] From 6fbe81fbf08e40a7e2b8d5aed329d8f577537325 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 34/81] Support type= --- mypy/plugin.py | 31 ++++++++++++++++++++++++++----- test-data/unit/check-attr.test | 9 +++++++++ test-data/unit/lib-stub/attr.pyi | 4 ++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index d2ab4cdfbcf1..b87083122ef0 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,16 +2,18 @@ from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, cast, Dict +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict from mypy import messages +from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.nodes import ( - Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, TypeInfo, + Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_POS, ARG_OPT, EllipsisExpr, NameExpr, Node ) +from mypy.tvar_scope import TypeVarScope from mypy.types import ( - Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, + Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny ) from mypy.messages import MessageBuilder @@ -80,6 +82,15 @@ def fail(self, msg: str, ctx: Context, serious: bool = False, *, def accept(self, node: Node) -> None: raise NotImplementedError + @abstractmethod + def anal_type(self, t: Type, *, + tvar_scope: Optional[TypeVarScope] = None, + allow_tuple_literal: bool = False, + aliasing: bool = False, + third_pass: bool = False) -> Type: + raise NotImplementedError + + # A context for a function hook that infers the return type of a function with # a special signature. @@ -422,8 +433,6 @@ def int_pow_callback(ctx: MethodContext) -> Type: def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add necessary dunder methods to classes decorated with attr.s.""" # TODO(David): - # o Figure out what to do with type=... - # o Handle None from get_bool_argument # o Support frozen=True? # o Support @dataclass # o Moar Tests! @@ -550,6 +559,18 @@ def is_class_var(expr: NameExpr) -> bool: # passes the _NOTHING sentinel object into attrs. attr_has_default = bool(get_argument(stmt.rvalue, "default", attrib_arguments)) + + # If the type isn't set through annotation but it is passed through type= + # use that. + type_arg = get_argument(stmt.rvalue, "type", attrib_arguments) + if type_arg and not typ: + try: + un_type = expr_to_unanalyzed_type(type_arg) + except TypeTranslationError: + ctx.api.fail('Invalid argument to type', type_arg) + else: + typ = ctx.api.anal_type(un_type) + add_init_argument(name, typ, attr_has_default, stmt) else: if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index d40733f67e50..821259dacffb 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -159,3 +159,12 @@ reveal_type(A) # E: Revealed type is 'def (a: builtins.int, x: builtins.int) -> reveal_type(B) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, x: builtins.int =) -> __main__.B' reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool, x: builtins.int) -> __main__.C' [builtins fixtures/bool.pyi] + +[case testAttrsTypeEquals] +import attr + +@attr.s +class A: + a = attr.ib(type=int) +reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.A' +[builtins fixtures/bool.pyi] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index ab2809648512..2b00a15e4e75 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -1,8 +1,8 @@ -from typing import TypeVar, overload, Callable, Any +from typing import TypeVar, overload, Callable, Any, Type _T = TypeVar('_T') -def attr(default: Any = ..., validator: Any = ...) -> Any: ... +def attr(default: Any = ..., validator: Any = ..., type: Type[_T] = ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ...) -> _T: ... From ccfd6c05a33a37bc69515c252744656771c7179a Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 35/81] Support frozen=True --- mypy/plugin.py | 183 ++++++++++++++++--------------- test-data/unit/check-attr.test | 11 ++ test-data/unit/lib-stub/attr.pyi | 4 +- 3 files changed, 108 insertions(+), 90 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index b87083122ef0..1e4d52d2d0a1 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -430,10 +430,10 @@ def int_pow_callback(ctx: MethodContext) -> Type: 'attr.attr': attrib_arguments, } + def attr_class_maker_callback(ctx: ClassDefContext) -> None: """Add necessary dunder methods to classes decorated with attr.s.""" # TODO(David): - # o Support frozen=True? # o Support @dataclass # o Moar Tests! @@ -444,7 +444,7 @@ def attr_class_maker_callback(ctx: ClassDefContext) -> None: # the object. The most important for type checking purposes are __init__ and all the cmp # methods. # - # See http://www.attrs.org/en/stable/how-does-it-work.html for information on how this works. + # See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. def called_function(expr: Expression) -> Optional[str]: """Return the full name of the function being called by the expr, or None.""" @@ -484,16 +484,79 @@ def get_bool_argument(expr: Expression, arg_name: str, func_args: OrderedDict) - return ret return default + def is_class_var(expr: NameExpr) -> bool: + if isinstance(expr.node, Var): + return expr.node.is_classvar + return False + decorator = ctx.reason - # Read the arguments off of attr.s() or the defaults of attr.s - init = get_bool_argument(decorator, "init", attrs_arguments) - cmp = get_bool_argument(decorator, "cmp", attrs_arguments) + # Step 1. Walk the class body (including the MRO) looking for the attributes. + + class Attribute(NamedTuple('Attribute', [('name', str), ('type', Type), ('has_default', bool), + ('init', bool), ('context', Context)])): + + def argument(self): + # Attrs removes leading underscores when creating the __init__ arguments. + return Argument(Var(self.name.strip("_"), self.type), self.type, + EllipsisExpr() if self.has_default else None, + ARG_OPT if self.has_default else ARG_POS) + + attributes = OrderedDict() # type: OrderedDict[str, Attribute] + + def add_attribute(name: str, typ: Optional[Type], default: bool, init: bool, + context: Context) -> None: + if not typ: + if ctx.api.options.disallow_untyped_defs: + # This is a compromise. If you don't have a type here then the __init__ will + # be untyped. But since the __init__ method doesn't have a line number it's + # difficult to point to the correct line number. So instead we just show the + # error in the assignment, which is where you would fix the issue. + ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) + typ = AnyType(TypeOfAny.unannotated) + + if name in attributes: + # When a subclass overrides an attrib it gets pushed to the end. + del attributes[name] + attributes[name] = Attribute(name, typ, default, init, context) + auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) - if not init and not cmp: - # Nothing to add. - return + # Walk the mro in reverse looking for those yummy attributes. + for info in reversed(ctx.cls.info.mro): + for stmt in info.defn.defs.body: + if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): + lhs = stmt.lvalues[0] + name = lhs.name + typ = stmt.type + + if called_function(stmt.rvalue) in attr_attrib_makers: + assert isinstance(stmt.rvalue, CallExpr) + + # Look for default= in the call. Note: This fails if someone + # passes the _NOTHING sentinel object into attrs. + attr_has_default = bool(get_argument(stmt.rvalue, "default", + attrib_arguments)) + + # If the type isn't set through annotation but it is passed through type= + # use that. + type_arg = get_argument(stmt.rvalue, "type", attrib_arguments) + if type_arg and not typ: + try: + un_type = expr_to_unanalyzed_type(type_arg) + except TypeTranslationError: + ctx.api.fail('Invalid argument to type', type_arg) + else: + typ = ctx.api.anal_type(un_type) + + add_attribute(name, typ, attr_has_default, + get_bool_argument(stmt.rvalue, "init", attrib_arguments), + stmt) + else: + if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(stmt.rvalue, TempNode) + add_attribute(name, typ, has_rhs, True, stmt) function_type = ctx.api.named_type('__builtins__.function') @@ -510,94 +573,38 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: ctx.api.accept(func) ctx.cls.info.names[method_name] = SymbolTableNode(MDEF, func) - if init: - # Walk the body looking for assignments. - init_args = OrderedDict() # type: : OrderedDict[str, Argument] - init_arg_sources = {} # type: Dict[str, Context] - - def add_init_argument(name: str, typ: Optional[Type], default: bool, - context: Context) -> None: - if not typ: - if ctx.api.options.disallow_untyped_defs: - # This is a compromise. If you don't have a type here then the __init__ will - # be untyped. But since the __init__ method doesn't have a line number it's - # difficult to point to the correct line number. So instead we just show the - # error in the assignment, which is where you would fix the issue. - ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) - typ = AnyType(TypeOfAny.unannotated) - - init_arg = Argument(Var(name, typ), typ, - EllipsisExpr() if default else None, - ARG_OPT if default else ARG_POS) - if name in init_args: - del init_args[name] - init_args[name] = init_arg - init_arg_sources[name] = Expression - - def is_class_var(expr: NameExpr) -> bool: - if isinstance(expr.node, Var): - return expr.node.is_classvar - return False - - # Walk the mro in reverse looking for those yummy attributes. - for info in reversed(ctx.cls.info.mro): - for stmt in info.defn.defs.body: - if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): - lhs = stmt.lvalues[0] - # Attrs removes leading underscores when creating the __init__ arguments. - name = lhs.name.lstrip("_") - typ = stmt.type - - if called_function(stmt.rvalue) in attr_attrib_makers: - assert isinstance(stmt.rvalue, CallExpr) - - if get_bool_argument(stmt.rvalue, "init", attrib_arguments) is False: - # This attribute doesn't go in init. - continue - - # Look for default= in the call. Note: This fails if someone - # passes the _NOTHING sentinel object into attrs. - attr_has_default = bool(get_argument(stmt.rvalue, "default", - attrib_arguments)) - - # If the type isn't set through annotation but it is passed through type= - # use that. - type_arg = get_argument(stmt.rvalue, "type", attrib_arguments) - if type_arg and not typ: - try: - un_type = expr_to_unanalyzed_type(type_arg) - except TypeTranslationError: - ctx.api.fail('Invalid argument to type', type_arg) - else: - typ = ctx.api.anal_type(un_type) - - add_init_argument(name, typ, attr_has_default, stmt) - else: - if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(stmt.rvalue, TempNode) - add_init_argument(name, typ, has_rhs, stmt) + if get_bool_argument(decorator, "init", attrs_arguments): + # Generate the __init__ method. - # Check the init args for correct default-ness. Note: Has to be done after all the + # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. last_default = False - for name, argument in init_args.items(): - if not argument.initializer and last_default: + for name, attribute in attributes.items(): + if not attribute.has_default and last_default: ctx.api.fail( "Non-default attributes not allowed after default attributes.", - init_arg_sources[name]) - last_default = bool(argument.initializer) - - add_method('__init__', list(init_args.values()), NoneTyp()) - - if cmp: + attribute.context) + last_default = attribute.has_default + + add_method('__init__', + [attribute.argument() for attribute in attributes.values() + if attribute.init], + NoneTyp()) + + if get_bool_argument(decorator, "frozen", attrs_arguments): + # If the class is frozen then all the attributes need to be turned into properties. + for name in attributes: + node = ctx.cls.info.names[name].node + assert isinstance(node, Var) + node.is_initialized_in_class = False + node.is_property = True + + if get_bool_argument(decorator, "cmp", attrs_arguments): # Generate cmp methods that look like this: # def __ne__(self, other: '') -> bool: ... - # We use fullname to handle nested classes. + # We use fullname to handle nested classes, splitting to remove the module name. other_type = UnboundType(ctx.cls.info.fullname().split(".", 1)[1]) bool_type = ctx.api.named_type('__builtins__.bool') args = [Argument(Var('other', other_type), other_type, None, ARG_POS)] - for method in ['__ne__', '__eq__', - '__lt__', '__le__', - '__gt__', '__ge__']: + for method in ['__ne__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__']: add_method(method, args, bool_type) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 821259dacffb..51e073e2b065 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -168,3 +168,14 @@ class A: a = attr.ib(type=int) reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.A' [builtins fixtures/bool.pyi] + +[case testAttrsFrozen] +import attr + +@attr.s(frozen=True) +class A: + a = attr.ib() + +a = A(5) +a.a = 16 # E: Property "a" defined in "A" is read-only +[builtins fixtures/bool.pyi] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 2b00a15e4e75..ef2dc5457d35 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -5,9 +5,9 @@ _T = TypeVar('_T') def attr(default: Any = ..., validator: Any = ..., type: Type[_T] = ...) -> Any: ... @overload -def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ...) -> _T: ... +def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> _T: ... @overload -def attributes(maybe_cls: None = ..., cmp: bool = ..., init: bool = ...) -> Callable[[_T], _T]: ... +def attributes(maybe_cls: None = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> Callable[[_T], _T]: ... # aliases s = attrs = attributes From 4d9c1828d40780dfb702711b66a8ed8a56d44469 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 36/81] Support @attr.dataclass --- mypy/plugin.py | 31 ++++++++++++++++--------------- test-data/unit/check-attr.test | 23 ++++++++++++++++++++--- test-data/unit/lib-stub/attr.pyi | 1 + 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 1e4d52d2d0a1..8e9a6a013281 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,7 +2,8 @@ from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict +from functools import partial +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict, Any from mypy import messages from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError @@ -287,7 +288,7 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: if fullname in attr_class_makers: - return attr_class_maker_callback + return partial(attr_class_maker_callback, attr_class_makers[fullname]) return None @@ -410,10 +411,6 @@ def int_pow_callback(ctx: MethodContext) -> Type: # These are the argument to the functions with their defaults in their correct order. -attrib_arguments = OrderedDict([ - ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), - ('init', True), ('convert', None), ('metadata', {}), ('type', None) -]) attrs_arguments = OrderedDict([ ('maybe_cls', None), ('these', None), ('repr_ns', None), ('repr', True), ('cmp', True), ('hash', None), ('init', True), ('slots', False), ('frozen', False), ('str', False), @@ -423,7 +420,12 @@ def int_pow_callback(ctx: MethodContext) -> Type: 'attr.s': attrs_arguments, 'attr.attrs': attrs_arguments, 'attr.attributes': attrs_arguments, + 'attr.dataclass': OrderedDict(attrs_arguments, auto_attribs=True), } +attrib_arguments = OrderedDict([ + ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), + ('init', True), ('convert', None), ('metadata', {}), ('type', None) +]) attr_attrib_makers = { 'attr.ib': attrib_arguments, 'attr.attrib': attrib_arguments, @@ -431,12 +433,8 @@ def int_pow_callback(ctx: MethodContext) -> Type: } -def attr_class_maker_callback(ctx: ClassDefContext) -> None: +def attr_class_maker_callback(attrs_arguments: 'OrderedDict[str, Any]', ctx: ClassDefContext) -> None: """Add necessary dunder methods to classes decorated with attr.s.""" - # TODO(David): - # o Support @dataclass - # o Moar Tests! - # attrs is a package that lets you define classes without writing dull boilerplate code. # # At a quick glance, the decorator searches the class object for instances of attr.ibs (or @@ -453,7 +451,7 @@ def called_function(expr: Expression) -> Optional[str]: return None def get_argument(call: CallExpr, arg_name: str, - func_args: OrderedDict) -> Optional[Expression]: + func_args: 'OrderedDict[str, Any]') -> Optional[Expression]: """Return the expression for the specific argument.""" arg_num = list(func_args).index(arg_name) assert arg_num >= 0, "Function doesn't have arg {}".format(arg_name) @@ -464,7 +462,7 @@ def get_argument(call: CallExpr, arg_name: str, return attr_value return None - def get_bool_argument(expr: Expression, arg_name: str, func_args: OrderedDict) -> bool: + def get_bool_argument(expr: Expression, arg_name: str, func_args: 'OrderedDict[str, Any]') -> bool: """Return the value of an argument name in the give Expression. If it's a CallExpr and the argument is one of the args then return it. @@ -485,18 +483,19 @@ def get_bool_argument(expr: Expression, arg_name: str, func_args: OrderedDict) - return default def is_class_var(expr: NameExpr) -> bool: + """Return whether the expression is ClassVar[...]""" if isinstance(expr.node, Var): return expr.node.is_classvar return False decorator = ctx.reason - # Step 1. Walk the class body (including the MRO) looking for the attributes. + # Walk the class body (including the MRO) looking for the attributes. class Attribute(NamedTuple('Attribute', [('name', str), ('type', Type), ('has_default', bool), ('init', bool), ('context', Context)])): - def argument(self): + def argument(self) -> Argument: # Attrs removes leading underscores when creating the __init__ arguments. return Argument(Var(self.name.strip("_"), self.type), self.type, EllipsisExpr() if self.has_default else None, @@ -520,6 +519,7 @@ def add_attribute(name: str, typ: Optional[Type], default: bool, init: bool, del attributes[name] attributes[name] = Attribute(name, typ, default, init, context) + # auto_attribs means we generate attributes from annotated variables. auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) # Walk the mro in reverse looking for those yummy attributes. @@ -570,6 +570,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, ret_type, function_type) func = FuncDef(method_name, args, Block([]), signature) + # The accept will resolve all unbound variables, etc. ctx.api.accept(func) ctx.cls.info.names[method_name] = SymbolTableNode(MDEF, func) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 51e073e2b065..379c0c467148 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -60,9 +60,9 @@ import attr from typing import List, ClassVar @attr.s(auto_attribs=True) class A: - a: int = attr.ib() - _b: List[str] = attr.ib() - c: str = attr.ib('18') + a: int + _b: List[str] + c: str = '18' _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 @@ -179,3 +179,20 @@ class A: a = A(5) a.a = 16 # E: Property "a" defined in "A" is read-only [builtins fixtures/bool.pyi] + +[case testAttrsDataClass] +import attr +from typing import List, ClassVar +@attr.dataclass +class A: + a: int + _b: List[str] + c: str = '18' + _d: int = attr.ib(validator=None, default=18) + E = 7 + F: ClassVar[int] = 22 +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +A(1, ['2']) +[builtins fixtures/list.pyi] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index ef2dc5457d35..90b58ebc1e3e 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -12,3 +12,4 @@ def attributes(maybe_cls: None = ..., cmp: bool = ..., init: bool = ..., frozen: # aliases s = attrs = attributes ib = attrib = attr +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) From 2fc6f5f0ffa051e169cca6a11da3b01e8ce55b09 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 37/81] Add more tests --- test-data/unit/check-attr.test | 103 ++++++++++++++++++++---- test-data/unit/fixtures/classmethod.pyi | 1 + 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 379c0c467148..656287d5859d 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1,13 +1,7 @@ [case testAttrsSimple] # TODO(David): # Add tests for: -# o type aliases (both inside and outside of a class) -# o type variables -# o decorated generic classes # o type inference -# o forward references (both inside and outside of class) -# o importing decorated classes -# o instance and class methods in decorated types import attr @attr.s @@ -16,8 +10,7 @@ class A: _b = attr.ib() c = attr.ib(18) _d = attr.ib(validator=None, default=18) - CLASS_VAR = 18 - + E = 18 reveal_type(A) # E: Revealed type is 'def (a: Any, b: Any, c: Any =, d: Any =) -> __main__.A' reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' @@ -30,12 +23,11 @@ A(1,2) import attr @attr.s class A: - normal = attr.ib() # E: Need type annotation for variable - _private = attr.ib() # E: Need type annotation for variable - def_arg = attr.ib(18) # E: Need type annotation for variable - _def_kwarg = attr.ib(validator=None, default=18) # E: Need type annotation for variable - - CLASS_VAR = 18 + a = attr.ib() # E: Need type annotation for variable + _b = attr.ib() # E: Need type annotation for variable + c = attr.ib(18) # E: Need type annotation for variable + _d = attr.ib(validator=None, default=18) # E: Need type annotation for variable + E = 18 [builtins fixtures/bool.pyi] [case testAttrsAnnotated] @@ -196,3 +188,86 @@ reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __ma reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1, ['2']) [builtins fixtures/list.pyi] + +[case testAttrsTypeAlias] +from typing import List +import attr +Alias = List[int] +@attr.s(auto_attribs=True) +class A: + Alias2 = List[str] + x: Alias + y: Alias2 = attr.ib() +reveal_type(A) # E: Revealed type is 'def (x: builtins.list[builtins.int], y: builtins.list[builtins.str]) -> __main__.A' +[builtins fixtures/list.pyi] + +[case testAttrsTypeVariable] +from typing import TypeVar, Generic, List +import attr +T = TypeVar('T') +@attr.s(auto_attribs=True) +class A(Generic[T]): + x: List[T] + y: T = attr.ib() +reveal_type(A) # E: Revealed type is 'def [T] (x: builtins.list[T`1], y: T`1) -> __main__.A[T`1]' + +a = A([1], 2) +reveal_type(a) # E: Revealed type is '__main__.A[builtins.int*]' +[builtins fixtures/list.pyi] + +[case testAttrsForwardReference] +import attr +@attr.s(auto_attribs=True) +class A: + parent: B + +@attr.s(auto_attribs=True) +class B: + parent: A + +reveal_type(A) # E: Revealed type is 'def (parent: __main__.B) -> __main__.A' +reveal_type(B) # E: Revealed type is 'def (parent: __main__.A) -> __main__.B' +A(B(None)) +[builtins fixtures/list.pyi] + +[case testAttrsForwardReferenceInClass] +import attr +@attr.s(auto_attribs=True) +class A: + parent: A.B + + @attr.s(auto_attribs=True) + class B: + parent: A + +reveal_type(A) # E: Revealed type is 'def (parent: __main__.A.B) -> __main__.A' +reveal_type(A.B) # E: Revealed type is 'def (parent: __main__.A) -> __main__.A.B' +A(A.B(None)) +[builtins fixtures/list.pyi] + +[case testAttrsImporting] +from helper import A +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> helper.A' +[file helper.py] +import attr +@attr.s(auto_attribs=True) +class A: + a: int + b: str = attr.ib() +[builtins fixtures/list.pyi] + +[case testAttrsOtherMethods] +import attr +@attr.s(auto_attribs=True) +class A: + a: int + b: str = attr.ib() + @classmethod + def new(cls) -> A: + return A(6, 'hello') + def foo(self) -> int: + return self.a +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> __main__.A' +a = A.new() +reveal_type(a.foo) # E: Revealed type is 'def () -> builtins.int' +[builtins fixtures/classmethod.pyi] diff --git a/test-data/unit/fixtures/classmethod.pyi b/test-data/unit/fixtures/classmethod.pyi index 6d7f71bd52ff..bac4ea7d153b 100644 --- a/test-data/unit/fixtures/classmethod.pyi +++ b/test-data/unit/fixtures/classmethod.pyi @@ -22,5 +22,6 @@ class int: class str: pass class bytes: pass class bool: pass +class ellipsis: pass class tuple(typing.Generic[_T]): pass From d11c138e03b75dde0c0785550f04c8c4cf9be10c Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 38/81] Cleanup --- mypy/plugin.py | 60 +++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 8e9a6a013281..523879ac6666 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -3,7 +3,7 @@ from collections import OrderedDict from abc import abstractmethod from functools import partial -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict, Any +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Any from mypy import messages from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError @@ -92,7 +92,6 @@ def anal_type(self, t: Type, *, raise NotImplementedError - # A context for a function hook that infers the return type of a function with # a special signature. # @@ -410,22 +409,26 @@ def int_pow_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -# These are the argument to the functions with their defaults in their correct order. +# Arguments to the attr functions (attr.s and attr.ib) with their defaults in their correct order. +# These are needed to find the actual value from the CallExpr. attrs_arguments = OrderedDict([ ('maybe_cls', None), ('these', None), ('repr_ns', None), ('repr', True), ('cmp', True), ('hash', None), ('init', True), ('slots', False), ('frozen', False), ('str', False), ('auto_attribs', False) ]) +attrib_arguments = OrderedDict([ + ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), + ('init', True), ('convert', None), ('metadata', {}), ('type', None) +]) + +# The names of the different functions that create classes or arguments. +# The right hand side is an OrderedDict of the arguments to the call. attr_class_makers = { 'attr.s': attrs_arguments, 'attr.attrs': attrs_arguments, 'attr.attributes': attrs_arguments, 'attr.dataclass': OrderedDict(attrs_arguments, auto_attribs=True), } -attrib_arguments = OrderedDict([ - ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), - ('init', True), ('convert', None), ('metadata', {}), ('type', None) -]) attr_attrib_makers = { 'attr.ib': attrib_arguments, 'attr.attrib': attrib_arguments, @@ -433,14 +436,18 @@ def int_pow_callback(ctx: MethodContext) -> Type: } -def attr_class_maker_callback(attrs_arguments: 'OrderedDict[str, Any]', ctx: ClassDefContext) -> None: - """Add necessary dunder methods to classes decorated with attr.s.""" +def attr_class_maker_callback(attrs_arguments: 'OrderedDict[str, Any]', + ctx: ClassDefContext) -> None: + """Add necessary dunder methods to classes decorated with attr.s. + + Currently supports init=True, cmp=True and frozen=True. + """ # attrs is a package that lets you define classes without writing dull boilerplate code. # - # At a quick glance, the decorator searches the class object for instances of attr.ibs (or - # annotated variables if auto_attribs=True) and tries to add a bunch of helpful methods to - # the object. The most important for type checking purposes are __init__ and all the cmp - # methods. + # At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or + # annotated variables if auto_attribs=True), then depending on how the decorator is called, + # it will add an __init__ or all the __cmp__ methods. For frozen=True it will turn the attrs + # into properties. # # See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. @@ -462,7 +469,8 @@ def get_argument(call: CallExpr, arg_name: str, return attr_value return None - def get_bool_argument(expr: Expression, arg_name: str, func_args: 'OrderedDict[str, Any]') -> bool: + def get_bool_argument(expr: Expression, arg_name: str, + func_args: 'OrderedDict[str, Any]') -> bool: """Return the value of an argument name in the give Expression. If it's a CallExpr and the argument is one of the args then return it. @@ -474,7 +482,6 @@ def get_bool_argument(expr: Expression, arg_name: str, func_args: 'OrderedDict[s if isinstance(expr, CallExpr): attr_value = get_argument(expr, arg_name, func_args) if attr_value: - # TODO: Handle None being returned here. ret = ctx.api.parse_bool(attr_value) if ret is None: ctx.api.fail('"{}" argument must be True or False.'.format(arg_name), expr) @@ -503,21 +510,21 @@ def argument(self) -> Argument: attributes = OrderedDict() # type: OrderedDict[str, Attribute] - def add_attribute(name: str, typ: Optional[Type], default: bool, init: bool, + def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init: bool, context: Context) -> None: - if not typ: + if not attr_type: if ctx.api.options.disallow_untyped_defs: # This is a compromise. If you don't have a type here then the __init__ will # be untyped. But since the __init__ method doesn't have a line number it's # difficult to point to the correct line number. So instead we just show the # error in the assignment, which is where you would fix the issue. ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) - typ = AnyType(TypeOfAny.unannotated) + attr_type = AnyType(TypeOfAny.unannotated) - if name in attributes: + if attr_name in attributes: # When a subclass overrides an attrib it gets pushed to the end. - del attributes[name] - attributes[name] = Attribute(name, typ, default, init, context) + del attributes[attr_name] + attributes[attr_name] = Attribute(attr_name, attr_type, default, init, context) # auto_attribs means we generate attributes from annotated variables. auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) @@ -530,17 +537,20 @@ def add_attribute(name: str, typ: Optional[Type], default: bool, init: bool, name = lhs.name typ = stmt.type - if called_function(stmt.rvalue) in attr_attrib_makers: + func_name = called_function(stmt.rvalue) + + if func_name in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) + func_arguments = attr_attrib_makers[func_name] # Look for default= in the call. Note: This fails if someone # passes the _NOTHING sentinel object into attrs. attr_has_default = bool(get_argument(stmt.rvalue, "default", - attrib_arguments)) + func_arguments)) # If the type isn't set through annotation but it is passed through type= # use that. - type_arg = get_argument(stmt.rvalue, "type", attrib_arguments) + type_arg = get_argument(stmt.rvalue, "type", func_arguments) if type_arg and not typ: try: un_type = expr_to_unanalyzed_type(type_arg) @@ -550,7 +560,7 @@ def add_attribute(name: str, typ: Optional[Type], default: bool, init: bool, typ = ctx.api.anal_type(un_type) add_attribute(name, typ, attr_has_default, - get_bool_argument(stmt.rvalue, "init", attrib_arguments), + get_bool_argument(stmt.rvalue, "init", func_arguments), stmt) else: if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): From dd947d2d32d8431e6be950a95ee3762b1924a118 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 39/81] Support x.default decorator --- mypy/plugin.py | 37 ++++++++++++++++++++++++++++++---- test-data/unit/check-attr.test | 12 +++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 523879ac6666..7235169f63b8 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -10,8 +10,7 @@ from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_POS, - ARG_OPT, EllipsisExpr, NameExpr, Node -) + ARG_OPT, EllipsisExpr, NameExpr, Node, Decorator, MemberExpr) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, @@ -499,10 +498,20 @@ def is_class_var(expr: NameExpr) -> bool: # Walk the class body (including the MRO) looking for the attributes. - class Attribute(NamedTuple('Attribute', [('name', str), ('type', Type), ('has_default', bool), - ('init', bool), ('context', Context)])): + class Attribute: + """An attribute that belongs to this class.""" + + def __init__(self, name: str, type: Type, + has_default: bool, init: bool, context: Context) -> None: + # I really wanted to use attrs for this. :) + self.name = name + self.type = type + self.has_default = has_default + self.init = init + self.context = context def argument(self) -> Argument: + """Return this attribute as an argument to __init__.""" # Attrs removes leading underscores when creating the __init__ arguments. return Argument(Var(self.name.strip("_"), self.type), self.type, EllipsisExpr() if self.has_default else None, @@ -567,6 +576,26 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(stmt.rvalue, TempNode) add_attribute(name, typ, has_rhs, True, stmt) + elif isinstance(stmt, Decorator): + # Look for attr specific decorators. ('x.default' and 'x.validator') + remove_me = [] + for func_decorator in stmt.decorators: + if (isinstance(func_decorator, MemberExpr) + and isinstance(func_decorator.expr, NameExpr) + and func_decorator.expr.name in attributes): + if func_decorator.name == 'default': + # This decorator lets you set a default after the fact. + attributes[func_decorator.expr.name].has_default = True + + if func_decorator.name in ('default', 'validator'): + # These are decorators on the attrib object that only exist during + # class creation time. In order to not trigger a type error later we + # just remove them. This might leave us with a Decorator with no + # decorators (Emperor's new clothes?) + remove_me.append(func_decorator) + + for dec in remove_me: + stmt.decorators.remove(dec) function_type = ctx.api.named_type('__builtins__.function') diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 656287d5859d..6233a3c702f5 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -271,3 +271,15 @@ reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> a = A.new() reveal_type(a.foo) # E: Revealed type is 'def () -> builtins.int' [builtins fixtures/classmethod.pyi] + +[case testAttrsDefaultDecorator] +import attr +@attr.s +class C(object): + x: int = attr.ib(default=1) + y: int = attr.ib() + @y.default + def name_does_not_matter(self): + return self.x + 1 +C() +[builtins fixtures/list.pyi] From ce2cac60b93ebb0b0ab8bbfb43a785299d5c3e21 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 40/81] Fix issue with classmethods calling cls --- mypy/plugin.py | 9 +++++++++ test-data/unit/check-attr.test | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 7235169f63b8..a9e24ce3118d 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -631,6 +631,15 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: if attribute.init], NoneTyp()) + for stmt in ctx.cls.defs.body: + # The implicit first type of cls methods will be wrong because it's based on + # the non-existent init. Set it back to any and accept it to correct it. + if isinstance(stmt, Decorator) and stmt.func.is_class: + func_type = stmt.func.type + if isinstance(func_type, CallableType): + func_type.arg_types[0] = AnyType(TypeOfAny.unannotated) + ctx.api.accept(stmt.func) + if get_bool_argument(decorator, "frozen", attrs_arguments): # If the class is frozen then all the attributes need to be turned into properties. for name in attributes: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 6233a3c702f5..2272379800ce 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -264,7 +264,7 @@ class A: b: str = attr.ib() @classmethod def new(cls) -> A: - return A(6, 'hello') + return cls(6, 'hello') def foo(self) -> int: return self.a reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> __main__.A' From d05c3646354be1fca79d4a482b7327256042d9e0 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 41/81] Get rid of accept. It was causing weird issues --- mypy/plugin.py | 35 ++++++++++++++++++---------------- test-data/unit/check-attr.test | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index a9e24ce3118d..b6fb0951794d 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -9,8 +9,9 @@ from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, - FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_POS, - ARG_OPT, EllipsisExpr, NameExpr, Node, Decorator, MemberExpr) + FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, + ARG_POS, ARG_OPT, EllipsisExpr, NameExpr, Decorator, MemberExpr, TypeInfo +) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, @@ -18,6 +19,7 @@ ) from mypy.messages import MessageBuilder from mypy.options import Options +from mypy.typevars import fill_typevars class TypeAnalyzerPluginInterface: @@ -78,10 +80,6 @@ def fail(self, msg: str, ctx: Context, serious: bool = False, *, blocker: bool = False) -> None: raise NotImplementedError - @abstractmethod - def accept(self, node: Node) -> None: - raise NotImplementedError - @abstractmethod def anal_type(self, t: Type, *, tvar_scope: Optional[TypeVarScope] = None, @@ -90,6 +88,9 @@ def anal_type(self, t: Type, *, third_pass: bool = False) -> Type: raise NotImplementedError + def class_type(self, info: TypeInfo) -> Type: + raise NotImplementedError + # A context for a function hook that infers the return type of a function with # a special signature. @@ -597,21 +598,25 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init for dec in remove_me: stmt.decorators.remove(dec) + info = ctx.cls.info + self_type = fill_typevars(info) function_type = ctx.api.named_type('__builtins__.function') def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: """Create a method: def (self, ) -> ): ...""" - args = [Argument(Var('self'), AnyType(TypeOfAny.unannotated), None, ARG_POS)] + args + from mypy.semanal import set_callable_name + args = [Argument(Var('self'), self_type, None, ARG_POS)] + args arg_types = [arg.type_annotation for arg in args] arg_names = [arg.variable.name() for arg in args] arg_kinds = [arg.kind for arg in args] assert None not in arg_types signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, ret_type, function_type) - func = FuncDef(method_name, args, Block([]), signature) - # The accept will resolve all unbound variables, etc. - ctx.api.accept(func) - ctx.cls.info.names[method_name] = SymbolTableNode(MDEF, func) + func = FuncDef(method_name, args, Block([])) + func.info = info + func.type = set_callable_name(signature, func) + func._fullname = info.fullname() + '.' + method_name + info.names[method_name] = SymbolTableNode(MDEF, func) if get_bool_argument(decorator, "init", attrs_arguments): # Generate the __init__ method. @@ -633,12 +638,11 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: for stmt in ctx.cls.defs.body: # The implicit first type of cls methods will be wrong because it's based on - # the non-existent init. Set it back to any and accept it to correct it. + # the non-existent init. Set it correctly. if isinstance(stmt, Decorator) and stmt.func.is_class: func_type = stmt.func.type if isinstance(func_type, CallableType): - func_type.arg_types[0] = AnyType(TypeOfAny.unannotated) - ctx.api.accept(stmt.func) + func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info) if get_bool_argument(decorator, "frozen", attrs_arguments): # If the class is frozen then all the attributes need to be turned into properties. @@ -652,8 +656,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: # Generate cmp methods that look like this: # def __ne__(self, other: '') -> bool: ... # We use fullname to handle nested classes, splitting to remove the module name. - other_type = UnboundType(ctx.cls.info.fullname().split(".", 1)[1]) bool_type = ctx.api.named_type('__builtins__.bool') - args = [Argument(Var('other', other_type), other_type, None, ARG_POS)] + args = [Argument(Var('other', self_type), self_type, None, ARG_POS)] for method in ['__ne__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__']: add_method(method, args, bool_type) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 2272379800ce..c85b235d46d6 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -264,6 +264,7 @@ class A: b: str = attr.ib() @classmethod def new(cls) -> A: + reveal_type(cls) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> __main__.A' return cls(6, 'hello') def foo(self) -> int: return self.a @@ -283,3 +284,16 @@ class C(object): return self.x + 1 C() [builtins fixtures/list.pyi] + +[case testAttrsLocalVariablesInClassMethod] +import attr +@attr.s(auto_attribs=True) +class A: + a: int + b: int = attr.ib() + @classmethod + def new(cls, foo: int) -> A: + a = foo + b = a + return cls(a, b) +[builtins fixtures/classmethod.pyi] From 695b42273a271bc2dc47e3f81755a9f979fac19b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 42/81] Fix Forward Reference Resolution issue --- mypy/plugin.py | 17 ++++++++++++----- test-data/unit/check-attr.test | 23 +++++++++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index b6fb0951794d..29740205f33b 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -10,7 +10,7 @@ from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_POS, ARG_OPT, EllipsisExpr, NameExpr, Decorator, MemberExpr, TypeInfo + ARG_POS, ARG_OPT, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt ) from mypy.tvar_scope import TypeVarScope from mypy.types import ( @@ -515,7 +515,7 @@ def argument(self) -> Argument: """Return this attribute as an argument to __init__.""" # Attrs removes leading underscores when creating the __init__ arguments. return Argument(Var(self.name.strip("_"), self.type), self.type, - EllipsisExpr() if self.has_default else None, + None, ARG_OPT if self.has_default else ARG_POS) attributes = OrderedDict() # type: OrderedDict[str, Attribute] @@ -602,7 +602,8 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init self_type = fill_typevars(info) function_type = ctx.api.named_type('__builtins__.function') - def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: + def add_method(method_name: str, args: List[Argument], ret_type: Type, + add_to_body: bool = False) -> None: """Create a method: def (self, ) -> ): ...""" from mypy.semanal import set_callable_name args = [Argument(Var('self'), self_type, None, ARG_POS)] + args @@ -612,11 +613,14 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: assert None not in arg_types signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, ret_type, function_type) - func = FuncDef(method_name, args, Block([])) + func = FuncDef(method_name, args, Block([PassStmt()])) func.info = info func.type = set_callable_name(signature, func) func._fullname = info.fullname() + '.' + method_name + func.line = ctx.cls.line info.names[method_name] = SymbolTableNode(MDEF, func) + if add_to_body: + info.defn.defs.body.append(func) if get_bool_argument(decorator, "init", attrs_arguments): # Generate the __init__ method. @@ -631,10 +635,12 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: attribute.context) last_default = attribute.has_default + # __init__ gets added to the body so that it can get further semantic analysis. + # e.g. Forward Reference Resolution. add_method('__init__', [attribute.argument() for attribute in attributes.values() if attribute.init], - NoneTyp()) + NoneTyp(), add_to_body=True) for stmt in ctx.cls.defs.body: # The implicit first type of cls methods will be wrong because it's based on @@ -659,4 +665,5 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type) -> None: bool_type = ctx.api.named_type('__builtins__.bool') args = [Argument(Var('other', self_type), self_type, None, ARG_POS)] for method in ['__ne__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__']: + # Don't add these to the body to avoid incompatible with supertype when subclassing. add_method(method, args, bool_type) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index c85b235d46d6..e028df36406b 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -18,10 +18,10 @@ reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __ma A(1,2) [builtins fixtures/bool.pyi] -[case testUntypedNoUntypedAttrS] +[case testAttrsUntypedNoUntypedDefs] # flags: --disallow-untyped-defs import attr -@attr.s +@attr.s # E: Function is missing a type annotation for one or more arguments class A: a = attr.ib() # E: Need type annotation for variable _b = attr.ib() # E: Need type annotation for variable @@ -297,3 +297,22 @@ class A: b = a return cls(a, b) [builtins fixtures/classmethod.pyi] + +[case testAttrsUnionForward] +import attr +from typing import Union, List + +@attr.s(auto_attribs=True) +class A: + frob: List['AOrB'] + +class B: + pass + +AOrB = Union[A, B] + +reveal_type(A) # E: Revealed type is 'def (frob: builtins.list[Union[__main__.A, __main__.B]]) -> __main__.A' +reveal_type(B) # E: Revealed type is 'def () -> __main__.B' + +A([B()]) +[builtins fixtures/list.pyi] From 76fa908595e5544bad5aeaf2c28dbe7fac686a92 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:58 -0800 Subject: [PATCH 43/81] Better types for argument lookups --- mypy/plugin.py | 110 ++++++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 29740205f33b..97c35dda86d8 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -3,7 +3,7 @@ from collections import OrderedDict from abc import abstractmethod from functools import partial -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Any +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict from mypy import messages from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError @@ -409,25 +409,51 @@ def int_pow_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -# Arguments to the attr functions (attr.s and attr.ib) with their defaults in their correct order. -# These are needed to find the actual value from the CallExpr. -attrs_arguments = OrderedDict([ - ('maybe_cls', None), ('these', None), ('repr_ns', None), ('repr', True), ('cmp', True), - ('hash', None), ('init', True), ('slots', False), ('frozen', False), ('str', False), - ('auto_attribs', False) -]) -attrib_arguments = OrderedDict([ - ('default', None), ('validator', None), ('repr', True), ('cmp', True), ('hash', None), - ('init', True), ('convert', None), ('metadata', {}), ('type', None) -]) +# Arguments to the attr functions (attr.s and attr.ib) with their defaults. +ArgumentInfo = NamedTuple( + 'ArgumentInfo', [ + ('default', Optional[bool]), + ('kwarg_name', Optional[str]), + ('index', Optional[int]), + ]) + +AttrClassMaker = NamedTuple( + 'AttrClassMaker', [ + ('init', ArgumentInfo), + ('frozen', ArgumentInfo), + ('cmp', ArgumentInfo), + ('auto_attribs', ArgumentInfo) + ] +) + +AttribMaker = NamedTuple( + 'AttribMaker', [ + ('default', ArgumentInfo), + ('init', ArgumentInfo), + ("type", ArgumentInfo), + ] +) + +attrs_arguments = AttrClassMaker( + cmp=ArgumentInfo(True, 'cmp', 4), + init=ArgumentInfo(True, 'init', 6), + frozen=ArgumentInfo(False, 'frozen', 8), + auto_attribs=ArgumentInfo(False, 'auto_attribs', 10) +) +attrib_arguments = AttribMaker( + default=ArgumentInfo(None, 'default', 0), + init=ArgumentInfo(True, "init", 5), + type=ArgumentInfo(None, 'type', 8), +) + # The names of the different functions that create classes or arguments. -# The right hand side is an OrderedDict of the arguments to the call. attr_class_makers = { 'attr.s': attrs_arguments, 'attr.attrs': attrs_arguments, 'attr.attributes': attrs_arguments, - 'attr.dataclass': OrderedDict(attrs_arguments, auto_attribs=True), + 'attr.dataclass': attrs_arguments._replace( + auto_attribs=ArgumentInfo(True, 'auto_attribs', 10)), } attr_attrib_makers = { 'attr.ib': attrib_arguments, @@ -436,11 +462,14 @@ def int_pow_callback(ctx: MethodContext) -> Type: } -def attr_class_maker_callback(attrs_arguments: 'OrderedDict[str, Any]', - ctx: ClassDefContext) -> None: +def attr_class_maker_callback( + attrs: AttrClassMaker, + ctx: ClassDefContext, + attribs: Dict[str, AttribMaker] = attr_attrib_makers +) -> None: """Add necessary dunder methods to classes decorated with attr.s. - Currently supports init=True, cmp=True and frozen=True. + Currently supports init=, cmp= and frozen= and auto_attribs=. """ # attrs is a package that lets you define classes without writing dull boilerplate code. # @@ -457,37 +486,35 @@ def called_function(expr: Expression) -> Optional[str]: return expr.callee.fullname return None - def get_argument(call: CallExpr, arg_name: str, - func_args: 'OrderedDict[str, Any]') -> Optional[Expression]: + def get_argument(call: CallExpr, arg: ArgumentInfo) -> Optional[Expression]: """Return the expression for the specific argument.""" - arg_num = list(func_args).index(arg_name) - assert arg_num >= 0, "Function doesn't have arg {}".format(arg_name) + if not arg.kwarg_name and not arg.index: + return None + for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): - if not attr_name and i == arg_num: + if arg.index is not None and not attr_name and i == arg.index: return attr_value - if attr_name == arg_name: + if attr_name == arg.kwarg_name: return attr_value return None - def get_bool_argument(expr: Expression, arg_name: str, - func_args: 'OrderedDict[str, Any]') -> bool: + def get_bool_argument(expr: Expression, arg: ArgumentInfo) -> bool: """Return the value of an argument name in the give Expression. If it's a CallExpr and the argument is one of the args then return it. Otherwise return the default value for the argument. """ - default = func_args[arg_name] - assert isinstance(default, bool), "Default value for {} isn't boolean".format(arg_name) - + assert arg.default is not None, "{}: default should be True or False".format(arg) if isinstance(expr, CallExpr): - attr_value = get_argument(expr, arg_name, func_args) + attr_value = get_argument(expr, arg) if attr_value: ret = ctx.api.parse_bool(attr_value) if ret is None: - ctx.api.fail('"{}" argument must be True or False.'.format(arg_name), expr) - return default + ctx.api.fail('"{}" argument must be True or False.'.format( + arg.kwarg_name), expr) + return arg.default return ret - return default + return arg.default def is_class_var(expr: NameExpr) -> bool: """Return whether the expression is ClassVar[...]""" @@ -537,7 +564,7 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init attributes[attr_name] = Attribute(attr_name, attr_type, default, init, context) # auto_attribs means we generate attributes from annotated variables. - auto_attribs = get_bool_argument(decorator, "auto_attribs", attrs_arguments) + auto_attribs = get_bool_argument(decorator, attrs.auto_attribs) # Walk the mro in reverse looking for those yummy attributes. for info in reversed(ctx.cls.info.mro): @@ -549,18 +576,17 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init func_name = called_function(stmt.rvalue) - if func_name in attr_attrib_makers: + if func_name in attribs: assert isinstance(stmt.rvalue, CallExpr) - func_arguments = attr_attrib_makers[func_name] + attrib = attribs[func_name] # Look for default= in the call. Note: This fails if someone # passes the _NOTHING sentinel object into attrs. - attr_has_default = bool(get_argument(stmt.rvalue, "default", - func_arguments)) + attr_has_default = bool(get_argument(stmt.rvalue, attrib.default)) # If the type isn't set through annotation but it is passed through type= # use that. - type_arg = get_argument(stmt.rvalue, "type", func_arguments) + type_arg = get_argument(stmt.rvalue, attrib.type) if type_arg and not typ: try: un_type = expr_to_unanalyzed_type(type_arg) @@ -570,7 +596,7 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init typ = ctx.api.anal_type(un_type) add_attribute(name, typ, attr_has_default, - get_bool_argument(stmt.rvalue, "init", func_arguments), + get_bool_argument(stmt.rvalue, attrib.init), stmt) else: if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): @@ -622,7 +648,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if add_to_body: info.defn.defs.body.append(func) - if get_bool_argument(decorator, "init", attrs_arguments): + if get_bool_argument(decorator, attrs.init): # Generate the __init__ method. # Check the init args for correct default-ness. Note: This has to be done after all the @@ -650,7 +676,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if isinstance(func_type, CallableType): func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info) - if get_bool_argument(decorator, "frozen", attrs_arguments): + if get_bool_argument(decorator, attrs.frozen): # If the class is frozen then all the attributes need to be turned into properties. for name in attributes: node = ctx.cls.info.names[name].node @@ -658,7 +684,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, node.is_initialized_in_class = False node.is_property = True - if get_bool_argument(decorator, "cmp", attrs_arguments): + if get_bool_argument(decorator, attrs.cmp): # Generate cmp methods that look like this: # def __ne__(self, other: '') -> bool: ... # We use fullname to handle nested classes, splitting to remove the module name. From 405204d205e24faa990152f120c3872c2b88139d Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 44/81] Use type= to set the annotation --- mypy/plugin.py | 5 +++++ test-data/unit/check-attr.test | 7 ++++--- test-data/unit/lib-stub/attr.pyi | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 97c35dda86d8..0f7b4f8fb1b0 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -571,6 +571,7 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init for stmt in info.defn.defs.body: if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): lhs = stmt.lvalues[0] + assert isinstance(lhs, NameExpr) name = lhs.name typ = stmt.type @@ -594,6 +595,10 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init ctx.api.fail('Invalid argument to type', type_arg) else: typ = ctx.api.anal_type(un_type) + if typ and isinstance(lhs.node, Var) and not lhs.node.type: + # If there is no annotation, add one. + lhs.node.type = typ + lhs.is_inferred_def = False add_attribute(name, typ, attr_has_default, get_bool_argument(stmt.rvalue, attrib.init), diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index e028df36406b..e2c419e079fe 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -140,11 +140,11 @@ class A: @attr.s class B(A): b: str = attr.ib() - x: int = attr.ib(default='value') + x: int = attr.ib(default=22) @attr.s class C(B): - c: bool = attr.ib() # No error here because the x below overwrite the x above. + c: bool = attr.ib() # No error here because the x below overwrites the x above. x: int = attr.ib() reveal_type(A) # E: Revealed type is 'def (a: builtins.int, x: builtins.int) -> __main__.A' @@ -158,7 +158,8 @@ import attr @attr.s class A: a = attr.ib(type=int) -reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.A' + b = attr.ib(18, type=int) +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.int =) -> __main__.A' [builtins fixtures/bool.pyi] [case testAttrsFrozen] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 90b58ebc1e3e..179084ad58d4 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -2,7 +2,10 @@ from typing import TypeVar, overload, Callable, Any, Type _T = TypeVar('_T') -def attr(default: Any = ..., validator: Any = ..., type: Type[_T] = ...) -> Any: ... +@overload +def attr(default: _T = ..., validator: Any = ..., type: type = ...) -> _T: ... +@overload +def attr(default: None = ..., validator: Any = ..., type: type = ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> _T: ... From 536117f8cb29d4257292ab3ee7c55c935d712d35 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 45/81] Add Python2 annotation test --- test-data/unit/check-attr.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index e2c419e079fe..7bf58994279b 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -47,6 +47,23 @@ reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __ma A(1, ['2']) [builtins fixtures/list.pyi] +[case testAttrsPython2Annotations] +import attr +from typing import List, ClassVar +@attr.s +class A: + a = attr.ib() # type: int + _b = attr.ib() # type: List[str] + c = attr.ib('18') # type: str + _d = attr.ib(validator=None, default=18) # type: int + E = 7 + F: ClassVar[int] = 22 +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' +A(1, ['2']) +[builtins fixtures/list.pyi] + [case testAttrsAutoAttribs] import attr from typing import List, ClassVar From 0fa72a974d83542d945465cd9dbae0764b705c3a Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 46/81] Support convert= (Happy New Year!) --- mypy/plugin.py | 20 +++++++++++++++++--- test-data/unit/check-attr.test | 15 +++++++++++++++ test-data/unit/lib-stub/attr.pyi | 6 +++--- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 0f7b4f8fb1b0..bf3e3c9e5b99 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -10,7 +10,7 @@ from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_POS, ARG_OPT, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt + ARG_POS, ARG_OPT, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase ) from mypy.tvar_scope import TypeVarScope from mypy.types import ( @@ -430,7 +430,8 @@ def int_pow_callback(ctx: MethodContext) -> Type: 'AttribMaker', [ ('default', ArgumentInfo), ('init', ArgumentInfo), - ("type", ArgumentInfo), + ('type', ArgumentInfo), + ('convert', ArgumentInfo), ] ) @@ -442,7 +443,8 @@ def int_pow_callback(ctx: MethodContext) -> Type: ) attrib_arguments = AttribMaker( default=ArgumentInfo(None, 'default', 0), - init=ArgumentInfo(True, "init", 5), + init=ArgumentInfo(True, 'init', 5), + convert=ArgumentInfo(None, 'convert', 6), type=ArgumentInfo(None, 'type', 8), ) @@ -600,6 +602,18 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init lhs.node.type = typ lhs.is_inferred_def = False + # If the attrib has a convert function take the type of the first argument + # for the init type. + convert = get_argument(stmt.rvalue, attrib.convert) + if (convert + and isinstance(convert, RefExpr) + and convert.node + and isinstance(convert.node, FuncBase) + and convert.node.type + and isinstance(convert.node.type, CallableType) + and convert.node.type.arg_types): + typ = convert.node.type.arg_types[0] + add_attribute(name, typ, attr_has_default, get_bool_argument(stmt.rvalue, attrib.init), stmt) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 7bf58994279b..42af841c8dc9 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -334,3 +334,18 @@ reveal_type(B) # E: Revealed type is 'def () -> __main__.B' A([B()]) [builtins fixtures/list.pyi] + +[case testAttrsUsingConvert] +import attr + +def convert(s:int) -> str: + return 'hello' + +@attr.s +class C: + x: str = attr.ib(convert=convert) + +# Because of the convert the __init__ takes an int, but the variable is a str. +reveal_type(C) # E: Revealed type is 'def (x: builtins.int) -> __main__.C' +reveal_type(C(15).x) # E: Revealed type is 'builtins.str' +[builtins fixtures/list.pyi] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 179084ad58d4..66cb43819d7b 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -1,11 +1,11 @@ -from typing import TypeVar, overload, Callable, Any, Type +from typing import TypeVar, overload, Callable, Any, Type, Optional _T = TypeVar('_T') @overload -def attr(default: _T = ..., validator: Any = ..., type: type = ...) -> _T: ... +def attr(default: _T = ..., validator: Any = ..., convert: Optional[Callable[[Any], _T]] = ..., type: Optional[Callable[..., _T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: Any = ..., type: type = ...) -> Any: ... +def attr(default: None = ..., validator: None = ..., convert: None = ..., type: None = ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> _T: ... From 6506d7f0fca36b47c6447c4e03bc111ee435cfc1 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 47/81] Make cmp methods work with superclasses --- mypy/plugin.py | 30 ++++++++++++----- test-data/unit/check-attr.test | 60 ++++++++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index bf3e3c9e5b99..ee9b0cbc33cf 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -15,7 +15,7 @@ from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, - AnyType, TypeList, UnboundType, TypeOfAny + AnyType, TypeList, UnboundType, TypeOfAny, TypeVarDef ) from mypy.messages import MessageBuilder from mypy.options import Options @@ -648,7 +648,8 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init function_type = ctx.api.named_type('__builtins__.function') def add_method(method_name: str, args: List[Argument], ret_type: Type, - add_to_body: bool = False) -> None: + self_type: Type = self_type, + tvd: Optional[TypeVarDef] = None) -> None: """Create a method: def (self, ) -> ): ...""" from mypy.semanal import set_callable_name args = [Argument(Var('self'), self_type, None, ARG_POS)] + args @@ -658,14 +659,15 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, assert None not in arg_types signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, ret_type, function_type) + if tvd: + signature.variables = [tvd] func = FuncDef(method_name, args, Block([PassStmt()])) func.info = info func.type = set_callable_name(signature, func) func._fullname = info.fullname() + '.' + method_name func.line = ctx.cls.line info.names[method_name] = SymbolTableNode(MDEF, func) - if add_to_body: - info.defn.defs.body.append(func) + info.defn.defs.body.append(func) if get_bool_argument(decorator, attrs.init): # Generate the __init__ method. @@ -685,7 +687,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, add_method('__init__', [attribute.argument() for attribute in attributes.values() if attribute.init], - NoneTyp(), add_to_body=True) + NoneTyp()) for stmt in ctx.cls.defs.body: # The implicit first type of cls methods will be wrong because it's based on @@ -708,7 +710,19 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, # def __ne__(self, other: '') -> bool: ... # We use fullname to handle nested classes, splitting to remove the module name. bool_type = ctx.api.named_type('__builtins__.bool') - args = [Argument(Var('other', self_type), self_type, None, ARG_POS)] - for method in ['__ne__', '__eq__', '__lt__', '__le__', '__gt__', '__ge__']: - # Don't add these to the body to avoid incompatible with supertype when subclassing. + object_type = ctx.api.named_type('__builtins__.object') + + # For __ne__ and __eq__ the type is: def __ne__(self, other: object) -> bool + args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] + for method in ['__ne__', '__eq__']: add_method(method, args, bool_type) + + # For the rest we use: + # T = TypeVar('T') + # def __lt__(self: T, other: T) -> bool + # This way we comparisons with subclasses will work correctly. + tvd = TypeVarDef('AT', 'AT', 1, [], object_type) + tvd_type = TypeVarType(tvd) + args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] + for method in ['__lt__', '__le__', '__gt__', '__ge__']: + add_method(method, args, bool_type, self_type=tvd_type, tvd=tvd) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 42af841c8dc9..7e885bbbaa61 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -13,8 +13,6 @@ class A: E = 18 reveal_type(A) # E: Revealed type is 'def (a: Any, b: Any, c: Any =, d: Any =) -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1,2) [builtins fixtures/bool.pyi] @@ -42,8 +40,6 @@ class A: E = 7 F: ClassVar[int] = 22 reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1, ['2']) [builtins fixtures/list.pyi] @@ -59,8 +55,6 @@ class A: E = 7 F: ClassVar[int] = 22 reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1, ['2']) [builtins fixtures/list.pyi] @@ -76,8 +70,6 @@ class A: E = 7 F: ClassVar[int] = 22 reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1, ['2']) [builtins fixtures/list.pyi] @@ -91,8 +83,6 @@ class A: _d = attrib(validator=None, default=18) CLASS_VAR = 18 reveal_type(A) # E: Revealed type is 'def (a: Any, b: builtins.int, c: Any =, d: Any =) -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1,2) [builtins fixtures/bool.pyi] @@ -105,8 +95,6 @@ class A: c: int = 18 _d: int = attrib(validator=None, default=18) reveal_type(A) # E: Revealed type is 'def () -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' [builtins fixtures/bool.pyi] [case testAttrsCmpFalse] @@ -202,8 +190,6 @@ class A: E = 7 F: ClassVar[int] = 22 reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' -reveal_type(A.__lt__) # E: Revealed type is 'def (self: __main__.A, other: __main__.A) -> builtins.bool' A(1, ['2']) [builtins fixtures/list.pyi] @@ -349,3 +335,49 @@ class C: reveal_type(C) # E: Revealed type is 'def (x: builtins.int) -> __main__.C' reveal_type(C(15).x) # E: Revealed type is 'builtins.str' [builtins fixtures/list.pyi] + +[case testAttrsCmpWithSubclasses] +import attr +@attr.s +class A: pass +@attr.s +class B: pass +@attr.s +class C(A, B): pass +@attr.s +class D(A): pass + +reveal_type(A.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(B.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(C.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(D.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' + +A() < A() +B() < B() +A() < B() # E: Unsupported operand types for > ("B" and "A") + +C() > A() +C() > B() +C() > C() +C() > D() # E: Unsupported operand types for < ("D" and "C") + +D() >= A() +D() >= B() # E: Unsupported operand types for <= ("B" and "D") +D() >= C() # E: Unsupported operand types for <= ("C" and "D") +D() >= D() + +A() <= 1 # E: Unsupported operand types for <= ("A" and "int") +B() <= 1 # E: Unsupported operand types for <= ("B" and "int") +C() <= 1 # E: Unsupported operand types for <= ("C" and "int") +D() <= 1 # E: Unsupported operand types for <= ("D" and "int") + +A() == A() +B() == A() +C() == A() +D() == A() + +A() == int +B() == int +C() == int +D() == int +[builtins fixtures/list.pyi] From 5d16792719ff4c9c16bdda54b45cfc11964a046a Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 48/81] Do the correct MRO handling --- mypy/plugin.py | 108 +++++++++++++++++++------------ test-data/unit/check-attr.test | 17 ++++- test-data/unit/lib-stub/attr.pyi | 4 +- 3 files changed, 83 insertions(+), 46 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index ee9b0cbc33cf..683427f85559 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -10,7 +10,7 @@ from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_POS, ARG_OPT, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase + ARG_POS, ARG_OPT, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, Statement ) from mypy.tvar_scope import TypeVarScope from mypy.types import ( @@ -464,6 +464,14 @@ def int_pow_callback(ctx: MethodContext) -> Type: } +class LastUpdatedOrderedDict(OrderedDict): + """Store items in the order the keys were last added.""" + def __setitem__(self, key, value): + if key in self: + del self[key] + OrderedDict.__setitem__(self, key, value) + + def attr_class_maker_callback( attrs: AttrClassMaker, ctx: ClassDefContext, @@ -531,9 +539,9 @@ def is_class_var(expr: NameExpr) -> bool: class Attribute: """An attribute that belongs to this class.""" - def __init__(self, name: str, type: Type, - has_default: bool, init: bool, context: Context) -> None: - # I really wanted to use attrs for this. :) + def __init__(self, name: str, type: Optional[Type], + has_default: bool, init: bool, + context: Context) -> None: self.name = name self.type = type self.has_default = has_default @@ -542,35 +550,20 @@ def __init__(self, name: str, type: Type, def argument(self) -> Argument: """Return this attribute as an argument to __init__.""" + # Convert type not set to Any. + _type = self.type or AnyType(TypeOfAny.unannotated) # Attrs removes leading underscores when creating the __init__ arguments. - return Argument(Var(self.name.strip("_"), self.type), self.type, + return Argument(Var(self.name.strip("_"), _type), _type, None, ARG_OPT if self.has_default else ARG_POS) - attributes = OrderedDict() # type: OrderedDict[str, Attribute] - - def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init: bool, - context: Context) -> None: - if not attr_type: - if ctx.api.options.disallow_untyped_defs: - # This is a compromise. If you don't have a type here then the __init__ will - # be untyped. But since the __init__ method doesn't have a line number it's - # difficult to point to the correct line number. So instead we just show the - # error in the assignment, which is where you would fix the issue. - ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) - attr_type = AnyType(TypeOfAny.unannotated) - - if attr_name in attributes: - # When a subclass overrides an attrib it gets pushed to the end. - del attributes[attr_name] - attributes[attr_name] = Attribute(attr_name, attr_type, default, init, context) - # auto_attribs means we generate attributes from annotated variables. auto_attribs = get_bool_argument(decorator, attrs.auto_attribs) - # Walk the mro in reverse looking for those yummy attributes. - for info in reversed(ctx.cls.info.mro): - for stmt in info.defn.defs.body: + def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: + attributes = LastUpdatedOrderedDict() # type: OrderedDict[str, Attribute] + + for stmt in body: if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): lhs = stmt.lvalues[0] assert isinstance(lhs, NameExpr) @@ -583,8 +576,8 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init assert isinstance(stmt.rvalue, CallExpr) attrib = attribs[func_name] - # Look for default= in the call. Note: This fails if someone - # passes the _NOTHING sentinel object into attrs. + # Look for default= in the call. + # TODO: Check for attr.NOTHING attr_has_default = bool(get_argument(stmt.rvalue, attrib.default)) # If the type isn't set through annotation but it is passed through type= @@ -603,7 +596,7 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init lhs.is_inferred_def = False # If the attrib has a convert function take the type of the first argument - # for the init type. + # as the init type. convert = get_argument(stmt.rvalue, attrib.convert) if (convert and isinstance(convert, RefExpr) @@ -614,14 +607,15 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init and convert.node.type.arg_types): typ = convert.node.type.arg_types[0] - add_attribute(name, typ, attr_has_default, - get_bool_argument(stmt.rvalue, attrib.init), - stmt) - else: - if auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(stmt.rvalue, TempNode) - add_attribute(name, typ, has_rhs, True, stmt) + # Does this even have to go in init? + init = get_bool_argument(stmt.rvalue, attrib.init) + + attributes[name] = Attribute(name, typ, attr_has_default, init, stmt) + elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(stmt.rvalue, TempNode) + attributes[name] = Attribute(name, typ, has_rhs, True, stmt) + elif isinstance(stmt, Decorator): # Look for attr specific decorators. ('x.default' and 'x.validator') remove_me = [] @@ -629,6 +623,7 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init if (isinstance(func_decorator, MemberExpr) and isinstance(func_decorator.expr, NameExpr) and func_decorator.expr.name in attributes): + if func_decorator.name == 'default': # This decorator lets you set a default after the fact. attributes[func_decorator.expr.name].has_default = True @@ -638,10 +633,41 @@ def add_attribute(attr_name: str, attr_type: Optional[Type], default: bool, init # class creation time. In order to not trigger a type error later we # just remove them. This might leave us with a Decorator with no # decorators (Emperor's new clothes?) + # XXX: Subclasses might still need this, sigh. remove_me.append(func_decorator) for dec in remove_me: stmt.decorators.remove(dec) + return list(attributes.values()) + + def get_attributes(info: TypeInfo) -> List[Attribute]: + own_attrs = get_attributes_for_body(info.defn.defs.body) + super_attrs = [] + taken_attr_names = {a.name: a for a in own_attrs} + + # Traverse the MRO and collect attributes. + for super_info in info.mro[1:-1]: + sub_attrs = get_attributes(super_info) + for a in sub_attrs: + prev_a = taken_attr_names.get(a.name) + # Only add an attribute if it hasn't been defined before. This + # allows for overwriting attribute definitions by subclassing. + if prev_a is None: + super_attrs.append(a) + taken_attr_names[a.name] = a + + return super_attrs + own_attrs + + attributes = get_attributes(ctx.cls.info) + + if ctx.api.options.disallow_untyped_defs: + for attribute in attributes: + if attribute.type is None: + # This is a compromise. If you don't have a type here then the __init__ will + # be untyped. But since the __init__ method doesn't have a line number it's + # difficult to point to the correct line number. So instead we just show the + # error in the assignment, which is where you would fix the issue. + ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, attribute.context) info = ctx.cls.info self_type = fill_typevars(info) @@ -675,7 +701,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. last_default = False - for name, attribute in attributes.items(): + for attribute in attributes: if not attribute.has_default and last_default: ctx.api.fail( "Non-default attributes not allowed after default attributes.", @@ -685,7 +711,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, # __init__ gets added to the body so that it can get further semantic analysis. # e.g. Forward Reference Resolution. add_method('__init__', - [attribute.argument() for attribute in attributes.values() + [attribute.argument() for attribute in attributes if attribute.init], NoneTyp()) @@ -699,8 +725,8 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if get_bool_argument(decorator, attrs.frozen): # If the class is frozen then all the attributes need to be turned into properties. - for name in attributes: - node = ctx.cls.info.names[name].node + for attribute in attributes: + node = ctx.cls.info.names[attribute.name].node assert isinstance(node, Var) node.is_initialized_in_class = False node.is_property = True diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 7e885bbbaa61..6d732e9b3a96 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -97,6 +97,18 @@ class A: reveal_type(A) # E: Revealed type is 'def () -> __main__.A' [builtins fixtures/bool.pyi] +[case testAttrsInitAttribFalse] +from attr import attrib, attrs + +@attrs +class A: + a = attrib(init=False) + b = attrib() + +reveal_type(A) # E: Revealed type is 'def (b: Any) -> __main__.A' + +[builtins fixtures/bool.pyi] + [case testAttrsCmpFalse] from attr import attrib, attrs @attrs(auto_attribs=True, cmp=False) @@ -107,7 +119,6 @@ reveal_type(A.__eq__) # E: Revealed type is 'def (builtins.object, builtins.obj A(1) < A(2) # E: Unsupported left operand type for < ("A") [builtins fixtures/attr.pyi] - [case testAttrsInheritance] import attr @attr.s @@ -116,8 +127,8 @@ class A: @attr.s class B: b: str = attr.ib() -@attr.s # type: ignore # Incompatible base classes because of __cmp__ methods. -class C(B, A): +@attr.s +class C(A, B): c: bool = attr.ib() reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.str, c: builtins.bool) -> __main__.C' [builtins fixtures/bool.pyi] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 66cb43819d7b..8c46f9829df3 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -3,9 +3,9 @@ from typing import TypeVar, overload, Callable, Any, Type, Optional _T = TypeVar('_T') @overload -def attr(default: _T = ..., validator: Any = ..., convert: Optional[Callable[[Any], _T]] = ..., type: Optional[Callable[..., _T]] = ...) -> _T: ... +def attr(default: _T = ..., validator: Any = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., type: Optional[Callable[..., _T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: None = ..., convert: None = ..., type: None = ...) -> Any: ... +def attr(default: None = ..., validator: None = ..., init: bool = ..., convert: None = ..., type: None = ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> _T: ... From 9fb4e30ca5e6db88ec2a20533e5076495cddf41f Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 49/81] Don't traverse superclasses a 2nd time Also fixes issue of @x.default not working in subclasses. --- mypy/nodes.py | 27 +++++++++++++++++++++ mypy/plugin.py | 44 ++++++++++------------------------ test-data/unit/check-attr.test | 22 +++++++++++++++++ 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 32f44065ecf1..34f645deb8e3 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1988,6 +1988,10 @@ class is generic then it will be a type constructor of higher kind. # needed during the semantic passes.) replaced = None # type: TypeInfo + # attr modified classes need to know what their attributes are in case they are + # subclassed. + attributes = None # type: List[Attribute] + FLAGS = [ 'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple', 'is_newtype', 'is_protocol', 'runtime_protocol' @@ -2010,6 +2014,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.inferring = [] self._cache = set() self._cache_proper = set() + self.attributes = [] self.add_type_vars() def add_type_vars(self) -> None: @@ -2607,3 +2612,25 @@ def check_arg_names(names: Sequence[Optional[str]], nodes: List[T], fail: Callab fail("Duplicate argument '{}' in {}".format(name, description), node) break seen_names.add(name) + + +class Attribute: + """The value of an attr.ib() call.""" + + def __init__(self, name: str, type: 'Optional[mypy.types.Type]', + has_default: bool, init: bool, + context: Context) -> None: + self.name = name + self.type = type + self.has_default = has_default + self.init = init + self.context = context + + def argument(self) -> Argument: + """Return this attribute as an argument to __init__.""" + # Convert type not set to Any. + _type = self.type or mypy.types.AnyType(mypy.types.TypeOfAny.unannotated) + # Attrs removes leading underscores when creating the __init__ arguments. + return Argument(Var(self.name.strip("_"), _type), _type, + None, + ARG_OPT if self.has_default else ARG_POS) diff --git a/mypy/plugin.py b/mypy/plugin.py index 683427f85559..1cbfff363f00 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -10,8 +10,9 @@ from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_POS, ARG_OPT, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, Statement -) + ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, + Statement, + Attribute) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, @@ -536,27 +537,6 @@ def is_class_var(expr: NameExpr) -> bool: # Walk the class body (including the MRO) looking for the attributes. - class Attribute: - """An attribute that belongs to this class.""" - - def __init__(self, name: str, type: Optional[Type], - has_default: bool, init: bool, - context: Context) -> None: - self.name = name - self.type = type - self.has_default = has_default - self.init = init - self.context = context - - def argument(self) -> Argument: - """Return this attribute as an argument to __init__.""" - # Convert type not set to Any. - _type = self.type or AnyType(TypeOfAny.unannotated) - # Attrs removes leading underscores when creating the __init__ arguments. - return Argument(Var(self.name.strip("_"), _type), _type, - None, - ARG_OPT if self.has_default else ARG_POS) - # auto_attribs means we generate attributes from annotated variables. auto_attribs = get_bool_argument(decorator, attrs.auto_attribs) @@ -633,7 +613,6 @@ def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: # class creation time. In order to not trigger a type error later we # just remove them. This might leave us with a Decorator with no # decorators (Emperor's new clothes?) - # XXX: Subclasses might still need this, sigh. remove_me.append(func_decorator) for dec in remove_me: @@ -647,8 +626,7 @@ def get_attributes(info: TypeInfo) -> List[Attribute]: # Traverse the MRO and collect attributes. for super_info in info.mro[1:-1]: - sub_attrs = get_attributes(super_info) - for a in sub_attrs: + for a in super_info.attributes: prev_a = taken_attr_names.get(a.name) # Only add an attribute if it hasn't been defined before. This # allows for overwriting attribute definitions by subclassing. @@ -659,6 +637,7 @@ def get_attributes(info: TypeInfo) -> List[Attribute]: return super_attrs + own_attrs attributes = get_attributes(ctx.cls.info) + ctx.cls.info.attributes = attributes if ctx.api.options.disallow_untyped_defs: for attribute in attributes: @@ -693,6 +672,8 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, func._fullname = info.fullname() + '.' + method_name func.line = ctx.cls.line info.names[method_name] = SymbolTableNode(MDEF, func) + # Add the created methods to the body so that they can get further semantic analysis. + # e.g. Forward Reference Resolution. info.defn.defs.body.append(func) if get_bool_argument(decorator, attrs.init): @@ -708,12 +689,11 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, attribute.context) last_default = attribute.has_default - # __init__ gets added to the body so that it can get further semantic analysis. - # e.g. Forward Reference Resolution. - add_method('__init__', - [attribute.argument() for attribute in attributes - if attribute.init], - NoneTyp()) + add_method( + '__init__', + [attribute.argument() for attribute in attributes if attribute.init], + NoneTyp() + ) for stmt in ctx.cls.defs.body: # The implicit first type of cls methods will be wrong because it's based on diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 6d732e9b3a96..0b19ca9555f7 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -392,3 +392,25 @@ B() == int C() == int D() == int [builtins fixtures/list.pyi] + +[case testAttrsComplexSuperclass] +import attr + +import attr +@attr.s +class C(object): + x: int = attr.ib(default=1) + y: int = attr.ib() + @y.default + def name_does_not_matter(self): + return self.x + 1 + + +@attr.s +class A(C): + z: int = attr.ib(default=18) + +reveal_type(C) # E: Revealed type is 'def (x: builtins.int =, y: builtins.int =) -> __main__.C' +reveal_type(A) # E: Revealed type is 'def (x: builtins.int =, y: builtins.int =, z: builtins.int =) -> __main__.A' + +[builtins fixtures/list.pyi] From 256b446c9f88857d42285168d501946d5bfde0ea Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 50/81] Support converter --- mypy/plugin.py | 28 +++++++++++++++++++--------- test-data/unit/check-attr.test | 28 ++++++++++++++++++++++++++++ test-data/unit/lib-stub/attr.pyi | 4 ++-- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 1cbfff363f00..b0be239913ea 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -433,6 +433,7 @@ def int_pow_callback(ctx: MethodContext) -> Type: ('init', ArgumentInfo), ('type', ArgumentInfo), ('convert', ArgumentInfo), + ('converter', ArgumentInfo), ] ) @@ -447,6 +448,7 @@ def int_pow_callback(ctx: MethodContext) -> Type: init=ArgumentInfo(True, 'init', 5), convert=ArgumentInfo(None, 'convert', 6), type=ArgumentInfo(None, 'type', 8), + converter=ArgumentInfo(None, 'converter', 9), ) @@ -575,17 +577,25 @@ def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: lhs.node.type = typ lhs.is_inferred_def = False - # If the attrib has a convert function take the type of the first argument + # If the attrib has a converter function take the type of the first argument # as the init type. + converter = get_argument(stmt.rvalue, attrib.converter) convert = get_argument(stmt.rvalue, attrib.convert) - if (convert - and isinstance(convert, RefExpr) - and convert.node - and isinstance(convert.node, FuncBase) - and convert.node.type - and isinstance(convert.node.type, CallableType) - and convert.node.type.arg_types): - typ = convert.node.type.arg_types[0] + + if convert and converter: + ctx.api.fail("Can't pass both `convert` and `converter`.", stmt.rvalue) + elif convert: + # Note: Convert is deprecated but works the same. + converter = convert + + if (converter + and isinstance(converter, RefExpr) + and converter.node + and isinstance(converter.node, FuncBase) + and converter.node.type + and isinstance(converter.node.type, CallableType) + and converter.node.type.arg_types): + typ = converter.node.type.arg_types[0] # Does this even have to go in init? init = get_bool_argument(stmt.rvalue, attrib.init) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 0b19ca9555f7..33af8e122359 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -347,6 +347,34 @@ reveal_type(C) # E: Revealed type is 'def (x: builtins.int) -> __main__.C' reveal_type(C(15).x) # E: Revealed type is 'builtins.str' [builtins fixtures/list.pyi] +[case testAttrsUsingConverter] +import attr + +def converter(s:int) -> str: + return 'hello' + +@attr.s +class C: + x: str = attr.ib(converter=converter) + +# Because of the converter the __init__ takes an int, but the variable is a str. +reveal_type(C) # E: Revealed type is 'def (x: builtins.int) -> __main__.C' +reveal_type(C(15).x) # E: Revealed type is 'builtins.str' +[builtins fixtures/list.pyi] + +[case testAttrsUsingConvertAndConverter] +import attr + +def converter(s:int) -> str: + return 'hello' + +@attr.s +class C: + x: str = attr.ib(converter=converter, convert=converter) # E: Can't pass both `convert` and `converter`. + +[builtins fixtures/list.pyi] + + [case testAttrsCmpWithSubclasses] import attr @attr.s diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 8c46f9829df3..6c8240ec7d5f 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -3,9 +3,9 @@ from typing import TypeVar, overload, Callable, Any, Type, Optional _T = TypeVar('_T') @overload -def attr(default: _T = ..., validator: Any = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., type: Optional[Callable[..., _T]] = ...) -> _T: ... +def attr(default: _T = ..., validator: Any = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., type: Optional[Callable[..., _T]] = ..., converter: Optional[Callable[[Any], _T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: None = ..., init: bool = ..., convert: None = ..., type: None = ...) -> Any: ... +def attr(default: None = ..., validator: None = ..., init: bool = ..., convert: None = ..., type: None = ..., converter: Optional[Callable[[Any], _T]] = ...) -> Any: ... @overload def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> _T: ... From d5668065a7386e295121156c7e6c439532d1e9be Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 51/81] Remove LastUpdatedOrderedDict --- mypy/plugin.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index b0be239913ea..bfe0ae79a94c 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -467,14 +467,6 @@ def int_pow_callback(ctx: MethodContext) -> Type: } -class LastUpdatedOrderedDict(OrderedDict): - """Store items in the order the keys were last added.""" - def __setitem__(self, key, value): - if key in self: - del self[key] - OrderedDict.__setitem__(self, key, value) - - def attr_class_maker_callback( attrs: AttrClassMaker, ctx: ClassDefContext, @@ -543,7 +535,7 @@ def is_class_var(expr: NameExpr) -> bool: auto_attribs = get_bool_argument(decorator, attrs.auto_attribs) def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: - attributes = LastUpdatedOrderedDict() # type: OrderedDict[str, Attribute] + attributes = OrderedDict() # type: OrderedDict[str, Attribute] for stmt in body: if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): @@ -600,10 +592,14 @@ def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: # Does this even have to go in init? init = get_bool_argument(stmt.rvalue, attrib.init) + if name in attributes: + del attributes[name] attributes[name] = Attribute(name, typ, attr_has_default, init, stmt) elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(stmt.rvalue, TempNode) + if name in attributes: + del attributes[name] attributes[name] = Attribute(name, typ, has_rhs, True, stmt) elif isinstance(stmt, Decorator): From 5b7b29021effb131e206445b80eb90823fca8e5c Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 52/81] Move Attribute list into plugin, to avoid touching TypeInfo --- mypy/nodes.py | 27 ------------------------ mypy/plugin.py | 56 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 34f645deb8e3..32f44065ecf1 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1988,10 +1988,6 @@ class is generic then it will be a type constructor of higher kind. # needed during the semantic passes.) replaced = None # type: TypeInfo - # attr modified classes need to know what their attributes are in case they are - # subclassed. - attributes = None # type: List[Attribute] - FLAGS = [ 'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple', 'is_newtype', 'is_protocol', 'runtime_protocol' @@ -2014,7 +2010,6 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self.inferring = [] self._cache = set() self._cache_proper = set() - self.attributes = [] self.add_type_vars() def add_type_vars(self) -> None: @@ -2612,25 +2607,3 @@ def check_arg_names(names: Sequence[Optional[str]], nodes: List[T], fail: Callab fail("Duplicate argument '{}' in {}".format(name, description), node) break seen_names.add(name) - - -class Attribute: - """The value of an attr.ib() call.""" - - def __init__(self, name: str, type: 'Optional[mypy.types.Type]', - has_default: bool, init: bool, - context: Context) -> None: - self.name = name - self.type = type - self.has_default = has_default - self.init = init - self.context = context - - def argument(self) -> Argument: - """Return this attribute as an argument to __init__.""" - # Convert type not set to Any. - _type = self.type or mypy.types.AnyType(mypy.types.TypeOfAny.unannotated) - # Attrs removes leading underscores when creating the __init__ arguments. - return Argument(Var(self.name.strip("_"), _type), _type, - None, - ARG_OPT if self.has_default else ARG_POS) diff --git a/mypy/plugin.py b/mypy/plugin.py index bfe0ae79a94c..bd6016f13541 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -10,9 +10,9 @@ from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, - Statement, - Attribute) + ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, + Statement +) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, @@ -263,6 +263,10 @@ def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: class DefaultPlugin(Plugin): """Type checker plugin that is enabled by default.""" + def __init__(self, options: Options) -> None: + super().__init__(options) + self._attr_classes = {} # type: Dict[TypeInfo, List[Attribute]] + def get_function_hook(self, fullname: str ) -> Optional[Callable[[FunctionContext], Type]]: if fullname == 'contextlib.contextmanager': @@ -288,7 +292,11 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: if fullname in attr_class_makers: - return partial(attr_class_maker_callback, attr_class_makers[fullname]) + return partial( + attr_class_maker_callback, + attr_class_makers[fullname], + self._attr_classes + ) return None @@ -467,8 +475,31 @@ def int_pow_callback(ctx: MethodContext) -> Type: } +class Attribute: + """The value of an attr.ib() call.""" + + def __init__(self, name: str, type: Optional[Type], + has_default: bool, init: bool, + context: Context) -> None: + self.name = name + self.type = type + self.has_default = has_default + self.init = init + self.context = context + + def argument(self) -> Argument: + """Return this attribute as an argument to __init__.""" + # Convert type not set to Any. + _type = self.type or AnyType(TypeOfAny.unannotated) + # Attrs removes leading underscores when creating the __init__ arguments. + return Argument(Var(self.name.strip("_"), _type), _type, + None, + ARG_OPT if self.has_default else ARG_POS) + + def attr_class_maker_callback( attrs: AttrClassMaker, + attr_classes: Dict[TypeInfo, List[Attribute]], ctx: ClassDefContext, attribs: Dict[str, AttribMaker] = attr_attrib_makers ) -> None: @@ -632,18 +663,19 @@ def get_attributes(info: TypeInfo) -> List[Attribute]: # Traverse the MRO and collect attributes. for super_info in info.mro[1:-1]: - for a in super_info.attributes: - prev_a = taken_attr_names.get(a.name) - # Only add an attribute if it hasn't been defined before. This - # allows for overwriting attribute definitions by subclassing. - if prev_a is None: - super_attrs.append(a) - taken_attr_names[a.name] = a + if super_info in attr_classes: + for a in attr_classes[super_info]: + prev_a = taken_attr_names.get(a.name) + # Only add an attribute if it hasn't been defined before. This + # allows for overwriting attribute definitions by subclassing. + if prev_a is None: + super_attrs.append(a) + taken_attr_names[a.name] = a return super_attrs + own_attrs attributes = get_attributes(ctx.cls.info) - ctx.cls.info.attributes = attributes + attr_classes[ctx.cls.info] = attributes if ctx.api.options.disallow_untyped_defs: for attribute in attributes: From b2f075e2c3c0870e99afa90f8a7458d277763e41 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 53/81] Get the argument ordering from the stub instead of hardcoded --- mypy/plugin.py | 140 +++++++++++++------------------ test-data/unit/lib-stub/attr.pyi | 9 +- 2 files changed, 65 insertions(+), 84 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index bd6016f13541..775af3723891 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -16,7 +16,7 @@ from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, - AnyType, TypeList, UnboundType, TypeOfAny, TypeVarDef + AnyType, TypeList, UnboundType, TypeOfAny, TypeVarDef, Overloaded ) from mypy.messages import MessageBuilder from mypy.options import Options @@ -294,9 +294,14 @@ def get_class_decorator_hook(self, fullname: str if fullname in attr_class_makers: return partial( attr_class_maker_callback, - attr_class_makers[fullname], self._attr_classes ) + elif fullname in attr_dataclass_makers: + return partial( + attr_class_maker_callback, + self._attr_classes, + auto_attribs_default=True + ) return None @@ -418,60 +423,19 @@ def int_pow_callback(ctx: MethodContext) -> Type: return ctx.default_return_type -# Arguments to the attr functions (attr.s and attr.ib) with their defaults. -ArgumentInfo = NamedTuple( - 'ArgumentInfo', [ - ('default', Optional[bool]), - ('kwarg_name', Optional[str]), - ('index', Optional[int]), - ]) - -AttrClassMaker = NamedTuple( - 'AttrClassMaker', [ - ('init', ArgumentInfo), - ('frozen', ArgumentInfo), - ('cmp', ArgumentInfo), - ('auto_attribs', ArgumentInfo) - ] -) - -AttribMaker = NamedTuple( - 'AttribMaker', [ - ('default', ArgumentInfo), - ('init', ArgumentInfo), - ('type', ArgumentInfo), - ('convert', ArgumentInfo), - ('converter', ArgumentInfo), - ] -) - -attrs_arguments = AttrClassMaker( - cmp=ArgumentInfo(True, 'cmp', 4), - init=ArgumentInfo(True, 'init', 6), - frozen=ArgumentInfo(False, 'frozen', 8), - auto_attribs=ArgumentInfo(False, 'auto_attribs', 10) -) -attrib_arguments = AttribMaker( - default=ArgumentInfo(None, 'default', 0), - init=ArgumentInfo(True, 'init', 5), - convert=ArgumentInfo(None, 'convert', 6), - type=ArgumentInfo(None, 'type', 8), - converter=ArgumentInfo(None, 'converter', 9), -) - - # The names of the different functions that create classes or arguments. attr_class_makers = { - 'attr.s': attrs_arguments, - 'attr.attrs': attrs_arguments, - 'attr.attributes': attrs_arguments, - 'attr.dataclass': attrs_arguments._replace( - auto_attribs=ArgumentInfo(True, 'auto_attribs', 10)), + 'attr.s', + 'attr.attrs', + 'attr.attributes', +} +attr_dataclass_makers = { + 'attr.dataclass', } attr_attrib_makers = { - 'attr.ib': attrib_arguments, - 'attr.attrib': attrib_arguments, - 'attr.attr': attrib_arguments, + 'attr.ib', + 'attr.attrib', + 'attr.attr', } @@ -498,10 +462,9 @@ def argument(self) -> Argument: def attr_class_maker_callback( - attrs: AttrClassMaker, attr_classes: Dict[TypeInfo, List[Attribute]], ctx: ClassDefContext, - attribs: Dict[str, AttribMaker] = attr_attrib_makers + auto_attribs_default:bool = False ) -> None: """Add necessary dunder methods to classes decorated with attr.s. @@ -522,35 +485,56 @@ def called_function(expr: Expression) -> Optional[str]: return expr.callee.fullname return None - def get_argument(call: CallExpr, arg: ArgumentInfo) -> Optional[Expression]: + def get_callable_type(call: CallExpr) -> Optional[CallableType]: + """Get the CallableType that's attached to a CallExpr.""" + callable_type = None + if (isinstance(call, CallExpr) + and isinstance(call.callee, RefExpr) + and isinstance(call.callee.node, Var) + and call.callee.node.type): + callee_type = call.callee.node.type + if isinstance(callee_type, Overloaded): + # We take the last overload. + callable_type = callee_type.items()[-1] + elif isinstance(callee_type, CallableType): + callable_type = callee_type + return callable_type + + def get_argument(call: CallExpr, name: str) -> Optional[Expression]: """Return the expression for the specific argument.""" - if not arg.kwarg_name and not arg.index: + callee = get_callable_type(call) + if not callee: + return None + + argument = callee.argument_by_name(name) + if not argument: + return None + + if not argument.name and not argument.pos: return None for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): - if arg.index is not None and not attr_name and i == arg.index: + if argument.pos is not None and not attr_name and i == argument.pos: return attr_value - if attr_name == arg.kwarg_name: + if attr_name == argument.name: return attr_value return None - def get_bool_argument(expr: Expression, arg: ArgumentInfo) -> bool: + def get_bool_argument(expr: Expression, name: str, default: bool) -> bool: """Return the value of an argument name in the give Expression. If it's a CallExpr and the argument is one of the args then return it. Otherwise return the default value for the argument. """ - assert arg.default is not None, "{}: default should be True or False".format(arg) if isinstance(expr, CallExpr): - attr_value = get_argument(expr, arg) + 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( - arg.kwarg_name), expr) - return arg.default + ctx.api.fail('"{}" argument must be True or False.'.format(name), expr) + return default return ret - return arg.default + return default def is_class_var(expr: NameExpr) -> bool: """Return whether the expression is ClassVar[...]""" @@ -560,10 +544,8 @@ def is_class_var(expr: NameExpr) -> bool: decorator = ctx.reason - # Walk the class body (including the MRO) looking for the attributes. - - # auto_attribs means we generate attributes from annotated variables. - auto_attribs = get_bool_argument(decorator, attrs.auto_attribs) + # auto_attribs means we also generate Attributes from annotated variables. + auto_attribs = get_bool_argument(decorator, 'auto_attribs', auto_attribs_default) def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: attributes = OrderedDict() # type: OrderedDict[str, Attribute] @@ -577,17 +559,15 @@ def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: func_name = called_function(stmt.rvalue) - if func_name in attribs: + if func_name in attr_attrib_makers: assert isinstance(stmt.rvalue, CallExpr) - attrib = attribs[func_name] - # Look for default= in the call. # TODO: Check for attr.NOTHING - attr_has_default = bool(get_argument(stmt.rvalue, attrib.default)) + attr_has_default = bool(get_argument(stmt.rvalue, 'default')) # If the type isn't set through annotation but it is passed through type= # use that. - type_arg = get_argument(stmt.rvalue, attrib.type) + type_arg = get_argument(stmt.rvalue, 'type') if type_arg and not typ: try: un_type = expr_to_unanalyzed_type(type_arg) @@ -602,8 +582,8 @@ def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: # If the attrib has a converter function take the type of the first argument # as the init type. - converter = get_argument(stmt.rvalue, attrib.converter) - convert = get_argument(stmt.rvalue, attrib.convert) + converter = get_argument(stmt.rvalue, 'converter') + convert = get_argument(stmt.rvalue, 'convert') if convert and converter: ctx.api.fail("Can't pass both `convert` and `converter`.", stmt.rvalue) @@ -621,7 +601,7 @@ def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: typ = converter.node.type.arg_types[0] # Does this even have to go in init? - init = get_bool_argument(stmt.rvalue, attrib.init) + init = get_bool_argument(stmt.rvalue, 'init', True) if name in attributes: del attributes[name] @@ -714,7 +694,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, # e.g. Forward Reference Resolution. info.defn.defs.body.append(func) - if get_bool_argument(decorator, attrs.init): + if get_bool_argument(decorator, 'init', True): # Generate the __init__ method. # Check the init args for correct default-ness. Note: This has to be done after all the @@ -741,7 +721,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if isinstance(func_type, CallableType): func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info) - if get_bool_argument(decorator, attrs.frozen): + if get_bool_argument(decorator, 'frozen', False): # If the class is frozen then all the attributes need to be turned into properties. for attribute in attributes: node = ctx.cls.info.names[attribute.name].node @@ -749,7 +729,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, node.is_initialized_in_class = False node.is_property = True - if get_bool_argument(decorator, attrs.cmp): + if get_bool_argument(decorator, 'cmp', True): # Generate cmp methods that look like this: # def __ne__(self, other: '') -> bool: ... # We use fullname to handle nested classes, splitting to remove the module name. diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 6c8240ec7d5f..0ab1648cd1ba 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -1,16 +1,17 @@ from typing import TypeVar, overload, Callable, Any, Type, Optional _T = TypeVar('_T') +_C = TypeVar('_C', bound=type) @overload -def attr(default: _T = ..., validator: Any = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., type: Optional[Callable[..., _T]] = ..., converter: Optional[Callable[[Any], _T]] = ...) -> _T: ... +def attr(default: Optional[_T] = ..., validator: Optional[Any] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., metadata: Any = ..., type: Optional[Type[_T]] = ..., converter: Optional[Callable[[Any], _T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: None = ..., init: bool = ..., convert: None = ..., type: None = ..., converter: Optional[Callable[[Any], _T]] = ...) -> Any: ... +def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., metadata: Any = ..., type: None = ..., converter: None = ...) -> Any: ... @overload -def attributes(maybe_cls: _T = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> _T: ... +def attributes(maybe_cls: _C, these: Optional[Any] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... @overload -def attributes(maybe_cls: None = ..., cmp: bool = ..., init: bool = ..., frozen: bool = ...) -> Callable[[_T], _T]: ... +def attributes(maybe_cls: None = ..., these: Optional[Any] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... # aliases s = attrs = attributes From f6f2f41f321f26b122aa9436b6e4366da8e773cd Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 10 Jan 2018 22:52:59 -0800 Subject: [PATCH 54/81] De-flake --- mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 775af3723891..c8f356ff87d2 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -464,7 +464,7 @@ def argument(self) -> Argument: def attr_class_maker_callback( attr_classes: Dict[TypeInfo, List[Attribute]], ctx: ClassDefContext, - auto_attribs_default:bool = False + auto_attribs_default: bool = False ) -> None: """Add necessary dunder methods to classes decorated with attr.s. From 6f60854a989b403c307433285e274c062a815e29 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 17 Jan 2018 09:46:52 -0800 Subject: [PATCH 55/81] Some CR --- mypy/plugin.py | 263 ++++++++++++++++--------------- test-data/unit/check-attr.test | 22 ++- test-data/unit/lib-stub/attr.pyi | 46 +++++- 3 files changed, 198 insertions(+), 133 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index c8f356ff87d2..519db618aab0 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -11,8 +11,7 @@ Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, - Statement -) + TupleExpr, ListExpr) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, @@ -456,7 +455,7 @@ def argument(self) -> Argument: # Convert type not set to Any. _type = self.type or AnyType(TypeOfAny.unannotated) # Attrs removes leading underscores when creating the __init__ arguments. - return Argument(Var(self.name.strip("_"), _type), _type, + return Argument(Var(self.name.lstrip("_"), _type), _type, None, ARG_OPT if self.has_default else ARG_POS) @@ -468,16 +467,15 @@ def attr_class_maker_callback( ) -> None: """Add necessary dunder methods to classes decorated with attr.s. - Currently supports init=, cmp= and frozen= and auto_attribs=. + attrs is a package that lets you define classes without writing dull boilerplate code. + + At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or + annotated variables if auto_attribs=True), then depending on how the decorator is called, + it will add an __init__ or all the __cmp__ methods. For frozen=True it will turn the attrs + into properties. + + See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. """ - # attrs is a package that lets you define classes without writing dull boilerplate code. - # - # At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or - # annotated variables if auto_attribs=True), then depending on how the decorator is called, - # it will add an __init__ or all the __cmp__ methods. For frozen=True it will turn the attrs - # into properties. - # - # See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. def called_function(expr: Expression) -> Optional[str]: """Return the full name of the function being called by the expr, or None.""" @@ -488,8 +486,7 @@ def called_function(expr: Expression) -> Optional[str]: def get_callable_type(call: CallExpr) -> Optional[CallableType]: """Get the CallableType that's attached to a CallExpr.""" callable_type = None - if (isinstance(call, CallExpr) - and isinstance(call.callee, RefExpr) + if (isinstance(call.callee, RefExpr) and isinstance(call.callee.node, Var) and call.callee.node.type): callee_type = call.callee.node.type @@ -521,7 +518,7 @@ def get_argument(call: CallExpr, name: str) -> Optional[Expression]: return None def get_bool_argument(expr: Expression, name: str, default: bool) -> bool: - """Return the value of an argument name in the give Expression. + """Return the value of an argument name in the given Expression. If it's a CallExpr and the argument is one of the args then return it. Otherwise return the default value for the argument. @@ -547,114 +544,134 @@ def is_class_var(expr: NameExpr) -> bool: # auto_attribs means we also generate Attributes from annotated variables. auto_attribs = get_bool_argument(decorator, 'auto_attribs', auto_attribs_default) - def get_attributes_for_body(body: List[Statement]) -> List[Attribute]: - attributes = OrderedDict() # type: OrderedDict[str, Attribute] - - for stmt in body: - if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): - lhs = stmt.lvalues[0] - assert isinstance(lhs, NameExpr) - name = lhs.name - typ = stmt.type - - func_name = called_function(stmt.rvalue) - - if func_name in attr_attrib_makers: - assert isinstance(stmt.rvalue, CallExpr) - # Look for default= in the call. - # TODO: Check for attr.NOTHING - attr_has_default = bool(get_argument(stmt.rvalue, 'default')) - - # If the type isn't set through annotation but it is passed through type= - # use that. - type_arg = get_argument(stmt.rvalue, 'type') - if type_arg and not typ: - try: - un_type = expr_to_unanalyzed_type(type_arg) - except TypeTranslationError: - ctx.api.fail('Invalid argument to type', type_arg) - else: - typ = ctx.api.anal_type(un_type) - if typ and isinstance(lhs.node, Var) and not lhs.node.type: - # If there is no annotation, add one. - lhs.node.type = typ - lhs.is_inferred_def = False - - # If the attrib has a converter function take the type of the first argument - # as the init type. - converter = get_argument(stmt.rvalue, 'converter') - convert = get_argument(stmt.rvalue, 'convert') - - if convert and converter: - ctx.api.fail("Can't pass both `convert` and `converter`.", stmt.rvalue) - elif convert: - # Note: Convert is deprecated but works the same. - converter = convert - - if (converter - and isinstance(converter, RefExpr) - and converter.node - and isinstance(converter.node, FuncBase) - and converter.node.type - and isinstance(converter.node.type, CallableType) - and converter.node.type.arg_types): - typ = converter.node.type.arg_types[0] - - # Does this even have to go in init? - init = get_bool_argument(stmt.rvalue, 'init', True) - - if name in attributes: - del attributes[name] - attributes[name] = Attribute(name, typ, attr_has_default, init, stmt) - elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(stmt.rvalue, TempNode) - if name in attributes: - del attributes[name] - attributes[name] = Attribute(name, typ, has_rhs, True, stmt) - - elif isinstance(stmt, Decorator): - # Look for attr specific decorators. ('x.default' and 'x.validator') - remove_me = [] - for func_decorator in stmt.decorators: - if (isinstance(func_decorator, MemberExpr) - and isinstance(func_decorator.expr, NameExpr) - and func_decorator.expr.name in attributes): - - if func_decorator.name == 'default': - # This decorator lets you set a default after the fact. - attributes[func_decorator.expr.name].has_default = True - - if func_decorator.name in ('default', 'validator'): - # These are decorators on the attrib object that only exist during - # class creation time. In order to not trigger a type error later we - # just remove them. This might leave us with a Decorator with no - # decorators (Emperor's new clothes?) - remove_me.append(func_decorator) - - for dec in remove_me: - stmt.decorators.remove(dec) - return list(attributes.values()) - - def get_attributes(info: TypeInfo) -> List[Attribute]: - own_attrs = get_attributes_for_body(info.defn.defs.body) - super_attrs = [] - taken_attr_names = {a.name: a for a in own_attrs} - - # Traverse the MRO and collect attributes. - for super_info in info.mro[1:-1]: - if super_info in attr_classes: - for a in attr_classes[super_info]: - prev_a = taken_attr_names.get(a.name) - # Only add an attribute if it hasn't been defined before. This - # allows for overwriting attribute definitions by subclassing. - if prev_a is None: - super_attrs.append(a) - taken_attr_names[a.name] = a - - return super_attrs + own_attrs - - attributes = get_attributes(ctx.cls.info) + # First, walk the body looking for attribute definitions. + # They will look like this: + # x = attr.ib() + # x = y = attr.ib() + # x, y = attr.ib(), attr.ib() + # or if auto_attribs is enabled also like this: + # x: type + # x: type = default_value + own_attrs = OrderedDict() # type: OrderedDict[str, Attribute] + for stmt in ctx.cls.info.defn.defs.body: + if isinstance(stmt, AssignmentStmt): + for lvalue in stmt.lvalues: + lhss = [] # type: List[NameExpr] + rvalues = [] # type: List[Expression] + # To handle all types of assignments we just convert everything + # to a matching lists of lefts and rights. + if isinstance(lvalue, (TupleExpr, ListExpr)): + if all(isinstance(item, NameExpr) for item in lvalue.items): + lhss = cast(List[NameExpr], lvalue.items) + if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): + rvalues = stmt.rvalue.items + elif isinstance(lvalue, NameExpr): + lhss = [lvalue] + rvalues = [stmt.rvalue] + + if len(lhss) != len(rvalues): + # This means we have some assignment that isn't 1 to 1. + # It can't be an attrib. + continue + + for lhs, rvalue in zip(lhss, rvalues): + typ = stmt.type + name = lhs.name + func_name = called_function(rvalue) + + if func_name in attr_attrib_makers: + assert isinstance(rvalue, CallExpr) + # Look for default= in the call. + # TODO: Check for attr.NOTHING + attr_has_default = bool(get_argument(rvalue, 'default')) + + # If the type isn't set through annotation but it is passed through type= + # use that. + type_arg = get_argument(rvalue, 'type') + if type_arg and not typ: + try: + un_type = expr_to_unanalyzed_type(type_arg) + except TypeTranslationError: + ctx.api.fail('Invalid argument to type', type_arg) + else: + typ = ctx.api.anal_type(un_type) + if typ and isinstance(lhs.node, Var) and not lhs.node.type: + # If there is no annotation, add one. + lhs.node.type = typ + lhs.is_inferred_def = False + + # If the attrib has a converter function take the type of the first + # argument as the init type. + # Note: convert is deprecated but works the same as converter. + converter = get_argument(rvalue, 'converter') + convert = get_argument(rvalue, 'convert') + if convert and converter: + ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) + elif convert: + converter = convert + if (converter + and isinstance(converter, RefExpr) + and converter.node + and isinstance(converter.node, FuncBase) + and converter.node.type + and isinstance(converter.node.type, CallableType) + and converter.node.type.arg_types): + typ = converter.node.type.arg_types[0] + + # Does this even have to go in init. + init = get_bool_argument(rvalue, 'init', True) + + # When attrs are defined twice in the same body we want to use + # the 2nd definition in the 2nd location. So remove it from the + # OrderedDict + if name in own_attrs: + del own_attrs[name] + own_attrs[name] = Attribute(name, typ, attr_has_default, init, stmt) + elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(rvalue, TempNode) + if name in own_attrs: + del own_attrs[name] + own_attrs[name] = Attribute(name, typ, has_rhs, True, stmt) + + elif isinstance(stmt, Decorator): + # Look for attr specific decorators. ('x.default' and 'x.validator') + remove_me = [] + for func_decorator in stmt.decorators: + if (isinstance(func_decorator, MemberExpr) + and isinstance(func_decorator.expr, NameExpr) + and func_decorator.expr.name in own_attrs): + + if func_decorator.name == 'default': + # This decorator lets you set a default after the fact. + own_attrs[func_decorator.expr.name].has_default = True + + if func_decorator.name in ('default', 'validator'): + # These are decorators on the attrib object that only exist during + # class creation time. In order to not trigger a type error later we + # just remove them. This might leave us with a Decorator with no + # decorators (Emperor's new clothes?) + remove_me.append(func_decorator) + + for dec in remove_me: + stmt.decorators.remove(dec) + + taken_attr_names = set(own_attrs) + super_attrs = [] + + # Traverse the MRO and collect attributes. + for super_info in ctx.cls.info.mro[1:-1]: + if super_info in attr_classes: + for a in attr_classes[super_info]: + # Only add an attribute if it hasn't been defined before. This + # allows for overwriting attribute definitions by subclassing. + if a.name not in taken_attr_names: + super_attrs.append(a) + taken_attr_names.add(a.name) + + attributes = super_attrs + list(own_attrs.values()) + # Save the attributes so that subclasses can reuse them. + # TODO: Fix caching. attr_classes[ctx.cls.info] = attributes if ctx.api.options.disallow_untyped_defs: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 33af8e122359..b8c41278aad4 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -422,23 +422,33 @@ D() == int [builtins fixtures/list.pyi] [case testAttrsComplexSuperclass] -import attr - import attr @attr.s -class C(object): +class C: x: int = attr.ib(default=1) y: int = attr.ib() @y.default def name_does_not_matter(self): return self.x + 1 - - @attr.s class A(C): z: int = attr.ib(default=18) - reveal_type(C) # E: Revealed type is 'def (x: builtins.int =, y: builtins.int =) -> __main__.C' reveal_type(A) # E: Revealed type is 'def (x: builtins.int =, y: builtins.int =, z: builtins.int =) -> __main__.A' +[builtins fixtures/list.pyi] +[case testAttrsMultiAssign] +import attr +@attr.s +class A: + x, y, z = attr.ib(), attr.ib(type=int), attr.ib(default=17) +reveal_type(A) # E: Revealed type is 'def (x: Any, y: builtins.int, z: Any =) -> __main__.A' +[builtins fixtures/list.pyi] + +[case testAttrsMultiAssign2] +import attr +@attr.s +class A: + x = y = z = attr.ib() +reveal_type(A) # E: Revealed type is 'def (x: Any, y: Any, z: Any) -> __main__.A' [builtins fixtures/list.pyi] diff --git a/test-data/unit/lib-stub/attr.pyi b/test-data/unit/lib-stub/attr.pyi index 0ab1648cd1ba..d62a99a685eb 100644 --- a/test-data/unit/lib-stub/attr.pyi +++ b/test-data/unit/lib-stub/attr.pyi @@ -4,14 +4,52 @@ _T = TypeVar('_T') _C = TypeVar('_C', bound=type) @overload -def attr(default: Optional[_T] = ..., validator: Optional[Any] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., metadata: Any = ..., type: Optional[Type[_T]] = ..., converter: Optional[Callable[[Any], _T]] = ...) -> _T: ... +def attr(default: Optional[_T] = ..., + validator: Optional[Any] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[Callable[[Any], _T]] = ..., + metadata: Any = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[Callable[[Any], _T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[Callable[[Any], _T]] = ..., metadata: Any = ..., type: None = ..., converter: None = ...) -> Any: ... +def attr(default: None = ..., + validator: None = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[Callable[[Any], _T]] = ..., + metadata: Any = ..., + type: None = ..., + converter: None = ...) -> Any: ... @overload -def attributes(maybe_cls: _C, these: Optional[Any] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... +def attributes(maybe_cls: _C, + these: Optional[Any] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> _C: ... @overload -def attributes(maybe_cls: None = ..., these: Optional[Any] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... +def attributes(maybe_cls: None = ..., + these: Optional[Any] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> Callable[[_C], _C]: ... # aliases s = attrs = attributes From 7492278b326e9eb57c469ec39a5446b7f8c37547 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 18 Jan 2018 08:48:26 -0800 Subject: [PATCH 56/81] Get rid of called_function helper --- mypy/plugin.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 519db618aab0..890cfc26aa78 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -477,12 +477,6 @@ def attr_class_maker_callback( See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. """ - def called_function(expr: Expression) -> Optional[str]: - """Return the full name of the function being called by the expr, or None.""" - if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): - return expr.callee.fullname - return None - def get_callable_type(call: CallExpr) -> Optional[CallableType]: """Get the CallableType that's attached to a CallExpr.""" callable_type = None @@ -556,10 +550,10 @@ def is_class_var(expr: NameExpr) -> bool: for stmt in ctx.cls.info.defn.defs.body: if isinstance(stmt, AssignmentStmt): for lvalue in stmt.lvalues: - lhss = [] # type: List[NameExpr] - rvalues = [] # type: List[Expression] # To handle all types of assignments we just convert everything # to a matching lists of lefts and rights. + lhss = [] # type: List[NameExpr] + rvalues = [] # type: List[Expression] if isinstance(lvalue, (TupleExpr, ListExpr)): if all(isinstance(item, NameExpr) for item in lvalue.items): lhss = cast(List[NameExpr], lvalue.items) @@ -577,10 +571,11 @@ def is_class_var(expr: NameExpr) -> bool: for lhs, rvalue in zip(lhss, rvalues): typ = stmt.type name = lhs.name - func_name = called_function(rvalue) - if func_name in attr_attrib_makers: - assert isinstance(rvalue, CallExpr) + # Check if the right hand side is a call to an attribute maker. + if (isinstance(rvalue, CallExpr) + and isinstance(rvalue.callee, RefExpr) + and rvalue.callee.fullname in attr_attrib_makers): # Look for default= in the call. # TODO: Check for attr.NOTHING attr_has_default = bool(get_argument(rvalue, 'default')) From 28c7b8d2baebacdb82e5454543019af40349116c Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 18 Jan 2018 09:53:03 -0800 Subject: [PATCH 57/81] Add some more tests --- mypy/plugin.py | 4 ++++ test-data/unit/check-attr.test | 22 ++++++++++++++++++++++ test-data/unit/fixtures/exception.pyi | 1 + 3 files changed, 27 insertions(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index 890cfc26aa78..ee1a44adc04f 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -646,6 +646,10 @@ def is_class_var(expr: NameExpr) -> bool: # class creation time. In order to not trigger a type error later we # just remove them. This might leave us with a Decorator with no # decorators (Emperor's new clothes?) + # TODO: Any way to type-check these? + # default should be Callable[[], T] + # validator should be Callable[[Any, 'Attribute', T], Any] + # where T is the type of the attribute. remove_me.append(func_decorator) for dec in remove_me: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index b8c41278aad4..02feb742c49a 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -300,6 +300,19 @@ class C(object): C() [builtins fixtures/list.pyi] +[case testAttrsValidatorDecorator] +import attr +@attr.s +class C(object): + x = attr.ib() + @x.validator + def check(self, attribute, value): + if value > 42: + raise ValueError("x must be smaller or equal to 42") +C(42) +C(43) +[builtins fixtures/exception.pyi] + [case testAttrsLocalVariablesInClassMethod] import attr @attr.s(auto_attribs=True) @@ -452,3 +465,12 @@ class A: x = y = z = attr.ib() reveal_type(A) # E: Revealed type is 'def (x: Any, y: Any, z: Any) -> __main__.A' [builtins fixtures/list.pyi] + +[case testAttrsPrivateInit] +import attr +@attr.s +class C(object): + _x = attr.ib(init=False, default=42) +C() +C(_x=42) # E: Unexpected keyword argument "_x" for "C" +[builtins fixtures/list.pyi] diff --git a/test-data/unit/fixtures/exception.pyi b/test-data/unit/fixtures/exception.pyi index 999a73739364..b6810d41fd1f 100644 --- a/test-data/unit/fixtures/exception.pyi +++ b/test-data/unit/fixtures/exception.pyi @@ -11,5 +11,6 @@ class int: pass class str: pass class unicode: pass class bool: pass +class ellipsis: pass class BaseException: pass From aeb01be10cd41c7a4df2deaf69caed008a4e9210 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 18 Jan 2018 20:44:17 -0800 Subject: [PATCH 58/81] Small cleanups --- mypy/plugin.py | 49 +++++++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index ee1a44adc04f..d69ae12c71e9 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -534,6 +534,7 @@ def is_class_var(expr: NameExpr) -> bool: return False decorator = ctx.reason + info = ctx.cls.info # auto_attribs means we also generate Attributes from annotated variables. auto_attribs = get_bool_argument(decorator, 'auto_attribs', auto_attribs_default) @@ -547,7 +548,7 @@ def is_class_var(expr: NameExpr) -> bool: # x: type # x: type = default_value own_attrs = OrderedDict() # type: OrderedDict[str, Attribute] - for stmt in ctx.cls.info.defn.defs.body: + for stmt in ctx.cls.defs.body: if isinstance(stmt, AssignmentStmt): for lvalue in stmt.lvalues: # To handle all types of assignments we just convert everything @@ -659,7 +660,7 @@ def is_class_var(expr: NameExpr) -> bool: super_attrs = [] # Traverse the MRO and collect attributes. - for super_info in ctx.cls.info.mro[1:-1]: + for super_info in info.mro[1:-1]: if super_info in attr_classes: for a in attr_classes[super_info]: # Only add an attribute if it hasn't been defined before. This @@ -671,7 +672,7 @@ def is_class_var(expr: NameExpr) -> bool: attributes = super_attrs + list(own_attrs.values()) # Save the attributes so that subclasses can reuse them. # TODO: Fix caching. - attr_classes[ctx.cls.info] = attributes + attr_classes[info] = attributes if ctx.api.options.disallow_untyped_defs: for attribute in attributes: @@ -682,7 +683,16 @@ def is_class_var(expr: NameExpr) -> bool: # error in the assignment, which is where you would fix the issue. ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, attribute.context) - info = ctx.cls.info + # Check the init args for correct default-ness. Note: This has to be done after all the + # attributes for all classes have been read, because subclasses can override parents. + last_default = False + for attribute in attributes: + if not attribute.has_default and last_default: + ctx.api.fail( + "Non-default attributes not allowed after default attributes.", + attribute.context) + last_default = attribute.has_default + self_type = fill_typevars(info) function_type = ctx.api.named_type('__builtins__.function') @@ -712,17 +722,6 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if get_bool_argument(decorator, 'init', True): # Generate the __init__ method. - - # Check the init args for correct default-ness. Note: This has to be done after all the - # attributes for all classes have been read, because subclasses can override parents. - last_default = False - for attribute in attributes: - if not attribute.has_default and last_default: - ctx.api.fail( - "Non-default attributes not allowed after default attributes.", - attribute.context) - last_default = attribute.has_default - add_method( '__init__', [attribute.argument() for attribute in attributes if attribute.init], @@ -730,37 +729,35 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, ) for stmt in ctx.cls.defs.body: - # The implicit first type of cls methods will be wrong because it's based on - # the non-existent init. Set it correctly. + # The type of classmethods will be wrong because it's based on the parent's __init__. + # Set it correctly. if isinstance(stmt, Decorator) and stmt.func.is_class: func_type = stmt.func.type if isinstance(func_type, CallableType): - func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info) + func_type.arg_types[0] = ctx.api.class_type(info) if get_bool_argument(decorator, 'frozen', False): # If the class is frozen then all the attributes need to be turned into properties. for attribute in attributes: - node = ctx.cls.info.names[attribute.name].node + node = info.names[attribute.name].node assert isinstance(node, Var) node.is_initialized_in_class = False node.is_property = True if get_bool_argument(decorator, 'cmp', True): - # Generate cmp methods that look like this: - # def __ne__(self, other: '') -> bool: ... - # We use fullname to handle nested classes, splitting to remove the module name. + # For __ne__ and __eq__ the type is: + # def __ne__(self, other: object) -> bool bool_type = ctx.api.named_type('__builtins__.bool') object_type = ctx.api.named_type('__builtins__.object') - # For __ne__ and __eq__ the type is: def __ne__(self, other: object) -> bool args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] for method in ['__ne__', '__eq__']: add_method(method, args, bool_type) # For the rest we use: - # T = TypeVar('T') - # def __lt__(self: T, other: T) -> bool - # This way we comparisons with subclasses will work correctly. + # AT = TypeVar('AT') + # def __lt__(self: AT, other: AT) -> bool + # This way comparisons with subclasses will work correctly. tvd = TypeVarDef('AT', 'AT', 1, [], object_type) tvd_type = TypeVarType(tvd) args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] From 4fcb5dd32a70bafab70dafd81387bc2b4cf43c16 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 18 Jan 2018 20:45:17 -0800 Subject: [PATCH 59/81] abstract --- mypy/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index d69ae12c71e9..eabd656a2770 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -88,6 +88,7 @@ def anal_type(self, t: Type, *, third_pass: bool = False) -> Type: raise NotImplementedError + @abstractmethod def class_type(self, info: TypeInfo) -> Type: raise NotImplementedError From 1ddb89e9de28dbe8e8dbf9ba22d79f0bebe9459d Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 18 Jan 2018 22:18:45 -0800 Subject: [PATCH 60/81] Add more tests. --- test-data/unit/check-attr.test | 214 +++++++++++++++++++++++++++------ 1 file changed, 177 insertions(+), 37 deletions(-) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 02feb742c49a..0e45e41dedf9 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1,8 +1,4 @@ [case testAttrsSimple] -# TODO(David): -# Add tests for: -# o type inference - import attr @attr.s class A: @@ -12,21 +8,14 @@ class A: _d = attr.ib(validator=None, default=18) E = 18 + def foo(self): + return self.a reveal_type(A) # E: Revealed type is 'def (a: Any, b: Any, c: Any =, d: Any =) -> __main__.A' -A(1,2) -[builtins fixtures/bool.pyi] - -[case testAttrsUntypedNoUntypedDefs] -# flags: --disallow-untyped-defs -import attr -@attr.s # E: Function is missing a type annotation for one or more arguments -class A: - a = attr.ib() # E: Need type annotation for variable - _b = attr.ib() # E: Need type annotation for variable - c = attr.ib(18) # E: Need type annotation for variable - _d = attr.ib(validator=None, default=18) # E: Need type annotation for variable - E = 18 -[builtins fixtures/bool.pyi] +A(1, [2]) +A(1, [2], '3', 4) +A(1, 2, 3, 4) +A(1, [2], '3', 4, 5) # E: Too many arguments for "A" +[builtins fixtures/list.pyi] [case testAttrsAnnotated] import attr @@ -34,13 +23,16 @@ from typing import List, ClassVar @attr.s class A: a: int = attr.ib() - _b: List[str] = attr.ib() + _b: List[int] = attr.ib() c: str = attr.ib('18') _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 -reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -A(1, ['2']) +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> __main__.A' +A(1, [2]) +A(1, [2], '3', 4) +A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" +A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [builtins fixtures/list.pyi] [case testAttrsPython2Annotations] @@ -49,13 +41,16 @@ from typing import List, ClassVar @attr.s class A: a = attr.ib() # type: int - _b = attr.ib() # type: List[str] + _b = attr.ib() # type: List[int] c = attr.ib('18') # type: str _d = attr.ib(validator=None, default=18) # type: int E = 7 F: ClassVar[int] = 22 -reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -A(1, ['2']) +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> __main__.A' +A(1, [2]) +A(1, [2], '3', 4) +A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" +A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [builtins fixtures/list.pyi] [case testAttrsAutoAttribs] @@ -64,26 +59,101 @@ from typing import List, ClassVar @attr.s(auto_attribs=True) class A: a: int - _b: List[str] + _b: List[int] c: str = '18' _d: int = attr.ib(validator=None, default=18) E = 7 F: ClassVar[int] = 22 -reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.str], c: builtins.str =, d: builtins.int =) -> __main__.A' -A(1, ['2']) +reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.list[builtins.int], c: builtins.str =, d: builtins.int =) -> __main__.A' +A(1, [2]) +A(1, [2], '3', 4) +A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" # E: Argument 3 to "A" has incompatible type "int"; expected "str" +A(1, [2], '3', 4, 5) # E: Too many arguments for "A" [builtins fixtures/list.pyi] +[case testAttrsUntypedNoUntypedDefs] +# flags: --disallow-untyped-defs +import attr +@attr.s # E: Function is missing a type annotation for one or more arguments +class A: + a = attr.ib() # E: Need type annotation for variable + _b = attr.ib() # E: Need type annotation for variable + c = attr.ib(18) # E: Need type annotation for variable + _d = attr.ib(validator=None, default=18) # E: Need type annotation for variable + E = 18 +[builtins fixtures/bool.pyi] + +[case testAttrsWrongReturnValue] +import attr +@attr.s +class A: + x: int = attr.ib(8) + def foo(self) -> str: + return self.x # E: Incompatible return value type (got "int", expected "str") +@attr.s +class B: + x = attr.ib(8) # type: int + def foo(self) -> str: + return self.x # E: Incompatible return value type (got "int", expected "str") +@attr.dataclass +class C: + x: int = 8 + def foo(self) -> str: + return self.x # E: Incompatible return value type (got "int", expected "str") +@attr.s +class D: + x = attr.ib(8, type=int) + def foo(self) -> str: + return self.x # E: Incompatible return value type (got "int", expected "str") +[builtins fixtures/bool.pyi] + [case testAttrsSeriousNames] from attr import attrib, attrs +from typing import List @attrs class A: a = attrib() - _b: int = attrib() + _b: List[int] = attrib() c = attrib(18) _d = attrib(validator=None, default=18) CLASS_VAR = 18 -reveal_type(A) # E: Revealed type is 'def (a: Any, b: builtins.int, c: Any =, d: Any =) -> __main__.A' -A(1,2) +reveal_type(A) # E: Revealed type is 'def (a: Any, b: builtins.list[builtins.int], c: Any =, d: Any =) -> __main__.A' +A(1, [2]) +A(1, [2], '3', 4) +A(1, 2, 3, 4) # E: Argument 2 to "A" has incompatible type "int"; expected "List[int]" +A(1, [2], '3', 4, 5) # E: Too many arguments for "A" +[builtins fixtures/list.pyi] + +[case testAttrsDefaultErrors] +import attr +@attr.s +class A: + x = attr.ib(default=17) + y = attr.ib() # E: Non-default attributes not allowed after default attributes. +@attr.s(auto_attribs=True) +class B: + x: int = 17 + y: int # E: Non-default attributes not allowed after default attributes. +@attr.s(auto_attribs=True) +class C: + x: int = attr.ib(default=17) + y: int # E: Non-default attributes not allowed after default attributes. +@attr.s +class D: + x = attr.ib() + y = attr.ib() # E: Non-default attributes not allowed after default attributes. + + @x.default + def foo(self): + return 17 +[builtins fixtures/bool.pyi] + +[case testAttrsNotBooleans] +import attr +x = True +@attr.s(cmp=1) # E: "cmp" argument must be True or False. +class A: + a = attr.ib(init=x) # E: "init" argument must be True or False. [builtins fixtures/bool.pyi] [case testAttrsInitFalse] @@ -95,20 +165,55 @@ class A: c: int = 18 _d: int = attrib(validator=None, default=18) reveal_type(A) # E: Revealed type is 'def () -> __main__.A' -[builtins fixtures/bool.pyi] +A() +A(1, [2]) # E: Too many arguments for "A" +A(1, [2], '3', 4) # E: Too many arguments for "A" +[builtins fixtures/list.pyi] [case testAttrsInitAttribFalse] from attr import attrib, attrs - @attrs class A: a = attrib(init=False) b = attrib() - reveal_type(A) # E: Revealed type is 'def (b: Any) -> __main__.A' - [builtins fixtures/bool.pyi] +[case testAttrsCmpTrue] +from attr import attrib, attrs +@attrs(auto_attribs=True) +class A: + a: int +reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.A' +reveal_type(A.__eq__) # E: Revealed type is 'def (self: __main__.A, other: builtins.object) -> builtins.bool' +reveal_type(A.__ne__) # E: Revealed type is 'def (self: __main__.A, other: builtins.object) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(A.__le__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(A.__gt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(A.__ge__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`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) +[builtins fixtures/attr.pyi] + [case testAttrsCmpFalse] from attr import attrib, attrs @attrs(auto_attribs=True, cmp=False) @@ -116,7 +221,28 @@ class A: a: int reveal_type(A) # E: Revealed type is 'def (a: builtins.int) -> __main__.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' + A(1) < A(2) # E: Unsupported left operand type for < ("A") +A(1) <= A(2) # E: Unsupported left operand type for <= ("A") +A(1) > A(2) # E: Unsupported left operand type for > ("A") +A(1) >= A(2) # E: Unsupported left operand type for >= ("A") +A(1) == A(2) +A(1) != A(2) + +A(1) < 1 # E: Unsupported left operand type for < ("A") +A(1) <= 1 # E: Unsupported left operand type for <= ("A") +A(1) > 1 # E: Unsupported left operand type for > ("A") +A(1) >= 1 # E: Unsupported left operand type for >= ("A") +A(1) == 1 +A(1) != 1 + +1 < A(1) # E: Unsupported left operand type for < ("int") +1 <= A(1) # E: Unsupported left operand type for <= ("int") +1 > A(1) # E: Unsupported left operand type for > ("int") +1 >= A(1) # E: Unsupported left operand type for >= ("int") +1 == A(1) +1 != A(1) [builtins fixtures/attr.pyi] [case testAttrsInheritance] @@ -216,7 +342,7 @@ class A: reveal_type(A) # E: Revealed type is 'def (x: builtins.list[builtins.int], y: builtins.list[builtins.str]) -> __main__.A' [builtins fixtures/list.pyi] -[case testAttrsTypeVariable] +[case testAttrsGeneric] from typing import TypeVar, Generic, List import attr T = TypeVar('T') @@ -224,17 +350,28 @@ T = TypeVar('T') class A(Generic[T]): x: List[T] y: T = attr.ib() + def foo(self) -> List[T]: + return [self.y] + def bar(self) -> T: + return self.x[0] + def problem(self) -> T: + return self.x # E: Incompatible return value type (got "List[T]", expected "T") reveal_type(A) # E: Revealed type is 'def [T] (x: builtins.list[T`1], y: T`1) -> __main__.A[T`1]' - a = A([1], 2) reveal_type(a) # E: Revealed type is '__main__.A[builtins.int*]' +reveal_type(a.x) # E: Revealed type is 'builtins.list[builtins.int*]' +reveal_type(a.y) # E: Revealed type is 'builtins.int*' + +A(['str'], 7) # E: Cannot infer type argument 1 of "A" +A([1], '2') # E: Cannot infer type argument 1 of "A" + [builtins fixtures/list.pyi] [case testAttrsForwardReference] import attr @attr.s(auto_attribs=True) class A: - parent: B + parent: 'B' @attr.s(auto_attribs=True) class B: @@ -281,6 +418,9 @@ class A: def new(cls) -> A: reveal_type(cls) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> __main__.A' return cls(6, 'hello') + @classmethod + def bad(cls) -> A: + return cls(17) # E: Too few arguments for "A" def foo(self) -> int: return self.a reveal_type(A) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> __main__.A' From 5155480e68b385669a25f0d8a448238cd2bd3eec Mon Sep 17 00:00:00 2001 From: David Euresti Date: Fri, 19 Jan 2018 09:40:53 -0800 Subject: [PATCH 61/81] Split get_bool_argument --- mypy/plugin.py | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index eabd656a2770..20e7d69f3e34 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -477,6 +477,8 @@ def attr_class_maker_callback( See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. """ + decorator = ctx.reason + info = ctx.cls.info def get_callable_type(call: CallExpr) -> Optional[CallableType]: """Get the CallableType that's attached to a CallExpr.""" @@ -512,20 +514,15 @@ def get_argument(call: CallExpr, name: str) -> Optional[Expression]: return attr_value return None - def get_bool_argument(expr: Expression, name: str, default: bool) -> bool: - """Return the value of an argument name in the given Expression. - - If it's a CallExpr and the argument is one of the args then return it. - Otherwise return the default value for the argument. - """ - if isinstance(expr, CallExpr): - 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 + def get_bool_argument(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 is_class_var(expr: NameExpr) -> bool: @@ -534,11 +531,18 @@ def is_class_var(expr: NameExpr) -> bool: return expr.node.is_classvar return False - decorator = ctx.reason - info = ctx.cls.info + def get_decorator_bool_argument(name: str, default: bool) -> bool: + """Return the bool argument for the decorator. + + This handles both @attr.s(...) and @attr.s + """ + if isinstance(decorator, CallExpr): + return get_bool_argument(decorator, name, default) + else: + return default # auto_attribs means we also generate Attributes from annotated variables. - auto_attribs = get_bool_argument(decorator, 'auto_attribs', auto_attribs_default) + auto_attribs = get_decorator_bool_argument('auto_attribs', auto_attribs_default) # First, walk the body looking for attribute definitions. # They will look like this: @@ -721,7 +725,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, # e.g. Forward Reference Resolution. info.defn.defs.body.append(func) - if get_bool_argument(decorator, 'init', True): + if get_decorator_bool_argument('init', True): # Generate the __init__ method. add_method( '__init__', @@ -737,7 +741,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if isinstance(func_type, CallableType): func_type.arg_types[0] = ctx.api.class_type(info) - if get_bool_argument(decorator, 'frozen', False): + if get_decorator_bool_argument('frozen', False): # If the class is frozen then all the attributes need to be turned into properties. for attribute in attributes: node = info.names[attribute.name].node @@ -745,7 +749,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, node.is_initialized_in_class = False node.is_property = True - if get_bool_argument(decorator, 'cmp', True): + if get_decorator_bool_argument('cmp', True): # For __ne__ and __eq__ the type is: # def __ne__(self, other: object) -> bool bool_type = ctx.api.named_type('__builtins__.bool') From ff00d73c5774f231b91ecc0bbd23d00524c24e23 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Fri, 19 Jan 2018 17:26:13 -0800 Subject: [PATCH 62/81] Fix auto_attribs=True --- mypy/plugin.py | 11 +++++++---- test-data/unit/check-attr.test | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 20e7d69f3e34..4fd4064d5439 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -582,6 +582,11 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: if (isinstance(rvalue, CallExpr) and isinstance(rvalue.callee, RefExpr) and rvalue.callee.fullname in attr_attrib_makers): + if auto_attribs and not stmt.new_syntax: + # auto_attribs requires annotation on every attr.ib. + ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, stmt) + continue + # Look for default= in the call. # TODO: Check for attr.NOTHING attr_has_default = bool(get_argument(rvalue, 'default')) @@ -624,15 +629,13 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: # When attrs are defined twice in the same body we want to use # the 2nd definition in the 2nd location. So remove it from the - # OrderedDict - if name in own_attrs: + # OrderedDict. auto_attribs doesn't work that way. + if not auto_attribs and name in own_attrs: del own_attrs[name] own_attrs[name] = Attribute(name, typ, attr_has_default, init, stmt) elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(rvalue, TempNode) - if name in own_attrs: - del own_attrs[name] own_attrs[name] = Attribute(name, typ, has_rhs, True, stmt) elif isinstance(stmt, Decorator): diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 0e45e41dedf9..48222e133144 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -614,3 +614,37 @@ class C(object): C() C(_x=42) # E: Unexpected keyword argument "_x" for "C" [builtins fixtures/list.pyi] + +[case testAttrsAutoMustBeAll] +import attr +@attr.s(auto_attribs=True) +class A: + a: int + b = 17 + # The following forms are not allowed with auto_attribs=True + c = attr.ib() # E: Need type annotation for variable + d, e = attr.ib(), attr.ib() # E: Need type annotation for variable + f = g = attr.ib() # E: Need type annotation for variable +[builtins fixtures/bool.pyi] + +[case testAttrsRepeatedName] +import attr +@attr.s +class A: + a = attr.ib(default=8) + b = attr.ib() + a = attr.ib() +reveal_type(A) # E: Revealed type is 'def (b: Any, a: Any) -> __main__.A' +@attr.s +class B: + a: int = attr.ib(default=8) + b: int = attr.ib() + a: int = attr.ib() # E: Name 'a' already defined +reveal_type(B) # E: Revealed type is 'def (b: builtins.int, a: builtins.int) -> __main__.B' +@attr.s(auto_attribs=True) +class C: + a: int = 8 + b: int + a: int = attr.ib() # E: Name 'a' already defined +reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.int) -> __main__.C' +[builtins fixtures/bool.pyi] From bc9db4b74d83d9a2ae0c3f7c5390538fb29708d2 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sat, 20 Jan 2018 08:05:10 -0800 Subject: [PATCH 63/81] Add some python 2 checks --- mypy/plugin.py | 9 +++++++++ test-data/unit/check-attr.test | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/mypy/plugin.py b/mypy/plugin.py index 4fd4064d5439..c2cbe82dff76 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -544,6 +544,15 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: # auto_attribs means we also generate Attributes from annotated variables. auto_attribs = get_decorator_bool_argument('auto_attribs', auto_attribs_default) + if ctx.api.options.python_version[0] < 3: + if auto_attribs: + ctx.api.fail("auto_attribs is not supported in Python 2", decorator) + return + if not info.defn.base_type_exprs: + # Note: This does not catch subclassing old-style classes. + ctx.api.fail("attrs only works with new-style classes", info.defn) + return + # First, walk the body looking for attribute definitions. # They will look like this: # x = attr.ib() diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 48222e133144..16a37c599467 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -648,3 +648,22 @@ class C: a: int = attr.ib() # E: Name 'a' already defined reveal_type(C) # E: Revealed type is 'def (a: builtins.int, b: builtins.int) -> __main__.C' [builtins fixtures/bool.pyi] + +[case testAttrsNewStyleClassPy2] +# flags: --py2 +import attr +@attr.s +class Good(object): + pass +@attr.s # E: attrs only works with new-style classes +class Bad: + pass +[builtins_py2 fixtures/bool.pyi] + +[case testAttrsAutoAttribsPy2] +# flags: --py2 +import attr +@attr.s(auto_attribs=True) # E: auto_attribs is not supported in Python 2 +class A(object): + x = attr.ib() +[builtins_py2 fixtures/bool.pyi] From a4f34a83d5c1688639d5a11f798d07c9e6654d5f Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sat, 20 Jan 2018 17:07:10 -0800 Subject: [PATCH 64/81] Warn against sharing one attrib --- mypy/plugin.py | 4 ++++ test-data/unit/check-attr.test | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index c2cbe82dff76..756a01c89634 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -596,6 +596,10 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, stmt) continue + if len(stmt.lvalues) > 1: + ctx.api.fail("Too many names for one attribute", stmt) + continue + # Look for default= in the call. # TODO: Check for attr.NOTHING attr_has_default = bool(get_argument(rvalue, 'default')) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 16a37c599467..40822ef64c42 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -602,8 +602,7 @@ reveal_type(A) # E: Revealed type is 'def (x: Any, y: builtins.int, z: Any =) -> import attr @attr.s class A: - x = y = z = attr.ib() -reveal_type(A) # E: Revealed type is 'def (x: Any, y: Any, z: Any) -> __main__.A' + x = y = z = attr.ib() # E: Too many names for one attribute [builtins fixtures/list.pyi] [case testAttrsPrivateInit] From d7a8e2e2f76f932210eb564847ead1fa2efc6bf7 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 22 Jan 2018 10:35:03 -0800 Subject: [PATCH 65/81] Move helper methods out --- mypy/nodes.py | 7 + mypy/plugin.py | 242 ++++++++++++++++++--------------- test-data/unit/check-attr.test | 2 +- 3 files changed, 139 insertions(+), 112 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 32f44065ecf1..0c8b3633b818 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2607,3 +2607,10 @@ def check_arg_names(names: Sequence[Optional[str]], nodes: List[T], fail: Callab fail("Duplicate argument '{}' in {}".format(name, description), node) break seen_names.add(name) + + +def is_class_var(expr: NameExpr) -> bool: + """Return whether the expression is ClassVar[...]""" + if isinstance(expr.node, Var): + return expr.node.is_classvar + return False diff --git a/mypy/plugin.py b/mypy/plugin.py index 756a01c89634..351209465616 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -8,10 +8,13 @@ from mypy import messages from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.nodes import ( - Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, - FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, - TupleExpr, ListExpr) + Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, + Argument, Var, + FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, + TempNode, + ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, + FuncBase, + TupleExpr, ListExpr, is_class_var) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, @@ -477,76 +480,14 @@ def attr_class_maker_callback( See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. """ - decorator = ctx.reason info = ctx.cls.info - def get_callable_type(call: CallExpr) -> Optional[CallableType]: - """Get the CallableType that's attached to a CallExpr.""" - callable_type = None - if (isinstance(call.callee, RefExpr) - and isinstance(call.callee.node, Var) - and call.callee.node.type): - callee_type = call.callee.node.type - if isinstance(callee_type, Overloaded): - # We take the last overload. - callable_type = callee_type.items()[-1] - elif isinstance(callee_type, CallableType): - callable_type = callee_type - return callable_type - - def get_argument(call: CallExpr, name: str) -> Optional[Expression]: - """Return the expression for the specific argument.""" - callee = get_callable_type(call) - if not callee: - return None - - argument = callee.argument_by_name(name) - if not argument: - return None - - if not argument.name and not argument.pos: - return None - - 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 get_bool_argument(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 is_class_var(expr: NameExpr) -> bool: - """Return whether the expression is ClassVar[...]""" - if isinstance(expr.node, Var): - return expr.node.is_classvar - return False - - def get_decorator_bool_argument(name: str, default: bool) -> bool: - """Return the bool argument for the decorator. - - This handles both @attr.s(...) and @attr.s - """ - if isinstance(decorator, CallExpr): - return get_bool_argument(decorator, name, default) - else: - return default - # auto_attribs means we also generate Attributes from annotated variables. - auto_attribs = get_decorator_bool_argument('auto_attribs', auto_attribs_default) + auto_attribs = _attrs_get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) if ctx.api.options.python_version[0] < 3: if auto_attribs: - ctx.api.fail("auto_attribs is not supported in Python 2", decorator) + ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) return if not info.defn.base_type_exprs: # Note: This does not catch subclassing old-style classes. @@ -602,11 +543,11 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: # Look for default= in the call. # TODO: Check for attr.NOTHING - attr_has_default = bool(get_argument(rvalue, 'default')) + attr_has_default = bool(_attrs_get_argument(rvalue, 'default')) # If the type isn't set through annotation but it is passed through type= # use that. - type_arg = get_argument(rvalue, 'type') + type_arg = _attrs_get_argument(rvalue, 'type') if type_arg and not typ: try: un_type = expr_to_unanalyzed_type(type_arg) @@ -622,8 +563,8 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: # If the attrib has a converter function take the type of the first # argument as the init type. # Note: convert is deprecated but works the same as converter. - converter = get_argument(rvalue, 'converter') - convert = get_argument(rvalue, 'convert') + converter = _attrs_get_argument(rvalue, 'converter') + convert = _attrs_get_argument(rvalue, 'convert') if convert and converter: ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) elif convert: @@ -638,7 +579,7 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: typ = converter.node.type.arg_types[0] # Does this even have to go in init. - init = get_bool_argument(rvalue, 'init', True) + init = _attrs_get_bool_argument(ctx, rvalue, 'init', True) # When attrs are defined twice in the same body we want to use # the 2nd definition in the 2nd location. So remove it from the @@ -668,7 +609,7 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: # class creation time. In order to not trigger a type error later we # just remove them. This might leave us with a Decorator with no # decorators (Emperor's new clothes?) - # TODO: Any way to type-check these? + # TODO: It would be nice to type-check these rather than remove them. # default should be Callable[[], T] # validator should be Callable[[Any, 'Attribute', T], Any] # where T is the type of the attribute. @@ -680,7 +621,7 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: taken_attr_names = set(own_attrs) super_attrs = [] - # Traverse the MRO and collect attributes. + # Traverse the MRO and collect attributes from the parents. for super_info in info.mro[1:-1]: if super_info in attr_classes: for a in attr_classes[super_info]: @@ -692,16 +633,16 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: attributes = super_attrs + list(own_attrs.values()) # Save the attributes so that subclasses can reuse them. - # TODO: Fix caching. + # TODO: This doesn't work with incremental mode if the parent class is in a different file. attr_classes[info] = attributes if ctx.api.options.disallow_untyped_defs: for attribute in attributes: if attribute.type is None: # This is a compromise. If you don't have a type here then the __init__ will - # be untyped. But since the __init__ method doesn't have a line number it's - # difficult to point to the correct line number. So instead we just show the - # error in the assignment, which is where you would fix the issue. + # be untyped. But since the __init__ is added it's pointing at the decorator. + # So instead we just show the error in the assignment, which is where you + # would fix the issue. ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, attribute.context) # Check the init args for correct default-ness. Note: This has to be done after all the @@ -714,36 +655,11 @@ def get_decorator_bool_argument(name: str, default: bool) -> bool: attribute.context) last_default = attribute.has_default - self_type = fill_typevars(info) - function_type = ctx.api.named_type('__builtins__.function') - - def add_method(method_name: str, args: List[Argument], ret_type: Type, - self_type: Type = self_type, - tvd: Optional[TypeVarDef] = None) -> None: - """Create a method: def (self, ) -> ): ...""" - from mypy.semanal import set_callable_name - args = [Argument(Var('self'), self_type, None, ARG_POS)] + args - arg_types = [arg.type_annotation for arg in args] - arg_names = [arg.variable.name() for arg in args] - arg_kinds = [arg.kind for arg in args] - assert None not in arg_types - signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, - ret_type, function_type) - if tvd: - signature.variables = [tvd] - func = FuncDef(method_name, args, Block([PassStmt()])) - func.info = info - func.type = set_callable_name(signature, func) - func._fullname = info.fullname() + '.' + method_name - func.line = ctx.cls.line - info.names[method_name] = SymbolTableNode(MDEF, func) - # Add the created methods to the body so that they can get further semantic analysis. - # e.g. Forward Reference Resolution. - info.defn.defs.body.append(func) + adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) - if get_decorator_bool_argument('init', True): + if _attrs_get_decorator_bool_argument(ctx, 'init', True): # Generate the __init__ method. - add_method( + adder.add_method( '__init__', [attribute.argument() for attribute in attributes if attribute.init], NoneTyp() @@ -757,7 +673,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, if isinstance(func_type, CallableType): func_type.arg_types[0] = ctx.api.class_type(info) - if get_decorator_bool_argument('frozen', False): + if _attrs_get_decorator_bool_argument(ctx, 'frozen', False): # If the class is frozen then all the attributes need to be turned into properties. for attribute in attributes: node = info.names[attribute.name].node @@ -765,7 +681,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, node.is_initialized_in_class = False node.is_property = True - if get_decorator_bool_argument('cmp', True): + if _attrs_get_decorator_bool_argument(ctx, 'cmp', True): # For __ne__ and __eq__ the type is: # def __ne__(self, other: object) -> bool bool_type = ctx.api.named_type('__builtins__.bool') @@ -773,7 +689,7 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] for method in ['__ne__', '__eq__']: - add_method(method, args, bool_type) + adder.add_method(method, args, bool_type) # For the rest we use: # AT = TypeVar('AT') @@ -783,4 +699,108 @@ def add_method(method_name: str, args: List[Argument], ret_type: Type, tvd_type = TypeVarType(tvd) args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] for method in ['__lt__', '__le__', '__gt__', '__ge__']: - add_method(method, args, bool_type, self_type=tvd_type, tvd=tvd) + adder.add_method(method, args, bool_type, + self_type=tvd_type, tvd=tvd) + + +def _attrs_get_decorator_bool_argument(ctx: 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 _attrs_get_bool_argument(ctx, ctx.reason, name, default) + else: + return default + + +def _attrs_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 = _attrs_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 _attrs_get_argument(call: CallExpr, name: str) -> Optional[Expression]: + """Return the expression for the specific argument.""" + # To do this we find the CallableType of the callee and to find the FormalArgument. + # 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) + 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 + + # Now walk the actual call to pick off the correct argument. + 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. + + info: The TypeInfo on which we will add methods. + function_type: The type of __builtins__.function that will be used as the + fallback for all methods added. + """ + + # TODO: Combine this with the code build_namedtuple_typeinfo to support both. + + def __init__(self, info: TypeInfo, function_type: Instance) -> None: + self.info = info + self.self_type = fill_typevars(info) + self.function_type = function_type + + def add_method(self, + method_name: str, args: List[Argument], ret_type: Type, + self_type: Optional[Type] = None, + tvd: Optional[TypeVarDef] = None) -> None: + """Add a method: def (self, ) -> ): ... to info. + + self_type: The type to use for the self argument or None to use the inferred self type. + tvd: If the method is generic these should be the type variables. + """ + from mypy.semanal import set_callable_name + self_type = self_type if self_type is not None else self.self_type + args = [Argument(Var('self'), self_type, None, ARG_POS)] + args + arg_types = [arg.type_annotation for arg in args] + arg_names = [arg.variable.name() for arg in args] + arg_kinds = [arg.kind for arg in args] + assert None not in arg_types + signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, + ret_type, self.function_type) + if tvd: + signature.variables = [tvd] + func = FuncDef(method_name, args, Block([PassStmt()])) + func.info = self.info + func.type = set_callable_name(signature, func) + func._fullname = self.info.fullname() + '.' + method_name + func.line = self.info.line + self.info.names[method_name] = SymbolTableNode(MDEF, func) + # Add the created methods to the body so that they can get further semantic analysis. + # e.g. Forward Reference Resolution. + self.info.defn.defs.body.append(func) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 40822ef64c42..51ce2dabdbfd 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -110,7 +110,7 @@ class D: [case testAttrsSeriousNames] from attr import attrib, attrs from typing import List -@attrs +@attrs(init=True) class A: a = attrib() _b: List[int] = attrib() From 8eae2733a77b4d293c9d7d9c32438c41794b1e21 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 22 Jan 2018 11:12:25 -0800 Subject: [PATCH 66/81] Fix for new messages --- mypy/plugin.py | 21 +++++++++++---------- test-data/unit/check-attr.test | 14 +++++++------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 351209465616..ada4e97c7e7f 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -69,6 +69,7 @@ class SemanticAnalyzerPluginInterface: """Interface for accessing semantic analyzer functionality in plugins.""" options = None # type: Options + msg = None # type: MessageBuilder @abstractmethod def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance: @@ -534,7 +535,8 @@ def attr_class_maker_callback( and rvalue.callee.fullname in attr_attrib_makers): if auto_attribs and not stmt.new_syntax: # auto_attribs requires annotation on every attr.ib. - ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, stmt) + assert lhs.node is not None + ctx.api.msg.need_annotation_for_var(lhs.node, stmt) continue if len(stmt.lvalues) > 1: @@ -560,6 +562,14 @@ def attr_class_maker_callback( lhs.node.type = typ lhs.is_inferred_def = False + if ctx.api.options.disallow_untyped_defs and not typ: + # This is a compromise. If you don't have a type here then the + # __init__ will be untyped. But since the __init__ is added it's + # pointing at the decorator. So instead we also show the error in the + # assignment, which is where you would fix the issue. + assert lhs.node is not None + ctx.api.msg.need_annotation_for_var(lhs.node, stmt) + # If the attrib has a converter function take the type of the first # argument as the init type. # Note: convert is deprecated but works the same as converter. @@ -636,15 +646,6 @@ def attr_class_maker_callback( # TODO: This doesn't work with incremental mode if the parent class is in a different file. attr_classes[info] = attributes - if ctx.api.options.disallow_untyped_defs: - for attribute in attributes: - if attribute.type is None: - # This is a compromise. If you don't have a type here then the __init__ will - # be untyped. But since the __init__ is added it's pointing at the decorator. - # So instead we just show the error in the assignment, which is where you - # would fix the issue. - ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, attribute.context) - # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. last_default = False diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 51ce2dabdbfd..c5459f4ccaaf 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -76,10 +76,10 @@ A(1, [2], '3', 4, 5) # E: Too many arguments for "A" import attr @attr.s # E: Function is missing a type annotation for one or more arguments class A: - a = attr.ib() # E: Need type annotation for variable - _b = attr.ib() # E: Need type annotation for variable - c = attr.ib(18) # E: Need type annotation for variable - _d = attr.ib(validator=None, default=18) # E: Need type annotation for variable + a = attr.ib() # E: Need type annotation for 'a' + _b = attr.ib() # E: Need type annotation for '_b' + c = attr.ib(18) # E: Need type annotation for 'c' + _d = attr.ib(validator=None, default=18) # E: Need type annotation for '_d' E = 18 [builtins fixtures/bool.pyi] @@ -621,9 +621,9 @@ class A: a: int b = 17 # The following forms are not allowed with auto_attribs=True - c = attr.ib() # E: Need type annotation for variable - d, e = attr.ib(), attr.ib() # E: Need type annotation for variable - f = g = attr.ib() # E: Need type annotation for variable + c = attr.ib() # E: Need type annotation for 'c' + d, e = attr.ib(), attr.ib() # E: Need type annotation for 'd' # E: Need type annotation for 'e' + f = g = attr.ib() # E: Need type annotation for 'f' # E: Need type annotation for 'g' [builtins fixtures/bool.pyi] [case testAttrsRepeatedName] From 93f80d6366df4c56087c605d496a7b151b30dc72 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 22 Jan 2018 11:15:04 -0800 Subject: [PATCH 67/81] Cleanup imports --- mypy/plugin.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index ada4e97c7e7f..dc31fb18ac3a 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -5,16 +5,12 @@ from functools import partial from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict -from mypy import messages from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.nodes import ( - Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, - Argument, Var, - FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, - TempNode, - ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, - FuncBase, - TupleExpr, ListExpr, is_class_var) + Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, + Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_OPT, ARG_POS, + NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, TupleExpr, ListExpr, is_class_var +) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, From 8cdcbfa99f84fed02483af26e027cefc7f425e1e Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 22 Jan 2018 11:41:24 -0800 Subject: [PATCH 68/81] De-flake --- mypy/plugin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index dc31fb18ac3a..da0881b21d6b 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -7,9 +7,10 @@ from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.nodes import ( - Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, FuncDef, - Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, ARG_OPT, ARG_POS, - NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, TupleExpr, ListExpr, is_class_var + Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, + FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, + ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, TupleExpr, + ListExpr, is_class_var ) from mypy.tvar_scope import TypeVarScope from mypy.types import ( From 70e25ddc4f633dd2f3062320a0af1430479be423 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 22 Jan 2018 12:19:31 -0800 Subject: [PATCH 69/81] Cleanup comment --- mypy/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index da0881b21d6b..ac5a97671e00 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -727,7 +727,9 @@ def _attrs_get_bool_argument(ctx: ClassDefContext, expr: CallExpr, def _attrs_get_argument(call: CallExpr, name: str) -> Optional[Expression]: """Return the expression for the specific argument.""" - # To do this we find the CallableType of the callee and to find the FormalArgument. + # 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 @@ -749,7 +751,6 @@ def _attrs_get_argument(call: CallExpr, name: str) -> Optional[Expression]: return None assert argument.name - # Now walk the actual call to pick off the correct argument. 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 From fc2b22b2b3f4383b7c503f1e488faa87240f4fc3 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 24 Jan 2018 09:42:24 -0800 Subject: [PATCH 70/81] Add warning for using convert --- mypy/plugin.py | 1 + test-data/unit/check-attr.test | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index ac5a97671e00..66827491982f 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -575,6 +575,7 @@ def attr_class_maker_callback( if convert and converter: ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) elif convert: + ctx.api.fail("convert is deprecated, use converter", rvalue) converter = convert if (converter and isinstance(converter, RefExpr) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index c5459f4ccaaf..da8e28099e0e 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -493,7 +493,7 @@ def convert(s:int) -> str: @attr.s class C: - x: str = attr.ib(convert=convert) + x: str = attr.ib(convert=convert) # E: convert is deprecated, use converter # Because of the convert the __init__ takes an int, but the variable is a str. reveal_type(C) # E: Revealed type is 'def (x: builtins.int) -> __main__.C' From 95e3ac0f2c74f1ac0579b1536105cf538bb0b278 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 30 Jan 2018 06:49:02 -0800 Subject: [PATCH 71/81] Fix bug found when using real stubs --- mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 66827491982f..92c01defa040 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -735,7 +735,7 @@ def _attrs_get_argument(call: CallExpr, name: str) -> Optional[Expression]: # attrib and class makers. callee_type = None if (isinstance(call.callee, RefExpr) - and isinstance(call.callee.node, Var) + 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): From a076957c61fba1eac9e08900e7e5473300edd148 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Fri, 2 Feb 2018 09:57:29 -0800 Subject: [PATCH 72/81] Move all attrs code to attrs_plugin.py --- mypy/attrs_plugin.py | 402 +++++++++++++++++++++++++++++++++++++++++++ mypy/plugin.py | 398 +----------------------------------------- 2 files changed, 409 insertions(+), 391 deletions(-) create mode 100644 mypy/attrs_plugin.py diff --git a/mypy/attrs_plugin.py b/mypy/attrs_plugin.py new file mode 100644 index 000000000000..f4eb377d232e --- /dev/null +++ b/mypy/attrs_plugin.py @@ -0,0 +1,402 @@ +"""Plugin for supporting the attrs library (http://www.attrs.org)""" +from collections import OrderedDict +from typing import Optional, Dict, List, cast + +import mypy.plugin # To avoid circular imports. +from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError +from mypy.nodes import ( + Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt, + TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncBase, + is_class_var, TempNode, Decorator, MemberExpr, Expression, FuncDef, Block, + PassStmt, SymbolTableNode, MDEF +) +from mypy.types import ( + Type, AnyType, TypeOfAny, CallableType, NoneTyp, TypeVarDef, TypeVarType, + Overloaded, Instance +) +from mypy.typevars import fill_typevars + + +# The names of the different functions that create classes or arguments. +attr_class_makers = { + 'attr.s', + 'attr.attrs', + 'attr.attributes', +} +attr_dataclass_makers = { + 'attr.dataclass', +} +attr_attrib_makers = { + 'attr.ib', + 'attr.attrib', + 'attr.attr', +} + + +class Attribute: + """The value of an attr.ib() call.""" + + def __init__(self, name: str, type: Optional[Type], + has_default: bool, init: bool, + context: Context) -> None: + self.name = name + self.type = type + self.has_default = has_default + self.init = init + self.context = context + + def argument(self) -> Argument: + """Return this attribute as an argument to __init__.""" + # Convert type not set to Any. + _type = self.type or AnyType(TypeOfAny.unannotated) + # Attrs removes leading underscores when creating the __init__ arguments. + return Argument(Var(self.name.lstrip("_"), _type), _type, + None, + ARG_OPT if self.has_default else ARG_POS) + + +def attr_class_maker_callback( + attr_classes: Dict[TypeInfo, List[Attribute]], + ctx: 'mypy.plugin.ClassDefContext', + auto_attribs_default: bool = False +) -> None: + """Add necessary dunder methods to classes decorated with attr.s. + + attrs is a package that lets you define classes without writing dull boilerplate code. + + At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or + annotated variables if auto_attribs=True), then depending on how the decorator is called, + it will add an __init__ or all the __cmp__ methods. For frozen=True it will turn the attrs + into properties. + + See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. + """ + info = ctx.cls.info + + # auto_attribs means we also generate Attributes from annotated variables. + auto_attribs = _attrs_get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) + + if ctx.api.options.python_version[0] < 3: + if auto_attribs: + ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) + return + if not info.defn.base_type_exprs: + # Note: This does not catch subclassing old-style classes. + ctx.api.fail("attrs only works with new-style classes", info.defn) + return + + # First, walk the body looking for attribute definitions. + # They will look like this: + # x = attr.ib() + # x = y = attr.ib() + # x, y = attr.ib(), attr.ib() + # or if auto_attribs is enabled also like this: + # x: type + # x: type = default_value + own_attrs = OrderedDict() # type: OrderedDict[str, Attribute] + for stmt in ctx.cls.defs.body: + if isinstance(stmt, AssignmentStmt): + for lvalue in stmt.lvalues: + # To handle all types of assignments we just convert everything + # to a matching lists of lefts and rights. + lhss = [] # type: List[NameExpr] + rvalues = [] # type: List[Expression] + if isinstance(lvalue, (TupleExpr, ListExpr)): + if all(isinstance(item, NameExpr) for item in lvalue.items): + lhss = cast(List[NameExpr], lvalue.items) + if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): + rvalues = stmt.rvalue.items + elif isinstance(lvalue, NameExpr): + lhss = [lvalue] + rvalues = [stmt.rvalue] + + if len(lhss) != len(rvalues): + # This means we have some assignment that isn't 1 to 1. + # It can't be an attrib. + continue + + for lhs, rvalue in zip(lhss, rvalues): + typ = stmt.type + name = lhs.name + + # Check if the right hand side is a call to an attribute maker. + if (isinstance(rvalue, CallExpr) + and isinstance(rvalue.callee, RefExpr) + and rvalue.callee.fullname in attr_attrib_makers): + if auto_attribs and not stmt.new_syntax: + # auto_attribs requires annotation on every attr.ib. + assert lhs.node is not None + ctx.api.msg.need_annotation_for_var(lhs.node, stmt) + continue + + if len(stmt.lvalues) > 1: + ctx.api.fail("Too many names for one attribute", stmt) + continue + + # Look for default= in the call. + # TODO: Check for attr.NOTHING + attr_has_default = bool(_attrs_get_argument(rvalue, 'default')) + + # If the type isn't set through annotation but it is passed through type= + # use that. + type_arg = _attrs_get_argument(rvalue, 'type') + if type_arg and not typ: + try: + un_type = expr_to_unanalyzed_type(type_arg) + except TypeTranslationError: + ctx.api.fail('Invalid argument to type', type_arg) + else: + typ = ctx.api.anal_type(un_type) + if typ and isinstance(lhs.node, Var) and not lhs.node.type: + # If there is no annotation, add one. + lhs.node.type = typ + lhs.is_inferred_def = False + + if ctx.api.options.disallow_untyped_defs and not typ: + # This is a compromise. If you don't have a type here then the + # __init__ will be untyped. But since the __init__ is added it's + # pointing at the decorator. So instead we also show the error in the + # assignment, which is where you would fix the issue. + assert lhs.node is not None + ctx.api.msg.need_annotation_for_var(lhs.node, stmt) + + # If the attrib has a converter function take the type of the first + # argument as the init type. + # Note: convert is deprecated but works the same as converter. + converter = _attrs_get_argument(rvalue, 'converter') + convert = _attrs_get_argument(rvalue, 'convert') + if convert and converter: + ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) + elif convert: + ctx.api.fail("convert is deprecated, use converter", rvalue) + converter = convert + if (converter + and isinstance(converter, RefExpr) + and converter.node + and isinstance(converter.node, FuncBase) + and converter.node.type + and isinstance(converter.node.type, CallableType) + and converter.node.type.arg_types): + typ = converter.node.type.arg_types[0] + + # Does this even have to go in init. + init = _attrs_get_bool_argument(ctx, rvalue, 'init', True) + + # When attrs are defined twice in the same body we want to use + # the 2nd definition in the 2nd location. So remove it from the + # OrderedDict. auto_attribs doesn't work that way. + if not auto_attribs and name in own_attrs: + del own_attrs[name] + own_attrs[name] = Attribute(name, typ, attr_has_default, init, stmt) + elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(rvalue, TempNode) + own_attrs[name] = Attribute(name, typ, has_rhs, True, stmt) + + elif isinstance(stmt, Decorator): + # Look for attr specific decorators. ('x.default' and 'x.validator') + remove_me = [] + for func_decorator in stmt.decorators: + if (isinstance(func_decorator, MemberExpr) + and isinstance(func_decorator.expr, NameExpr) + and func_decorator.expr.name in own_attrs): + + if func_decorator.name == 'default': + # This decorator lets you set a default after the fact. + own_attrs[func_decorator.expr.name].has_default = True + + if func_decorator.name in ('default', 'validator'): + # These are decorators on the attrib object that only exist during + # class creation time. In order to not trigger a type error later we + # just remove them. This might leave us with a Decorator with no + # decorators (Emperor's new clothes?) + # TODO: It would be nice to type-check these rather than remove them. + # default should be Callable[[], T] + # validator should be Callable[[Any, 'Attribute', T], Any] + # where T is the type of the attribute. + remove_me.append(func_decorator) + + for dec in remove_me: + stmt.decorators.remove(dec) + + taken_attr_names = set(own_attrs) + super_attrs = [] + + # Traverse the MRO and collect attributes from the parents. + for super_info in info.mro[1:-1]: + if super_info in attr_classes: + for a in attr_classes[super_info]: + # Only add an attribute if it hasn't been defined before. This + # allows for overwriting attribute definitions by subclassing. + if a.name not in taken_attr_names: + super_attrs.append(a) + taken_attr_names.add(a.name) + + attributes = super_attrs + list(own_attrs.values()) + # Save the attributes so that subclasses can reuse them. + # TODO: This doesn't work with incremental mode if the parent class is in a different file. + attr_classes[info] = attributes + + # Check the init args for correct default-ness. Note: This has to be done after all the + # attributes for all classes have been read, because subclasses can override parents. + last_default = False + for attribute in attributes: + if not attribute.has_default and last_default: + ctx.api.fail( + "Non-default attributes not allowed after default attributes.", + attribute.context) + last_default = attribute.has_default + + adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) + + if _attrs_get_decorator_bool_argument(ctx, 'init', True): + # Generate the __init__ method. + adder.add_method( + '__init__', + [attribute.argument() for attribute in attributes if attribute.init], + NoneTyp() + ) + + for stmt in ctx.cls.defs.body: + # The type of classmethods will be wrong because it's based on the parent's __init__. + # Set it correctly. + if isinstance(stmt, Decorator) and stmt.func.is_class: + func_type = stmt.func.type + if isinstance(func_type, CallableType): + func_type.arg_types[0] = ctx.api.class_type(info) + + if _attrs_get_decorator_bool_argument(ctx, 'frozen', False): + # If the class is frozen then all the attributes need to be turned into properties. + for attribute in attributes: + node = info.names[attribute.name].node + assert isinstance(node, Var) + node.is_initialized_in_class = False + node.is_property = True + + if _attrs_get_decorator_bool_argument(ctx, 'cmp', True): + # For __ne__ and __eq__ the type is: + # def __ne__(self, other: object) -> bool + bool_type = ctx.api.named_type('__builtins__.bool') + object_type = ctx.api.named_type('__builtins__.object') + + args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] + for method in ['__ne__', '__eq__']: + adder.add_method(method, args, bool_type) + + # For the rest we use: + # AT = TypeVar('AT') + # def __lt__(self: AT, other: AT) -> bool + # This way comparisons with subclasses will work correctly. + tvd = TypeVarDef('AT', 'AT', 1, [], object_type) + tvd_type = TypeVarType(tvd) + args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] + for method in ['__lt__', '__le__', '__gt__', '__ge__']: + adder.add_method(method, args, bool_type, + self_type=tvd_type, tvd=tvd) + + +def _attrs_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 _attrs_get_bool_argument(ctx, ctx.reason, name, default) + else: + return default + + +def _attrs_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 = _attrs_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 _attrs_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. + + info: The TypeInfo on which we will add methods. + function_type: The type of __builtins__.function that will be used as the + fallback for all methods added. + """ + + # TODO: Combine this with the code build_namedtuple_typeinfo to support both. + + def __init__(self, info: TypeInfo, function_type: Instance) -> None: + self.info = info + self.self_type = fill_typevars(info) + self.function_type = function_type + + def add_method(self, + method_name: str, args: List[Argument], ret_type: Type, + self_type: Optional[Type] = None, + tvd: Optional[TypeVarDef] = None) -> None: + """Add a method: def (self, ) -> ): ... to info. + + self_type: The type to use for the self argument or None to use the inferred self type. + tvd: If the method is generic these should be the type variables. + """ + from mypy.semanal import set_callable_name + self_type = self_type if self_type is not None else self.self_type + args = [Argument(Var('self'), self_type, None, ARG_POS)] + args + arg_types = [arg.type_annotation for arg in args] + arg_names = [arg.variable.name() for arg in args] + arg_kinds = [arg.kind for arg in args] + assert None not in arg_types + signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, + ret_type, self.function_type) + if tvd: + signature.variables = [tvd] + func = FuncDef(method_name, args, Block([PassStmt()])) + func.info = self.info + func.type = set_callable_name(signature, func) + func._fullname = self.info.fullname() + '.' + method_name + func.line = self.info.line + self.info.names[method_name] = SymbolTableNode(MDEF, func) + # Add the created methods to the body so that they can get further semantic analysis. + # e.g. Forward Reference Resolution. + self.info.defn.defs.body.append(func) diff --git a/mypy/plugin.py b/mypy/plugin.py index 92c01defa040..c8d6e3e11341 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,25 +1,23 @@ """Plugin system for extending mypy.""" -from collections import OrderedDict from abc import abstractmethod from functools import partial -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, cast, Dict +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict -from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError +from mypy.attrs_plugin import ( + attr_class_makers, attr_dataclass_makers, attr_class_maker_callback, Attribute +) from mypy.nodes import ( - Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, Argument, Var, - FuncDef, Block, SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, - ARG_OPT, ARG_POS, NameExpr, Decorator, MemberExpr, TypeInfo, PassStmt, FuncBase, TupleExpr, - ListExpr, is_class_var + Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, + TypeInfo ) from mypy.tvar_scope import TypeVarScope from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, - AnyType, TypeList, UnboundType, TypeOfAny, TypeVarDef, Overloaded + AnyType, TypeList, UnboundType, TypeOfAny ) from mypy.messages import MessageBuilder from mypy.options import Options -from mypy.typevars import fill_typevars class TypeAnalyzerPluginInterface: @@ -422,385 +420,3 @@ def int_pow_callback(ctx: MethodContext) -> Type: else: return ctx.api.named_generic_type('builtins.float', []) return ctx.default_return_type - - -# The names of the different functions that create classes or arguments. -attr_class_makers = { - 'attr.s', - 'attr.attrs', - 'attr.attributes', -} -attr_dataclass_makers = { - 'attr.dataclass', -} -attr_attrib_makers = { - 'attr.ib', - 'attr.attrib', - 'attr.attr', -} - - -class Attribute: - """The value of an attr.ib() call.""" - - def __init__(self, name: str, type: Optional[Type], - has_default: bool, init: bool, - context: Context) -> None: - self.name = name - self.type = type - self.has_default = has_default - self.init = init - self.context = context - - def argument(self) -> Argument: - """Return this attribute as an argument to __init__.""" - # Convert type not set to Any. - _type = self.type or AnyType(TypeOfAny.unannotated) - # Attrs removes leading underscores when creating the __init__ arguments. - return Argument(Var(self.name.lstrip("_"), _type), _type, - None, - ARG_OPT if self.has_default else ARG_POS) - - -def attr_class_maker_callback( - attr_classes: Dict[TypeInfo, List[Attribute]], - ctx: ClassDefContext, - auto_attribs_default: bool = False -) -> None: - """Add necessary dunder methods to classes decorated with attr.s. - - attrs is a package that lets you define classes without writing dull boilerplate code. - - At a quick glance, the decorator searches the class body for assignments of `attr.ib`s (or - annotated variables if auto_attribs=True), then depending on how the decorator is called, - it will add an __init__ or all the __cmp__ methods. For frozen=True it will turn the attrs - into properties. - - See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. - """ - info = ctx.cls.info - - # auto_attribs means we also generate Attributes from annotated variables. - auto_attribs = _attrs_get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) - - if ctx.api.options.python_version[0] < 3: - if auto_attribs: - ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) - return - if not info.defn.base_type_exprs: - # Note: This does not catch subclassing old-style classes. - ctx.api.fail("attrs only works with new-style classes", info.defn) - return - - # First, walk the body looking for attribute definitions. - # They will look like this: - # x = attr.ib() - # x = y = attr.ib() - # x, y = attr.ib(), attr.ib() - # or if auto_attribs is enabled also like this: - # x: type - # x: type = default_value - own_attrs = OrderedDict() # type: OrderedDict[str, Attribute] - for stmt in ctx.cls.defs.body: - if isinstance(stmt, AssignmentStmt): - for lvalue in stmt.lvalues: - # To handle all types of assignments we just convert everything - # to a matching lists of lefts and rights. - lhss = [] # type: List[NameExpr] - rvalues = [] # type: List[Expression] - if isinstance(lvalue, (TupleExpr, ListExpr)): - if all(isinstance(item, NameExpr) for item in lvalue.items): - lhss = cast(List[NameExpr], lvalue.items) - if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): - rvalues = stmt.rvalue.items - elif isinstance(lvalue, NameExpr): - lhss = [lvalue] - rvalues = [stmt.rvalue] - - if len(lhss) != len(rvalues): - # This means we have some assignment that isn't 1 to 1. - # It can't be an attrib. - continue - - for lhs, rvalue in zip(lhss, rvalues): - typ = stmt.type - name = lhs.name - - # Check if the right hand side is a call to an attribute maker. - if (isinstance(rvalue, CallExpr) - and isinstance(rvalue.callee, RefExpr) - and rvalue.callee.fullname in attr_attrib_makers): - if auto_attribs and not stmt.new_syntax: - # auto_attribs requires annotation on every attr.ib. - assert lhs.node is not None - ctx.api.msg.need_annotation_for_var(lhs.node, stmt) - continue - - if len(stmt.lvalues) > 1: - ctx.api.fail("Too many names for one attribute", stmt) - continue - - # Look for default= in the call. - # TODO: Check for attr.NOTHING - attr_has_default = bool(_attrs_get_argument(rvalue, 'default')) - - # If the type isn't set through annotation but it is passed through type= - # use that. - type_arg = _attrs_get_argument(rvalue, 'type') - if type_arg and not typ: - try: - un_type = expr_to_unanalyzed_type(type_arg) - except TypeTranslationError: - ctx.api.fail('Invalid argument to type', type_arg) - else: - typ = ctx.api.anal_type(un_type) - if typ and isinstance(lhs.node, Var) and not lhs.node.type: - # If there is no annotation, add one. - lhs.node.type = typ - lhs.is_inferred_def = False - - if ctx.api.options.disallow_untyped_defs and not typ: - # This is a compromise. If you don't have a type here then the - # __init__ will be untyped. But since the __init__ is added it's - # pointing at the decorator. So instead we also show the error in the - # assignment, which is where you would fix the issue. - assert lhs.node is not None - ctx.api.msg.need_annotation_for_var(lhs.node, stmt) - - # If the attrib has a converter function take the type of the first - # argument as the init type. - # Note: convert is deprecated but works the same as converter. - converter = _attrs_get_argument(rvalue, 'converter') - convert = _attrs_get_argument(rvalue, 'convert') - if convert and converter: - ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) - elif convert: - ctx.api.fail("convert is deprecated, use converter", rvalue) - converter = convert - if (converter - and isinstance(converter, RefExpr) - and converter.node - and isinstance(converter.node, FuncBase) - and converter.node.type - and isinstance(converter.node.type, CallableType) - and converter.node.type.arg_types): - typ = converter.node.type.arg_types[0] - - # Does this even have to go in init. - init = _attrs_get_bool_argument(ctx, rvalue, 'init', True) - - # When attrs are defined twice in the same body we want to use - # the 2nd definition in the 2nd location. So remove it from the - # OrderedDict. auto_attribs doesn't work that way. - if not auto_attribs and name in own_attrs: - del own_attrs[name] - own_attrs[name] = Attribute(name, typ, attr_has_default, init, stmt) - elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(rvalue, TempNode) - own_attrs[name] = Attribute(name, typ, has_rhs, True, stmt) - - elif isinstance(stmt, Decorator): - # Look for attr specific decorators. ('x.default' and 'x.validator') - remove_me = [] - for func_decorator in stmt.decorators: - if (isinstance(func_decorator, MemberExpr) - and isinstance(func_decorator.expr, NameExpr) - and func_decorator.expr.name in own_attrs): - - if func_decorator.name == 'default': - # This decorator lets you set a default after the fact. - own_attrs[func_decorator.expr.name].has_default = True - - if func_decorator.name in ('default', 'validator'): - # These are decorators on the attrib object that only exist during - # class creation time. In order to not trigger a type error later we - # just remove them. This might leave us with a Decorator with no - # decorators (Emperor's new clothes?) - # TODO: It would be nice to type-check these rather than remove them. - # default should be Callable[[], T] - # validator should be Callable[[Any, 'Attribute', T], Any] - # where T is the type of the attribute. - remove_me.append(func_decorator) - - for dec in remove_me: - stmt.decorators.remove(dec) - - taken_attr_names = set(own_attrs) - super_attrs = [] - - # Traverse the MRO and collect attributes from the parents. - for super_info in info.mro[1:-1]: - if super_info in attr_classes: - for a in attr_classes[super_info]: - # Only add an attribute if it hasn't been defined before. This - # allows for overwriting attribute definitions by subclassing. - if a.name not in taken_attr_names: - super_attrs.append(a) - taken_attr_names.add(a.name) - - attributes = super_attrs + list(own_attrs.values()) - # Save the attributes so that subclasses can reuse them. - # TODO: This doesn't work with incremental mode if the parent class is in a different file. - attr_classes[info] = attributes - - # Check the init args for correct default-ness. Note: This has to be done after all the - # attributes for all classes have been read, because subclasses can override parents. - last_default = False - for attribute in attributes: - if not attribute.has_default and last_default: - ctx.api.fail( - "Non-default attributes not allowed after default attributes.", - attribute.context) - last_default = attribute.has_default - - adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) - - if _attrs_get_decorator_bool_argument(ctx, 'init', True): - # Generate the __init__ method. - adder.add_method( - '__init__', - [attribute.argument() for attribute in attributes if attribute.init], - NoneTyp() - ) - - for stmt in ctx.cls.defs.body: - # The type of classmethods will be wrong because it's based on the parent's __init__. - # Set it correctly. - if isinstance(stmt, Decorator) and stmt.func.is_class: - func_type = stmt.func.type - if isinstance(func_type, CallableType): - func_type.arg_types[0] = ctx.api.class_type(info) - - if _attrs_get_decorator_bool_argument(ctx, 'frozen', False): - # If the class is frozen then all the attributes need to be turned into properties. - for attribute in attributes: - node = info.names[attribute.name].node - assert isinstance(node, Var) - node.is_initialized_in_class = False - node.is_property = True - - if _attrs_get_decorator_bool_argument(ctx, 'cmp', True): - # For __ne__ and __eq__ the type is: - # def __ne__(self, other: object) -> bool - bool_type = ctx.api.named_type('__builtins__.bool') - object_type = ctx.api.named_type('__builtins__.object') - - args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] - for method in ['__ne__', '__eq__']: - adder.add_method(method, args, bool_type) - - # For the rest we use: - # AT = TypeVar('AT') - # def __lt__(self: AT, other: AT) -> bool - # This way comparisons with subclasses will work correctly. - tvd = TypeVarDef('AT', 'AT', 1, [], object_type) - tvd_type = TypeVarType(tvd) - args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] - for method in ['__lt__', '__le__', '__gt__', '__ge__']: - adder.add_method(method, args, bool_type, - self_type=tvd_type, tvd=tvd) - - -def _attrs_get_decorator_bool_argument(ctx: 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 _attrs_get_bool_argument(ctx, ctx.reason, name, default) - else: - return default - - -def _attrs_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 = _attrs_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 _attrs_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. - - info: The TypeInfo on which we will add methods. - function_type: The type of __builtins__.function that will be used as the - fallback for all methods added. - """ - - # TODO: Combine this with the code build_namedtuple_typeinfo to support both. - - def __init__(self, info: TypeInfo, function_type: Instance) -> None: - self.info = info - self.self_type = fill_typevars(info) - self.function_type = function_type - - def add_method(self, - method_name: str, args: List[Argument], ret_type: Type, - self_type: Optional[Type] = None, - tvd: Optional[TypeVarDef] = None) -> None: - """Add a method: def (self, ) -> ): ... to info. - - self_type: The type to use for the self argument or None to use the inferred self type. - tvd: If the method is generic these should be the type variables. - """ - from mypy.semanal import set_callable_name - self_type = self_type if self_type is not None else self.self_type - args = [Argument(Var('self'), self_type, None, ARG_POS)] + args - arg_types = [arg.type_annotation for arg in args] - arg_names = [arg.variable.name() for arg in args] - arg_kinds = [arg.kind for arg in args] - assert None not in arg_types - signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, - ret_type, self.function_type) - if tvd: - signature.variables = [tvd] - func = FuncDef(method_name, args, Block([PassStmt()])) - func.info = self.info - func.type = set_callable_name(signature, func) - func._fullname = self.info.fullname() + '.' + method_name - func.line = self.info.line - self.info.names[method_name] = SymbolTableNode(MDEF, func) - # Add the created methods to the body so that they can get further semantic analysis. - # e.g. Forward Reference Resolution. - self.info.defn.defs.body.append(func) From 3fa9e4f84f2b888f8087881bab258ae082e6d138 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Fri, 2 Feb 2018 22:31:24 -0800 Subject: [PATCH 73/81] Refactor code, split into helper methods --- mypy/attrs_plugin.py | 312 ++++++++++++++++++++++++------------------- 1 file changed, 171 insertions(+), 141 deletions(-) diff --git a/mypy/attrs_plugin.py b/mypy/attrs_plugin.py index f4eb377d232e..cd7ddfac981b 100644 --- a/mypy/attrs_plugin.py +++ b/mypy/attrs_plugin.py @@ -1,6 +1,6 @@ """Plugin for supporting the attrs library (http://www.attrs.org)""" from collections import OrderedDict -from typing import Optional, Dict, List, cast +from typing import Optional, Dict, List, cast, Tuple import mypy.plugin # To avoid circular imports. from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError @@ -74,7 +74,7 @@ def attr_class_maker_callback( info = ctx.cls.info # auto_attribs means we also generate Attributes from annotated variables. - auto_attribs = _attrs_get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) + auto_attribs = _get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) if ctx.api.options.python_version[0] < 3: if auto_attribs: @@ -97,101 +97,30 @@ def attr_class_maker_callback( for stmt in ctx.cls.defs.body: if isinstance(stmt, AssignmentStmt): for lvalue in stmt.lvalues: - # To handle all types of assignments we just convert everything - # to a matching lists of lefts and rights. - lhss = [] # type: List[NameExpr] - rvalues = [] # type: List[Expression] - if isinstance(lvalue, (TupleExpr, ListExpr)): - if all(isinstance(item, NameExpr) for item in lvalue.items): - lhss = cast(List[NameExpr], lvalue.items) - if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): - rvalues = stmt.rvalue.items - elif isinstance(lvalue, NameExpr): - lhss = [lvalue] - rvalues = [stmt.rvalue] - - if len(lhss) != len(rvalues): + lvalues, rvalues = _parse_assignments(lvalue, stmt) + + if len(lvalues) != len(rvalues): # This means we have some assignment that isn't 1 to 1. # It can't be an attrib. continue - for lhs, rvalue in zip(lhss, rvalues): - typ = stmt.type - name = lhs.name - + for lhs, rvalue in zip(lvalues, rvalues): # Check if the right hand side is a call to an attribute maker. if (isinstance(rvalue, CallExpr) and isinstance(rvalue.callee, RefExpr) and rvalue.callee.fullname in attr_attrib_makers): - if auto_attribs and not stmt.new_syntax: - # auto_attribs requires annotation on every attr.ib. - assert lhs.node is not None - ctx.api.msg.need_annotation_for_var(lhs.node, stmt) - continue - - if len(stmt.lvalues) > 1: - ctx.api.fail("Too many names for one attribute", stmt) - continue - - # Look for default= in the call. - # TODO: Check for attr.NOTHING - attr_has_default = bool(_attrs_get_argument(rvalue, 'default')) - - # If the type isn't set through annotation but it is passed through type= - # use that. - type_arg = _attrs_get_argument(rvalue, 'type') - if type_arg and not typ: - try: - un_type = expr_to_unanalyzed_type(type_arg) - except TypeTranslationError: - ctx.api.fail('Invalid argument to type', type_arg) - else: - typ = ctx.api.anal_type(un_type) - if typ and isinstance(lhs.node, Var) and not lhs.node.type: - # If there is no annotation, add one. - lhs.node.type = typ - lhs.is_inferred_def = False - - if ctx.api.options.disallow_untyped_defs and not typ: - # This is a compromise. If you don't have a type here then the - # __init__ will be untyped. But since the __init__ is added it's - # pointing at the decorator. So instead we also show the error in the - # assignment, which is where you would fix the issue. - assert lhs.node is not None - ctx.api.msg.need_annotation_for_var(lhs.node, stmt) - - # If the attrib has a converter function take the type of the first - # argument as the init type. - # Note: convert is deprecated but works the same as converter. - converter = _attrs_get_argument(rvalue, 'converter') - convert = _attrs_get_argument(rvalue, 'convert') - if convert and converter: - ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) - elif convert: - ctx.api.fail("convert is deprecated, use converter", rvalue) - converter = convert - if (converter - and isinstance(converter, RefExpr) - and converter.node - and isinstance(converter.node, FuncBase) - and converter.node.type - and isinstance(converter.node.type, CallableType) - and converter.node.type.arg_types): - typ = converter.node.type.arg_types[0] - - # Does this even have to go in init. - init = _attrs_get_bool_argument(ctx, rvalue, 'init', True) - - # When attrs are defined twice in the same body we want to use - # the 2nd definition in the 2nd location. So remove it from the - # OrderedDict. auto_attribs doesn't work that way. - if not auto_attribs and name in own_attrs: - del own_attrs[name] - own_attrs[name] = Attribute(name, typ, attr_has_default, init, stmt) - elif auto_attribs and typ and stmt.new_syntax and not is_class_var(lhs): - # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) - has_rhs = not isinstance(rvalue, TempNode) - own_attrs[name] = Attribute(name, typ, has_rhs, True, stmt) + attr = _attribute_from_attrib_maker(ctx, auto_attribs, lhs, + rvalue, stmt) + if attr: + # When attrs are defined twice in the same body we want to use + # the 2nd definition in the 2nd location. So remove it from the + # OrderedDict. auto_attribs doesn't work that way. + if not auto_attribs and attr.name in own_attrs: + del own_attrs[attr.name] + own_attrs[attr.name] = attr + elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): + attr_auto = _attribute_from_auto_attrib(lhs, rvalue, stmt) + own_attrs[attr_auto.name] = attr_auto elif isinstance(stmt, Decorator): # Look for attr specific decorators. ('x.default' and 'x.validator') @@ -248,54 +177,155 @@ def attr_class_maker_callback( last_default = attribute.has_default adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) + if _get_decorator_bool_argument(ctx, 'init', True): + _add_init(ctx, attributes, adder) + + if _get_decorator_bool_argument(ctx, 'frozen', False): + _make_frozen(ctx, attributes) + + if _get_decorator_bool_argument(ctx, 'cmp', True): + _make_cmp(ctx, adder) + + +def _attribute_from_auto_attrib(lhs: NameExpr, + rvalue: Expression, + stmt: AssignmentStmt) -> Attribute: + """Return an Attribute for a new type assignment.""" + # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) + has_rhs = not isinstance(rvalue, TempNode) + return Attribute(lhs.name, stmt.type, has_rhs, True, stmt) + + +def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', + auto_attribs: bool, + lhs: NameExpr, + rvalue: CallExpr, + stmt: AssignmentStmt) -> Optional[Attribute]: + """Return an Attribute from the assignment or None if you can't make one.""" + if auto_attribs and not stmt.new_syntax: + # auto_attribs requires an annotation on *every* attr.ib. + assert lhs.node is not None + ctx.api.msg.need_annotation_for_var(lhs.node, stmt) + return None + + if len(stmt.lvalues) > 1: + ctx.api.fail("Too many names for one attribute", stmt) + return None + + typ = stmt.type + + # Read all the arguments from the call. + init = _get_bool_argument(ctx, rvalue, 'init', True) + # TODO: Check for attr.NOTHING + attr_has_default = bool(_get_argument(rvalue, 'default')) + + # If the type isn't set through annotation but is passed through `type=` use that. + type_arg = _get_argument(rvalue, 'type') + if type_arg and not typ: + try: + un_type = expr_to_unanalyzed_type(type_arg) + except TypeTranslationError: + ctx.api.fail('Invalid argument to type', type_arg) + else: + typ = ctx.api.anal_type(un_type) + if typ and isinstance(lhs.node, Var) and not lhs.node.type: + # If there is no annotation, add one. + lhs.node.type = typ + lhs.is_inferred_def = False + + # If the attrib has a converter function take the type of the first argument as the init type. + # Note: convert is deprecated but works the same as converter. + converter = _get_argument(rvalue, 'converter') + convert = _get_argument(rvalue, 'convert') + if convert and converter: + ctx.api.fail("Can't pass both `convert` and `converter`.", rvalue) + elif convert: + ctx.api.fail("convert is deprecated, use converter", rvalue) + converter = convert + if (converter + and isinstance(converter, RefExpr) + and converter.node + and isinstance(converter.node, FuncBase) + and converter.node.type + and isinstance(converter.node.type, CallableType) + and converter.node.type.arg_types): + typ = converter.node.type.arg_types[0] + + if ctx.api.options.disallow_untyped_defs and not typ: + # This is a compromise. If you don't have a type here then the + # __init__ will be untyped. But since the __init__ is added it's + # pointing at the decorator. So instead we also show the error in the + # assignment, which is where you would fix the issue. + assert lhs.node is not None + ctx.api.msg.need_annotation_for_var(lhs.node, stmt) + + return Attribute(lhs.name, typ, attr_has_default, init, stmt) + + +def _parse_assignments( + lvalue: Expression, + stmt: AssignmentStmt) -> Tuple[List[NameExpr], List[Expression]]: + """Convert a possibly complex assignment expression into lists of lvalues and rvalues.""" + lvalues = [] # type: List[NameExpr] + rvalues = [] # type: List[Expression] + if isinstance(lvalue, (TupleExpr, ListExpr)): + if all(isinstance(item, NameExpr) for item in lvalue.items): + lvalues = cast(List[NameExpr], lvalue.items) + if isinstance(stmt.rvalue, (TupleExpr, ListExpr)): + rvalues = stmt.rvalue.items + elif isinstance(lvalue, NameExpr): + lvalues = [lvalue] + rvalues = [stmt.rvalue] + return lvalues, rvalues + + +def _make_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None: + """Generate all the cmp methods for this class.""" + # For __ne__ and __eq__ the type is: + # def __ne__(self, other: object) -> bool + bool_type = ctx.api.named_type('__builtins__.bool') + object_type = ctx.api.named_type('__builtins__.object') + args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] + for method in ['__ne__', '__eq__']: + adder.add_method(method, args, bool_type) + # For the rest we use: + # AT = TypeVar('AT') + # def __lt__(self: AT, other: AT) -> bool + # This way comparisons with subclasses will work correctly. + tvd = TypeVarDef('AT', 'AT', 1, [], object_type) + tvd_type = TypeVarType(tvd) + args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] + for method in ['__lt__', '__le__', '__gt__', '__ge__']: + adder.add_method(method, args, bool_type, self_type=tvd_type, tvd=tvd) + + +def _make_frozen(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute]) -> None: + """Turn all the attributes into properties to simulate frozen classes.""" + for attribute in attributes: + node = ctx.cls.info.names[attribute.name].node + assert isinstance(node, Var) + node.is_initialized_in_class = False + node.is_property = True + + +def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute], + adder: 'MethodAdder') -> None: + """Generate an __init__ method for the attributes and add it to the class.""" + adder.add_method( + '__init__', + [attribute.argument() for attribute in attributes if attribute.init], + NoneTyp() + ) + for stmt in ctx.cls.defs.body: + # The type of classmethods will be wrong because it's based on the parent's __init__. + # Set it correctly. + if isinstance(stmt, Decorator) and stmt.func.is_class: + func_type = stmt.func.type + if isinstance(func_type, CallableType): + func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info) + - if _attrs_get_decorator_bool_argument(ctx, 'init', True): - # Generate the __init__ method. - adder.add_method( - '__init__', - [attribute.argument() for attribute in attributes if attribute.init], - NoneTyp() - ) - - for stmt in ctx.cls.defs.body: - # The type of classmethods will be wrong because it's based on the parent's __init__. - # Set it correctly. - if isinstance(stmt, Decorator) and stmt.func.is_class: - func_type = stmt.func.type - if isinstance(func_type, CallableType): - func_type.arg_types[0] = ctx.api.class_type(info) - - if _attrs_get_decorator_bool_argument(ctx, 'frozen', False): - # If the class is frozen then all the attributes need to be turned into properties. - for attribute in attributes: - node = info.names[attribute.name].node - assert isinstance(node, Var) - node.is_initialized_in_class = False - node.is_property = True - - if _attrs_get_decorator_bool_argument(ctx, 'cmp', True): - # For __ne__ and __eq__ the type is: - # def __ne__(self, other: object) -> bool - bool_type = ctx.api.named_type('__builtins__.bool') - object_type = ctx.api.named_type('__builtins__.object') - - args = [Argument(Var('other', object_type), object_type, None, ARG_POS)] - for method in ['__ne__', '__eq__']: - adder.add_method(method, args, bool_type) - - # For the rest we use: - # AT = TypeVar('AT') - # def __lt__(self: AT, other: AT) -> bool - # This way comparisons with subclasses will work correctly. - tvd = TypeVarDef('AT', 'AT', 1, [], object_type) - tvd_type = TypeVarType(tvd) - args = [Argument(Var('other', tvd_type), tvd_type, None, ARG_POS)] - for method in ['__lt__', '__le__', '__gt__', '__ge__']: - adder.add_method(method, args, bool_type, - self_type=tvd_type, tvd=tvd) - - -def _attrs_get_decorator_bool_argument( +def _get_decorator_bool_argument( ctx: 'mypy.plugin.ClassDefContext', name: str, default: bool) -> bool: @@ -304,15 +334,15 @@ def _attrs_get_decorator_bool_argument( This handles both @attr.s(...) and @attr.s """ if isinstance(ctx.reason, CallExpr): - return _attrs_get_bool_argument(ctx, ctx.reason, name, default) + return _get_bool_argument(ctx, ctx.reason, name, default) else: return default -def _attrs_get_bool_argument(ctx: 'mypy.plugin.ClassDefContext', expr: CallExpr, - name: str, default: bool) -> bool: +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 = _attrs_get_argument(expr, name) + attr_value = _get_argument(expr, name) if attr_value: ret = ctx.api.parse_bool(attr_value) if ret is None: @@ -322,7 +352,7 @@ def _attrs_get_bool_argument(ctx: 'mypy.plugin.ClassDefContext', expr: CallExpr, return default -def _attrs_get_argument(call: CallExpr, name: str) -> Optional[Expression]: +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. From cad44bb3dc44990364761b01a973e97bbb2c57af Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sun, 4 Feb 2018 13:06:08 -0800 Subject: [PATCH 74/81] Work around circular import --- mypy/plugin.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index c8d6e3e11341..5c7b2817cf49 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -4,9 +4,7 @@ from functools import partial from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict -from mypy.attrs_plugin import ( - attr_class_makers, attr_dataclass_makers, attr_class_maker_callback, Attribute -) +import mypy.attrs_plugin from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, TypeInfo @@ -264,7 +262,7 @@ class DefaultPlugin(Plugin): def __init__(self, options: Options) -> None: super().__init__(options) - self._attr_classes = {} # type: Dict[TypeInfo, List[Attribute]] + self._attr_classes = {} # type: Dict[TypeInfo, List[mypy.attrs_plugin.Attribute]] def get_function_hook(self, fullname: str ) -> Optional[Callable[[FunctionContext], Type]]: @@ -290,14 +288,14 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - if fullname in attr_class_makers: + if fullname in mypy.attrs_plugin.attr_class_makers: return partial( - attr_class_maker_callback, + mypy.attrs_plugin.attr_class_maker_callback, self._attr_classes ) - elif fullname in attr_dataclass_makers: + elif fullname in mypy.attrs_plugin.attr_dataclass_makers: return partial( - attr_class_maker_callback, + mypy.attrs_plugin.attr_class_maker_callback, self._attr_classes, auto_attribs_default=True ) From 02400926091d4a356e3ac916be7d1312e34f7538 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sun, 4 Feb 2018 16:19:13 -0800 Subject: [PATCH 75/81] Support incremental mode with attrs --- mypy/attrs_plugin.py | 110 ++++++++++++++++++-------- mypy/nodes.py | 7 ++ mypy/plugin.py | 18 ++--- test-data/unit/check-attr.test | 32 +++++++- test-data/unit/check-incremental.test | 62 +++++++++++++++ 5 files changed, 181 insertions(+), 48 deletions(-) diff --git a/mypy/attrs_plugin.py b/mypy/attrs_plugin.py index cd7ddfac981b..b532120d99af 100644 --- a/mypy/attrs_plugin.py +++ b/mypy/attrs_plugin.py @@ -8,7 +8,7 @@ Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt, TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncBase, is_class_var, TempNode, Decorator, MemberExpr, Expression, FuncDef, Block, - PassStmt, SymbolTableNode, MDEF + PassStmt, SymbolTableNode, MDEF, JsonDict ) from mypy.types import ( Type, AnyType, TypeOfAny, CallableType, NoneTyp, TypeVarDef, TypeVarType, @@ -36,30 +36,63 @@ class Attribute: """The value of an attr.ib() call.""" - def __init__(self, name: str, type: Optional[Type], - has_default: bool, init: bool, + def __init__(self, name: str, init_type: Optional[Type], + has_default: bool, init: bool, converter_name: Optional[str], context: Context) -> None: self.name = name - self.type = type + self.init_type = init_type self.has_default = has_default self.init = init + self.converter_name = converter_name self.context = context def argument(self) -> Argument: """Return this attribute as an argument to __init__.""" # Convert type not set to Any. - _type = self.type or AnyType(TypeOfAny.unannotated) + init_type = self.init_type or AnyType(TypeOfAny.unannotated) # Attrs removes leading underscores when creating the __init__ arguments. - return Argument(Var(self.name.lstrip("_"), _type), _type, + return Argument(Var(self.name.lstrip("_"), init_type), init_type, None, ARG_OPT if self.has_default else ARG_POS) - -def attr_class_maker_callback( - attr_classes: Dict[TypeInfo, List[Attribute]], - ctx: 'mypy.plugin.ClassDefContext', - auto_attribs_default: bool = False -) -> None: + def serialize(self) -> JsonDict: + """Serialize this object so it can be saved and restored.""" + return { + 'name': self.name, + 'has_default': self.has_default, + 'init': self.init, + 'converter_name': self.converter_name, + 'context_line': self.context.line, + 'context_column': self.context.column, + } + + @classmethod + def deserialize(cls, ctx: 'mypy.plugin.ClassDefContext', + info: TypeInfo, data: JsonDict) -> 'Attribute': + """Return the Attribute that was serialized.""" + attrib = info.get(data['name']) + assert attrib is not None + init_type = attrib.type + if data['converter_name']: + # When a converter is set the init_type was overriden. + converter = ctx.api.lookup_fully_qualified(data['converter_name']) + if (converter + and converter.type + and isinstance(converter.type, CallableType) + and converter.type.arg_types): + init_type = converter.type.arg_types[0] + return Attribute( + data['name'], + init_type, + data['has_default'], + data['init'], + data['converter_name'], + Context(line=data['context_line'], column=data['context_column']) + ) + + +def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', + auto_attribs_default: bool = False) -> None: """Add necessary dunder methods to classes decorated with attr.s. attrs is a package that lets you define classes without writing dull boilerplate code. @@ -153,18 +186,18 @@ def attr_class_maker_callback( # Traverse the MRO and collect attributes from the parents. for super_info in info.mro[1:-1]: - if super_info in attr_classes: - for a in attr_classes[super_info]: + if 'attrs' in super_info.metadata: + for data in super_info.metadata['attrs']['attributes']: # Only add an attribute if it hasn't been defined before. This # allows for overwriting attribute definitions by subclassing. - if a.name not in taken_attr_names: + if data['name'] not in taken_attr_names: + a = Attribute.deserialize(ctx, super_info, data) super_attrs.append(a) taken_attr_names.add(a.name) attributes = super_attrs + list(own_attrs.values()) # Save the attributes so that subclasses can reuse them. - # TODO: This doesn't work with incremental mode if the parent class is in a different file. - attr_classes[info] = attributes + info.metadata['attrs'] = {'attributes': [attr.serialize() for attr in attributes]} # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. @@ -193,7 +226,7 @@ def _attribute_from_auto_attrib(lhs: NameExpr, """Return an Attribute for a new type assignment.""" # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(rvalue, TempNode) - return Attribute(lhs.name, stmt.type, has_rhs, True, stmt) + return Attribute(lhs.name, stmt.type, has_rhs, True, None, stmt) def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', @@ -212,7 +245,8 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', ctx.api.fail("Too many names for one attribute", stmt) return None - typ = stmt.type + # This is the type that belongs in the __init__ method for this attrib. + init_type = stmt.type # Read all the arguments from the call. init = _get_bool_argument(ctx, rvalue, 'init', True) @@ -221,16 +255,16 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', # If the type isn't set through annotation but is passed through `type=` use that. type_arg = _get_argument(rvalue, 'type') - if type_arg and not typ: + if type_arg and not init_type: try: un_type = expr_to_unanalyzed_type(type_arg) except TypeTranslationError: ctx.api.fail('Invalid argument to type', type_arg) else: - typ = ctx.api.anal_type(un_type) - if typ and isinstance(lhs.node, Var) and not lhs.node.type: + init_type = ctx.api.anal_type(un_type) + if init_type and isinstance(lhs.node, Var) and not lhs.node.type: # If there is no annotation, add one. - lhs.node.type = typ + lhs.node.type = init_type lhs.is_inferred_def = False # If the attrib has a converter function take the type of the first argument as the init type. @@ -242,16 +276,12 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', elif convert: ctx.api.fail("convert is deprecated, use converter", rvalue) converter = convert - if (converter - and isinstance(converter, RefExpr) - and converter.node - and isinstance(converter.node, FuncBase) - and converter.node.type - and isinstance(converter.node.type, CallableType) - and converter.node.type.arg_types): - typ = converter.node.type.arg_types[0] + converter_name, converter_first_arg_type = get_converter_name_and_type(converter) + if converter_first_arg_type: + # When there is a converter set, use the first arg as the type for the init. + init_type = converter_first_arg_type - if ctx.api.options.disallow_untyped_defs and not typ: + if ctx.api.options.disallow_untyped_defs and not init_type: # This is a compromise. If you don't have a type here then the # __init__ will be untyped. But since the __init__ is added it's # pointing at the decorator. So instead we also show the error in the @@ -259,7 +289,21 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', assert lhs.node is not None ctx.api.msg.need_annotation_for_var(lhs.node, stmt) - return Attribute(lhs.name, typ, attr_has_default, init, stmt) + return Attribute(lhs.name, init_type, attr_has_default, init, converter_name, stmt) + + +def get_converter_name_and_type(converter: Optional[Expression] + ) -> Tuple[Optional[str], Optional[Type]]: + """Extract the name of the converter and the type of the first argument.""" + if (converter + and isinstance(converter, RefExpr) + and converter.node + and isinstance(converter.node, FuncBase) + and converter.node.type + and isinstance(converter.node.type, CallableType) + and converter.node.type.arg_types): + return converter.node.fullname(), converter.node.type.arg_types[0] + return None, None def _parse_assignments( diff --git a/mypy/nodes.py b/mypy/nodes.py index 0c8b3633b818..2ad33f97aff8 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1988,6 +1988,10 @@ class is generic then it will be a type constructor of higher kind. # needed during the semantic passes.) replaced = None # type: TypeInfo + # This is a dictionary that will be serialized and un-serialized as is. + # It is useful for plugins to add their data to save in the cache. + metadata = None # type: Dict[str, JsonDict] + FLAGS = [ 'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple', 'is_newtype', 'is_protocol', 'runtime_protocol' @@ -2011,6 +2015,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No self._cache = set() self._cache_proper = set() self.add_type_vars() + self.metadata = {} def add_type_vars(self) -> None: if self.defn.type_vars: @@ -2213,6 +2218,7 @@ def serialize(self) -> JsonDict: 'typeddict_type': None if self.typeddict_type is None else self.typeddict_type.serialize(), 'flags': get_flags(self, TypeInfo.FLAGS), + 'metadata': self.metadata, } return data @@ -2239,6 +2245,7 @@ def deserialize(cls, data: JsonDict) -> 'TypeInfo': else mypy.types.TupleType.deserialize(data['tuple_type'])) ti.typeddict_type = (None if data['typeddict_type'] is None else mypy.types.TypedDictType.deserialize(data['typeddict_type'])) + ti.metadata = data['metadata'] set_flags(ti, data['flags']) return ti diff --git a/mypy/plugin.py b/mypy/plugin.py index 5c7b2817cf49..7dc816fb66a3 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -2,12 +2,12 @@ from abc import abstractmethod from functools import partial -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar import mypy.attrs_plugin from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, - TypeInfo + TypeInfo, SymbolTableNode ) from mypy.tvar_scope import TypeVarScope from mypy.types import ( @@ -89,6 +89,10 @@ def anal_type(self, t: Type, *, def class_type(self, info: TypeInfo) -> Type: raise NotImplementedError + @abstractmethod + def lookup_fully_qualified(self, name: str) -> SymbolTableNode: + raise NotImplementedError + # A context for a function hook that infers the return type of a function with # a special signature. @@ -260,10 +264,6 @@ def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: class DefaultPlugin(Plugin): """Type checker plugin that is enabled by default.""" - def __init__(self, options: Options) -> None: - super().__init__(options) - self._attr_classes = {} # type: Dict[TypeInfo, List[mypy.attrs_plugin.Attribute]] - def get_function_hook(self, fullname: str ) -> Optional[Callable[[FunctionContext], Type]]: if fullname == 'contextlib.contextmanager': @@ -289,14 +289,10 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: if fullname in mypy.attrs_plugin.attr_class_makers: - return partial( - mypy.attrs_plugin.attr_class_maker_callback, - self._attr_classes - ) + return mypy.attrs_plugin.attr_class_maker_callback elif fullname in mypy.attrs_plugin.attr_dataclass_makers: return partial( mypy.attrs_plugin.attr_class_maker_callback, - self._attr_classes, auto_attribs_default=True ) return None diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index da8e28099e0e..1537c6042c9b 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -502,17 +502,22 @@ reveal_type(C(15).x) # E: Revealed type is 'builtins.str' [case testAttrsUsingConverter] import attr +import helper -def converter(s:int) -> str: +def converter2(s:int) -> str: return 'hello' @attr.s class C: - x: str = attr.ib(converter=converter) + x: str = attr.ib(converter=helper.converter) + y: str = attr.ib(converter=converter2) # Because of the converter the __init__ takes an int, but the variable is a str. -reveal_type(C) # E: Revealed type is 'def (x: builtins.int) -> __main__.C' -reveal_type(C(15).x) # E: Revealed type is 'builtins.str' +reveal_type(C) # E: Revealed type is 'def (x: builtins.int, y: builtins.int) -> __main__.C' +reveal_type(C(15, 16).x) # E: Revealed type is 'builtins.str' +[file helper.py] +def converter(s:int) -> str: + return 'hello' [builtins fixtures/list.pyi] [case testAttrsUsingConvertAndConverter] @@ -528,6 +533,25 @@ class C: [builtins fixtures/list.pyi] +[case testAttrsUsingConverterAndSubclass] +import attr + +def converter(s:int) -> str: + return 'hello' + +@attr.s +class C: + x: str = attr.ib(converter=converter) + +@attr.s +class A(C): + pass + +# Because of the convert the __init__ takes an int, but the variable is a str. +reveal_type(A) # E: Revealed type is 'def (x: builtins.int) -> __main__.A' +reveal_type(A(15).x) # E: Revealed type is 'builtins.str' +[builtins fixtures/list.pyi] + [case testAttrsCmpWithSubclasses] import attr @attr.s diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 653db572564c..21782efca454 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -3466,3 +3466,65 @@ tmp/main.py:2: error: Expression has type "Any" [out2] tmp/main.py:2: error: Expression has type "Any" + +[case testAttrsIncrementalSubclassingCached] +from a import A +import attr +@attr.s +class B(A): + pass +reveal_type(B) +[file a.py] +import attr +@attr.s +class A: + x: int = attr.ib() + +[out1] +main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +[out2] +main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' + +[builtins fixtures/list.pyi] + +[case testAttrsIncrementalSubclassingCachedConverter] +from a import A +import attr +@attr.s +class B(A): + pass +reveal_type(B) +[file a.py] +def converter(s:int) -> str: + return 'hello' + +import attr +@attr.s +class A: + x: str = attr.ib(converter=converter) + +[out1] +main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +[out2] +main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +[builtins fixtures/list.pyi] + +[case testAttrsIncrementalSubclassingCachedType] +from a import A +import attr +@attr.s +class B(A): + pass +reveal_type(B) +[file a.py] +import attr +@attr.s +class A: + x = attr.ib(type=int) + +[out1] +main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +[out2] +main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' + +[builtins fixtures/list.pyi] From c37147d08b2ba2b832311f344bad8ce048150c01 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 5 Feb 2018 10:49:41 -0800 Subject: [PATCH 76/81] A little more refactoring --- mypy/attrs_plugin.py | 169 ++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 75 deletions(-) diff --git a/mypy/attrs_plugin.py b/mypy/attrs_plugin.py index b532120d99af..4b9ca57caa3e 100644 --- a/mypy/attrs_plugin.py +++ b/mypy/attrs_plugin.py @@ -1,6 +1,6 @@ """Plugin for supporting the attrs library (http://www.attrs.org)""" from collections import OrderedDict -from typing import Optional, Dict, List, cast, Tuple +from typing import Optional, Dict, List, cast, Tuple, Iterable import mypy.plugin # To avoid circular imports. from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError @@ -106,7 +106,9 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', """ info = ctx.cls.info - # auto_attribs means we also generate Attributes from annotated variables. + init = _get_decorator_bool_argument(ctx, 'init', True) + frozen = _get_decorator_bool_argument(ctx, 'frozen', False) + cmp = _get_decorator_bool_argument(ctx, 'cmp', True) auto_attribs = _get_decorator_bool_argument(ctx, 'auto_attribs', auto_attribs_default) if ctx.api.options.python_version[0] < 3: @@ -114,78 +116,42 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) return if not info.defn.base_type_exprs: - # Note: This does not catch subclassing old-style classes. + # Note: This will not catch subclassing old-style classes. ctx.api.fail("attrs only works with new-style classes", info.defn) return - # First, walk the body looking for attribute definitions. - # They will look like this: - # x = attr.ib() - # x = y = attr.ib() - # x, y = attr.ib(), attr.ib() - # or if auto_attribs is enabled also like this: - # x: type - # x: type = default_value + attributes = _analyze_class(ctx, auto_attribs) + + adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) + if init: + _add_init(ctx, attributes, adder) + if cmp: + _add_cmp(ctx, adder) + if frozen: + _make_frozen(ctx, attributes) + + +def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', auto_attribs: bool) -> List[Attribute]: + """Analyze the class body of an attr maker, its parents, and return the Attributes found.""" own_attrs = OrderedDict() # type: OrderedDict[str, Attribute] + # Walk the body looking for assignments and decorators. for stmt in ctx.cls.defs.body: if isinstance(stmt, AssignmentStmt): - for lvalue in stmt.lvalues: - lvalues, rvalues = _parse_assignments(lvalue, stmt) - - if len(lvalues) != len(rvalues): - # This means we have some assignment that isn't 1 to 1. - # It can't be an attrib. - continue - - for lhs, rvalue in zip(lvalues, rvalues): - # Check if the right hand side is a call to an attribute maker. - if (isinstance(rvalue, CallExpr) - and isinstance(rvalue.callee, RefExpr) - and rvalue.callee.fullname in attr_attrib_makers): - attr = _attribute_from_attrib_maker(ctx, auto_attribs, lhs, - rvalue, stmt) - if attr: - # When attrs are defined twice in the same body we want to use - # the 2nd definition in the 2nd location. So remove it from the - # OrderedDict. auto_attribs doesn't work that way. - if not auto_attribs and attr.name in own_attrs: - del own_attrs[attr.name] - own_attrs[attr.name] = attr - elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): - attr_auto = _attribute_from_auto_attrib(lhs, rvalue, stmt) - own_attrs[attr_auto.name] = attr_auto - + for attr in _attributes_from_assignment(ctx, stmt, auto_attribs): + # When attrs are defined twice in the same body we want to use the 2nd definition + # in the 2nd location. So remove it from the OrderedDict. + # Unless it's auto_attribs in which case we want the 2nd definition in the + # 1st location. + if not auto_attribs and attr.name in own_attrs: + del own_attrs[attr.name] + own_attrs[attr.name] = attr elif isinstance(stmt, Decorator): - # Look for attr specific decorators. ('x.default' and 'x.validator') - remove_me = [] - for func_decorator in stmt.decorators: - if (isinstance(func_decorator, MemberExpr) - and isinstance(func_decorator.expr, NameExpr) - and func_decorator.expr.name in own_attrs): - - if func_decorator.name == 'default': - # This decorator lets you set a default after the fact. - own_attrs[func_decorator.expr.name].has_default = True - - if func_decorator.name in ('default', 'validator'): - # These are decorators on the attrib object that only exist during - # class creation time. In order to not trigger a type error later we - # just remove them. This might leave us with a Decorator with no - # decorators (Emperor's new clothes?) - # TODO: It would be nice to type-check these rather than remove them. - # default should be Callable[[], T] - # validator should be Callable[[Any, 'Attribute', T], Any] - # where T is the type of the attribute. - remove_me.append(func_decorator) - - for dec in remove_me: - stmt.decorators.remove(dec) + _cleanup_decorator(stmt, own_attrs) + # Traverse the MRO and collect attributes from the parents. taken_attr_names = set(own_attrs) super_attrs = [] - - # Traverse the MRO and collect attributes from the parents. - for super_info in info.mro[1:-1]: + for super_info in ctx.cls.info.mro[1:-1]: if 'attrs' in super_info.metadata: for data in super_info.metadata['attrs']['attributes']: # Only add an attribute if it hasn't been defined before. This @@ -194,30 +160,83 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', a = Attribute.deserialize(ctx, super_info, data) super_attrs.append(a) taken_attr_names.add(a.name) - attributes = super_attrs + list(own_attrs.values()) + # Save the attributes so that subclasses can reuse them. - info.metadata['attrs'] = {'attributes': [attr.serialize() for attr in attributes]} + ctx.cls.info.metadata['attrs'] = {'attributes': [attr.serialize() for attr in attributes]} # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. last_default = False for attribute in attributes: - if not attribute.has_default and last_default: + if attribute.init and not attribute.has_default and last_default: ctx.api.fail( "Non-default attributes not allowed after default attributes.", attribute.context) last_default = attribute.has_default - adder = MethodAdder(info, ctx.api.named_type('__builtins__.function')) - if _get_decorator_bool_argument(ctx, 'init', True): - _add_init(ctx, attributes, adder) + return attributes - if _get_decorator_bool_argument(ctx, 'frozen', False): - _make_frozen(ctx, attributes) - if _get_decorator_bool_argument(ctx, 'cmp', True): - _make_cmp(ctx, adder) +def _attributes_from_assignment(ctx: 'mypy.plugin.ClassDefContext', + stmt: AssignmentStmt, auto_attribs: bool) -> Iterable[Attribute]: + """Return Attribute objects that are created by this assignment. + + The assignments can look like this: + x = attr.ib() + x = y = attr.ib() + x, y = attr.ib(), attr.ib() + or if auto_attribs is enabled also like this: + x: type + x: type = default_value + """ + for lvalue in stmt.lvalues: + lvalues, rvalues = _parse_assignments(lvalue, stmt) + + if len(lvalues) != len(rvalues): + # This means we have some assignment that isn't 1 to 1. + # It can't be an attrib. + continue + + for lhs, rvalue in zip(lvalues, rvalues): + # Check if the right hand side is a call to an attribute maker. + if (isinstance(rvalue, CallExpr) + and isinstance(rvalue.callee, RefExpr) + and rvalue.callee.fullname in attr_attrib_makers): + attr = _attribute_from_attrib_maker(ctx, auto_attribs, lhs, rvalue, stmt) + if attr: + yield attr + elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): + yield _attribute_from_auto_attrib(lhs, rvalue, stmt) + + +def _cleanup_decorator(stmt: Decorator, attr_map: Dict[str, Attribute]) -> None: + """Handle decorators in class bodies. + + `x.default` will set a default value on x + `x.validator` and `x.default` will get removed to avoid throwing a type error. + """ + remove_me = [] + for func_decorator in stmt.decorators: + if (isinstance(func_decorator, MemberExpr) + and isinstance(func_decorator.expr, NameExpr) + and func_decorator.expr.name in attr_map): + + if func_decorator.name == 'default': + attr_map[func_decorator.expr.name].has_default = True + + if func_decorator.name in ('default', 'validator'): + # These are decorators on the attrib object that only exist during + # class creation time. In order to not trigger a type error later we + # just remove them. This might leave us with a Decorator with no + # decorators (Emperor's new clothes?) + # TODO: It would be nice to type-check these rather than remove them. + # default should be Callable[[], T] + # validator should be Callable[[Any, 'Attribute', T], Any] + # where T is the type of the attribute. + remove_me.append(func_decorator) + for dec in remove_me: + stmt.decorators.remove(dec) def _attribute_from_auto_attrib(lhs: NameExpr, @@ -323,7 +342,7 @@ def _parse_assignments( return lvalues, rvalues -def _make_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None: +def _add_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None: """Generate all the cmp methods for this class.""" # For __ne__ and __eq__ the type is: # def __ne__(self, other: object) -> bool From cb795cd7bc78afd2e0ebde31743374738b15a9ac Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 5 Feb 2018 11:27:29 -0800 Subject: [PATCH 77/81] Handle converter in one place --- mypy/attrs_plugin.py | 89 +++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/mypy/attrs_plugin.py b/mypy/attrs_plugin.py index 4b9ca57caa3e..b990e03cd639 100644 --- a/mypy/attrs_plugin.py +++ b/mypy/attrs_plugin.py @@ -36,20 +36,46 @@ class Attribute: """The value of an attr.ib() call.""" - def __init__(self, name: str, init_type: Optional[Type], + def __init__(self, name: str, info: TypeInfo, has_default: bool, init: bool, converter_name: Optional[str], context: Context) -> None: self.name = name - self.init_type = init_type + self.info = info self.has_default = has_default self.init = init self.converter_name = converter_name self.context = context - def argument(self) -> Argument: + def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: """Return this attribute as an argument to __init__.""" - # Convert type not set to Any. - init_type = self.init_type or AnyType(TypeOfAny.unannotated) + assert self.init + init_type = self.info[self.name].type + + if self.converter_name: + # When a converter is set the init_type is overriden by the first argument + # of the converter method. + converter = ctx.api.lookup_fully_qualified(self.converter_name) + if (converter + and converter.type + and isinstance(converter.type, CallableType) + and converter.type.arg_types): + init_type = converter.type.arg_types[0] + else: + init_type = None + + if init_type is None: + if ctx.api.options.disallow_untyped_defs: + # This is a compromise. If you don't have a type here then the + # __init__ will be untyped. But since the __init__ is added it's + # pointing at the decorator. So instead we also show the error in the + # assignment, which is where you would fix the issue. + node = self.info[self.name].node + assert node is not None + ctx.api.msg.need_annotation_for_var(node, self.context) + + # Convert type not set to Any. + init_type = AnyType(TypeOfAny.unannotated) + # Attrs removes leading underscores when creating the __init__ arguments. return Argument(Var(self.name.lstrip("_"), init_type), init_type, None, @@ -67,23 +93,11 @@ def serialize(self) -> JsonDict: } @classmethod - def deserialize(cls, ctx: 'mypy.plugin.ClassDefContext', - info: TypeInfo, data: JsonDict) -> 'Attribute': + def deserialize(cls, info: TypeInfo, data: JsonDict) -> 'Attribute': """Return the Attribute that was serialized.""" - attrib = info.get(data['name']) - assert attrib is not None - init_type = attrib.type - if data['converter_name']: - # When a converter is set the init_type was overriden. - converter = ctx.api.lookup_fully_qualified(data['converter_name']) - if (converter - and converter.type - and isinstance(converter.type, CallableType) - and converter.type.arg_types): - init_type = converter.type.arg_types[0] return Attribute( data['name'], - init_type, + info, data['has_default'], data['init'], data['converter_name'], @@ -157,7 +171,7 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', auto_attribs: bool) -> Li # Only add an attribute if it hasn't been defined before. This # allows for overwriting attribute definitions by subclassing. if data['name'] not in taken_attr_names: - a = Attribute.deserialize(ctx, super_info, data) + a = Attribute.deserialize(super_info, data) super_attrs.append(a) taken_attr_names.add(a.name) attributes = super_attrs + list(own_attrs.values()) @@ -207,7 +221,7 @@ def _attributes_from_assignment(ctx: 'mypy.plugin.ClassDefContext', if attr: yield attr elif auto_attribs and stmt.type and stmt.new_syntax and not is_class_var(lhs): - yield _attribute_from_auto_attrib(lhs, rvalue, stmt) + yield _attribute_from_auto_attrib(ctx, lhs, rvalue, stmt) def _cleanup_decorator(stmt: Decorator, attr_map: Dict[str, Attribute]) -> None: @@ -239,13 +253,14 @@ def _cleanup_decorator(stmt: Decorator, attr_map: Dict[str, Attribute]) -> None: stmt.decorators.remove(dec) -def _attribute_from_auto_attrib(lhs: NameExpr, +def _attribute_from_auto_attrib(ctx: 'mypy.plugin.ClassDefContext', + lhs: NameExpr, rvalue: Expression, stmt: AssignmentStmt) -> Attribute: """Return an Attribute for a new type assignment.""" # `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) has_rhs = not isinstance(rvalue, TempNode) - return Attribute(lhs.name, stmt.type, has_rhs, True, None, stmt) + return Attribute(lhs.name, ctx.cls.info, has_rhs, True, None, stmt) def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', @@ -286,7 +301,6 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', lhs.node.type = init_type lhs.is_inferred_def = False - # If the attrib has a converter function take the type of the first argument as the init type. # Note: convert is deprecated but works the same as converter. converter = _get_argument(rvalue, 'converter') convert = _get_argument(rvalue, 'convert') @@ -295,25 +309,14 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext', elif convert: ctx.api.fail("convert is deprecated, use converter", rvalue) converter = convert - converter_name, converter_first_arg_type = get_converter_name_and_type(converter) - if converter_first_arg_type: - # When there is a converter set, use the first arg as the type for the init. - init_type = converter_first_arg_type - - if ctx.api.options.disallow_untyped_defs and not init_type: - # This is a compromise. If you don't have a type here then the - # __init__ will be untyped. But since the __init__ is added it's - # pointing at the decorator. So instead we also show the error in the - # assignment, which is where you would fix the issue. - assert lhs.node is not None - ctx.api.msg.need_annotation_for_var(lhs.node, stmt) + converter_name = _get_converter_name(converter) - return Attribute(lhs.name, init_type, attr_has_default, init, converter_name, stmt) + return Attribute(lhs.name, ctx.cls.info, attr_has_default, init, converter_name, stmt) -def get_converter_name_and_type(converter: Optional[Expression] - ) -> Tuple[Optional[str], Optional[Type]]: - """Extract the name of the converter and the type of the first argument.""" +def _get_converter_name(converter: Optional[Expression]) -> Optional[str]: + """Return the full name of the converter if it exists and is a simple function.""" + # TODO: Support complex converters, e.g. lambdas, calls, etc. if (converter and isinstance(converter, RefExpr) and converter.node @@ -321,8 +324,8 @@ def get_converter_name_and_type(converter: Optional[Expression] and converter.node.type and isinstance(converter.node.type, CallableType) and converter.node.type.arg_types): - return converter.node.fullname(), converter.node.type.arg_types[0] - return None, None + return converter.node.fullname() + return None def _parse_assignments( @@ -376,7 +379,7 @@ def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute], """Generate an __init__ method for the attributes and add it to the class.""" adder.add_method( '__init__', - [attribute.argument() for attribute in attributes if attribute.init], + [attribute.argument(ctx) for attribute in attributes if attribute.init], NoneTyp() ) for stmt in ctx.cls.defs.body: From d1e3e0be35f4fb94fe56fb3043a3ce589824318b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 8 Feb 2018 06:42:22 -0800 Subject: [PATCH 78/81] Sync typeshed --- typeshed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typeshed b/typeshed index c2fa0a153a6b..1713ad64de71 160000 --- a/typeshed +++ b/typeshed @@ -1 +1 @@ -Subproject commit c2fa0a153a6bb83881c7abca6d57af43df605d3d +Subproject commit 1713ad64de7190ef625f7c895d41241af93e6544 From feab654915d9d647938d0af236e71d11b4772594 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sat, 10 Feb 2018 13:59:24 -0800 Subject: [PATCH 79/81] Fix test --- test-data/unit/check-attr.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 1537c6042c9b..7062115876ec 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -151,7 +151,7 @@ class D: [case testAttrsNotBooleans] import attr x = True -@attr.s(cmp=1) # E: "cmp" argument must be True or False. +@attr.s(cmp=x) # E: "cmp" argument must be True or False. class A: a = attr.ib(init=x) # E: "init" argument must be True or False. [builtins fixtures/bool.pyi] From 641230d3d32bbc0997bb0a792fd7bd8a0b53226f Mon Sep 17 00:00:00 2001 From: David Euresti Date: Sun, 11 Feb 2018 15:10:02 -0800 Subject: [PATCH 80/81] Move attrs_plugin to plugins/attrs --- mypy/plugin.py | 10 +++++----- mypy/plugins/__init__.py | 0 mypy/{attrs_plugin.py => plugins/attrs.py} | 0 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 mypy/plugins/__init__.py rename mypy/{attrs_plugin.py => plugins/attrs.py} (100%) diff --git a/mypy/plugin.py b/mypy/plugin.py index 7dc816fb66a3..3e1e2bea3011 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -4,7 +4,7 @@ from functools import partial from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar -import mypy.attrs_plugin +import mypy.plugins.attrs from mypy.nodes import ( Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef, TypeInfo, SymbolTableNode @@ -288,11 +288,11 @@ def get_method_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - if fullname in mypy.attrs_plugin.attr_class_makers: - return mypy.attrs_plugin.attr_class_maker_callback - elif fullname in mypy.attrs_plugin.attr_dataclass_makers: + 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: return partial( - mypy.attrs_plugin.attr_class_maker_callback, + mypy.plugins.attrs.attr_class_maker_callback, auto_attribs_default=True ) return None diff --git a/mypy/plugins/__init__.py b/mypy/plugins/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/mypy/attrs_plugin.py b/mypy/plugins/attrs.py similarity index 100% rename from mypy/attrs_plugin.py rename to mypy/plugins/attrs.py From bf49a874224f480f55d2b277fdbcce4299eb523a Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 12 Feb 2018 22:46:47 -0800 Subject: [PATCH 81/81] Add more incremental tests --- mypy/plugins/attrs.py | 1 + test-data/unit/check-incremental.test | 251 +++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 10 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index b990e03cd639..5ba4cd2c236b 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -367,6 +367,7 @@ def _add_cmp(ctx: 'mypy.plugin.ClassDefContext', adder: 'MethodAdder') -> None: def _make_frozen(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute]) -> None: """Turn all the attributes into properties to simulate frozen classes.""" + # TODO: Handle subclasses of frozen classes. for attribute in attributes: node = ctx.cls.info.names[attribute.name].node assert isinstance(node, Var) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 8af1ee3d8f61..60537eeef2ba 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -3470,22 +3470,32 @@ tmp/main.py:2: error: Expression has type "Any" [case testAttrsIncrementalSubclassingCached] from a import A import attr -@attr.s +@attr.s(auto_attribs=True) class B(A): - pass -reveal_type(B) + 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] import attr -@attr.s +import attr +from typing import List, ClassVar +@attr.s(auto_attribs=True) class A: - x: int = attr.ib() + a: int + _b: List[int] + c: str = '18' + _d: int = attr.ib(validator=None, default=18) + E = 7 + F: ClassVar[int] = 22 +[builtins fixtures/list.pyi] [out1] -main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' [out2] -main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' - -[builtins fixtures/list.pyi] [case testAttrsIncrementalSubclassingCachedConverter] from a import A @@ -3494,6 +3504,7 @@ import attr class B(A): pass reveal_type(B) + [file a.py] def converter(s:int) -> str: return 'hello' @@ -3503,11 +3514,12 @@ import attr class A: x: str = attr.ib(converter=converter) +[builtins fixtures/list.pyi] [out1] main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' + [out2] main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' -[builtins fixtures/list.pyi] [case testAttrsIncrementalSubclassingCachedType] from a import A @@ -3516,18 +3528,237 @@ import attr class B(A): pass reveal_type(B) + [file a.py] import attr @attr.s class A: x = attr.ib(type=int) +[builtins fixtures/list.pyi] [out1] main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' [out2] main:6: error: Revealed type is 'def (x: builtins.int) -> __main__.B' +[case testAttrsIncrementalArguments] +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] +import attr +@attr.s(frozen=True) +class Frozen: + x: int = attr.ib() +@attr.s(init=False) +class NoInit: + x: int = attr.ib() +@attr.s(cmp=False) +class NoCmp: + x: int = attr.ib() + [builtins fixtures/list.pyi] +[rechecked] +[stale] +[out1] +main:3: error: Property "x" defined in "Frozen" is read-only +main:12: error: Unsupported left operand type for < ("NoCmp") +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") + +[out2] +main:3: error: Property "x" defined in "Frozen" is read-only +main:12: error: Unsupported left operand type for < ("NoCmp") +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") + +[case testAttrsIncrementalDunder] +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 (self: a.A, other: builtins.object) -> builtins.bool' +reveal_type(A.__ne__) # E: Revealed type is 'def (self: a.A, other: builtins.object) -> builtins.bool' +reveal_type(A.__lt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(A.__le__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(A.__gt__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +reveal_type(A.__ge__) # E: Revealed type is 'def [AT] (self: AT`1, other: AT`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 attr import attrib, attrs +@attrs(auto_attribs=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 (self: a.A, other: builtins.object) -> builtins.bool' +main:4: error: Revealed type is 'def (self: a.A, other: builtins.object) -> builtins.bool' +main:5: error: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +main:6: error: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +main:7: error: Revealed type is 'def [AT] (self: AT`1, other: AT`1) -> builtins.bool' +main:8: error: Revealed type is 'def [AT] (self: AT`1, other: AT`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 testAttrsIncrementalSubclassModified] +from b import B +B(5, 'foo') + +[file a.py] +import attr +@attr.s(auto_attribs=True) +class A: + x: int + +[file b.py] +import attr +from a import A +@attr.s(auto_attribs=True) +class B(A): + y: str + +[file b.py.2] +import attr +from a import A +@attr.s(auto_attribs=True) +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 testAttrsIncrementalSubclassModifiedErrorFirst] +from b import B +B(5, 'foo') +[file a.py] +import attr +@attr.s(auto_attribs=True) +class A: + x: int + +[file b.py] +import attr +from a import A +@attr.s(auto_attribs=True) +class B(A): + y: int + +[file b.py.2] +import attr +from a import A +@attr.s(auto_attribs=True) +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 testAttrsIncrementalThreeFiles] +from c import C +C(5, 'foo', True) + +[file a.py] +import attr +@attr.s +class A: + a: int = attr.ib() + +[file b.py] +import attr +@attr.s +class B: + b: str = attr.ib() + +[file c.py] +from a import A +from b import B +import attr +@attr.s +class C(A, B): + c: bool = attr.ib() + +[builtins fixtures/list.pyi] +[out1] +[out2] + +[case testAttrsIncrementalThreeRuns] +from a import A +A(5) + +[file a.py] +import attr +@attr.s(auto_attribs=True) +class A: + a: int + +[file a.py.2] +import attr +@attr.s(auto_attribs=True) +class A: + a: str + +[file a.py.3] +import attr +@attr.s(auto_attribs=True) +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] [case testDeletedDepLineNumber] # The import is not on line 1 and that data should be preserved