From b84d8916f74f875ead2b3444258ac7abd98ec1db Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 4 Dec 2017 10:00:22 -0800 Subject: [PATCH 01/13] RFC: Add Class Decorator/Metaclass/Base Class plugin Also use the plugin to demo adding __init__ to attr.s Helps #2088 --- mypy/plugin.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++-- mypy/semanal.py | 29 ++++++++++++++- 2 files changed, 121 insertions(+), 6 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 27917a6216f5..399ad4f3c9b3 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,12 +1,13 @@ """Plugin system for extending mypy.""" -from collections import OrderedDict from abc import abstractmethod from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar -from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr +from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ + DictExpr, TypeInfo, ClassDef, ARG_POS, ARG_OPT, Var, Argument, FuncDef, \ + Block, SymbolTableNode, MDEF 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 @@ -53,6 +54,14 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance: raise NotImplementedError +class SemanticAnalyzerPluginInterface: + """Interface for accessing semantic analyzer functionality in plugins.""" + + @abstractmethod + def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance: + raise NotImplementedError + + # A context for a function hook that infers the return type of a function with # a special signature. # @@ -98,6 +107,11 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance: ('context', Context), ('api', CheckerPluginInterface)]) +ClassDefContext = NamedTuple( + 'ClassDecoratorContext', [ + ('cls', ClassDef), + ('api', SemanticAnalyzerPluginInterface) + ]) class Plugin: """Base class of all type checker plugins. @@ -136,7 +150,17 @@ def get_attribute_hook(self, fullname: str ) -> Optional[Callable[[AttributeContext], Type]]: return None - # TODO: metaclass / class decorator hook + def get_class_decorator_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return None + + def get_class_metaclass_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return None + + def get_class_base_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return None T = TypeVar('T') @@ -182,6 +206,18 @@ def get_attribute_hook(self, fullname: str ) -> Optional[Callable[[AttributeContext], Type]]: return self._find_hook(lambda plugin: plugin.get_attribute_hook(fullname)) + def get_class_decorator_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return self._find_hook(lambda plugin: plugin.get_class_decorator_hook(fullname)) + + def get_class_metaclass_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return self._find_hook(lambda plugin: plugin.get_class_metaclass_hook(fullname)) + + def get_class_base_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return self._find_hook(lambda plugin: plugin.get_class_base_hook(fullname)) + def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: for plugin in self._plugins: hook = lookup(plugin) @@ -215,6 +251,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], Type]]: + if fullname == 'attr.s': + return attr_s_callback + def open_callback(ctx: FunctionContext) -> Type: """Infer a better return type for 'open'. @@ -332,3 +373,52 @@ 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.""" + info = ctx.cls.info + has_default = {} # TODO: Handle these. + args = [] + + for name, table in info.names.items(): + if 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 diff --git a/mypy/semanal.py b/mypy/semanal.py index e9d71eb4ecf3..399f27fcb16b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -81,7 +81,7 @@ from mypy.sametypes import is_same_type from mypy.options import Options from mypy import experiments -from mypy.plugin import Plugin +from mypy.plugin import Plugin, ClassDefContext, SemanticAnalyzerPluginInterface from mypy import join from mypy.util import get_prefix @@ -172,7 +172,7 @@ } -class SemanticAnalyzerPass2(NodeVisitor[None]): +class SemanticAnalyzerPass2(NodeVisitor[None], SemanticAnalyzerPluginInterface): """Semantically analyze parsed mypy files. The analyzer binds names and does various consistency checks for a @@ -720,6 +720,31 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: self.calculate_abstract_status(defn.info) self.setup_type_promotion(defn) + for decorator in defn.decorators: + if isinstance(decorator, CallExpr): + fullname = decorator.callee.fullname + else: + fullname = decorator.fullname + hook = self.plugin.get_class_decorator_hook(fullname) + if hook: + hook(ClassDefContext(defn, self)) + + if defn.metaclass: + metaclass_name = None + if isinstance(defn.metaclass, NameExpr): + metaclass_name = defn.metaclass.name + elif isinstance(defn.metaclass, MemberExpr): + metaclass_name = get_member_expr_fullname( + defn.metaclass) + hook = self.plugin.get_class_metaclass_hook(metaclass_name) + if hook: + hook(ClassDefContext(defn, self)) + + for type_info in defn.info.bases: + hook = self.plugin.get_class_base_hook(type_info.type.fullname()) + if hook: + hook(ClassDefContext(defn, self)) + self.leave_class() def analyze_class_keywords(self, defn: ClassDef) -> None: From c64cd485400dd955b4fba3c39f20078b691d220b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 5 Dec 2017 16:52:14 -0800 Subject: [PATCH 02/13] Fix types --- mypy/plugin.py | 7 ++++--- mypy/semanal.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 399ad4f3c9b3..af58bc28c094 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,7 +1,7 @@ """Plugin system for extending mypy.""" from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ DictExpr, TypeInfo, ClassDef, ARG_POS, ARG_OPT, Var, Argument, FuncDef, \ @@ -252,9 +252,10 @@ def get_method_hook(self, fullname: str return None def get_class_decorator_hook(self, fullname: str - ) -> Optional[Callable[[ClassDefContext], Type]]: + ) -> Optional[Callable[[ClassDefContext], None]]: if fullname == 'attr.s': return attr_s_callback + return None def open_callback(ctx: FunctionContext) -> Type: @@ -404,7 +405,7 @@ def add_method( def attr_s_callback(ctx: ClassDefContext) -> None: """Add an __init__ method to classes decorated with attr.s.""" info = ctx.cls.info - has_default = {} # TODO: Handle these. + has_default: Dict[str, Expression] = {} # TODO: Handle these. args = [] for name, table in info.names.items(): diff --git a/mypy/semanal.py b/mypy/semanal.py index 399f27fcb16b..e2ccd86cfc4a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -721,13 +721,17 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: self.setup_type_promotion(defn) for decorator in defn.decorators: + fullname: Optional[str] = None if isinstance(decorator, CallExpr): - fullname = decorator.callee.fullname - else: + if isinstance(decorator.callee, RefExpr): + fullname = decorator.callee.fullname + elif isinstance(decorator, NameExpr): fullname = decorator.fullname - hook = self.plugin.get_class_decorator_hook(fullname) - if hook: - hook(ClassDefContext(defn, self)) + + if fullname: + hook = self.plugin.get_class_decorator_hook(fullname) + if hook: + hook(ClassDefContext(defn, self)) if defn.metaclass: metaclass_name = None From f9e049d77ab7bfb68f6d0d2f79a011c04d9373f5 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 5 Dec 2017 17:00:10 -0800 Subject: [PATCH 03/13] Fix old pythons --- mypy/plugin.py | 3 ++- mypy/semanal.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index af58bc28c094..fbfbe6c165a9 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -405,7 +405,8 @@ def add_method( def attr_s_callback(ctx: ClassDefContext) -> None: """Add an __init__ method to classes decorated with attr.s.""" info = ctx.cls.info - has_default: Dict[str, Expression] = {} # TODO: Handle these. + # TODO: Handle default arguments + has_default = {} # type: Dict[str, Expression] args = [] for name, table in info.names.items(): diff --git a/mypy/semanal.py b/mypy/semanal.py index e2ccd86cfc4a..fa3ef668aaf7 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -721,7 +721,7 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: self.setup_type_promotion(defn) for decorator in defn.decorators: - fullname: Optional[str] = None + fullname = None if isinstance(decorator, CallExpr): if isinstance(decorator.callee, RefExpr): fullname = decorator.callee.fullname From 757efc8eecce7caca7c1baa07ce9fb2c5eae3cab Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 5 Dec 2017 17:33:46 -0800 Subject: [PATCH 04/13] Allow MemberExpr --- mypy/plugin.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- mypy/semanal.py | 66 +++++++++++++++++++++++++--------------------- 2 files changed, 102 insertions(+), 33 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index fbfbe6c165a9..a07c96284ba6 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -5,7 +5,8 @@ from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ DictExpr, TypeInfo, ClassDef, ARG_POS, ARG_OPT, Var, Argument, FuncDef, \ - Block, SymbolTableNode, MDEF + Block, SymbolTableNode, MDEF, CallExpr, RefExpr, MemberExpr, NameExpr, \ + AssignmentStmt, TempNode from mypy.types import ( Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny @@ -110,6 +111,7 @@ def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> ClassDefContext = NamedTuple( 'ClassDecoratorContext', [ ('cls', ClassDef), + ('context', Optional[Expression]), ('api', SemanticAnalyzerPluginInterface) ]) @@ -402,15 +404,74 @@ 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. + + 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 - # TODO: Handle default arguments + + # 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 table.type: + 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 diff --git a/mypy/semanal.py b/mypy/semanal.py index fa3ef668aaf7..9a24fea09076 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -719,37 +719,45 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: yield True self.calculate_abstract_status(defn.info) self.setup_type_promotion(defn) + self.apply_class_plugin_hooks(defn) + self.leave_class() - for decorator in defn.decorators: - fullname = None - if isinstance(decorator, CallExpr): - if isinstance(decorator.callee, RefExpr): - fullname = decorator.callee.fullname - elif isinstance(decorator, NameExpr): - fullname = decorator.fullname - - if fullname: - hook = self.plugin.get_class_decorator_hook(fullname) - if hook: - hook(ClassDefContext(defn, self)) - - if defn.metaclass: - metaclass_name = None - if isinstance(defn.metaclass, NameExpr): - metaclass_name = defn.metaclass.name - elif isinstance(defn.metaclass, MemberExpr): - metaclass_name = get_member_expr_fullname( - defn.metaclass) - hook = self.plugin.get_class_metaclass_hook(metaclass_name) - if hook: - hook(ClassDefContext(defn, self)) - - for type_info in defn.info.bases: - hook = self.plugin.get_class_base_hook(type_info.type.fullname()) - if hook: - hook(ClassDefContext(defn, self)) + def apply_class_plugin_hooks(self, defn: ClassDef) -> None: + for decorator in defn.decorators: + fullname = None + if isinstance(decorator, CallExpr): + if isinstance(decorator.callee, RefExpr): + fullname = decorator.callee.fullname + elif isinstance(decorator, (MemberExpr, NameExpr)): + fullname = decorator.fullname - self.leave_class() + if fullname: + hook = self.plugin.get_class_decorator_hook(fullname) + if hook: + hook(ClassDefContext(defn, decorator, self)) + + if defn.metaclass: + metaclass_name = None + if isinstance(defn.metaclass, NameExpr): + metaclass_name = defn.metaclass.name + elif isinstance(defn.metaclass, MemberExpr): + metaclass_name = get_member_expr_fullname(defn.metaclass) + if metaclass_name: + hook = self.plugin.get_class_metaclass_hook(metaclass_name) + if hook: + hook(ClassDefContext(defn, defn.metaclass, self)) + + for base in defn.base_type_exprs: + base_name = None + if isinstance(base, NameExpr): + base_name = base.name + elif isinstance(base, MemberExpr): + base_name = get_member_expr_fullname(base) + + if base_name: + hook = self.plugin.get_class_base_hook(base_name) + if hook: + hook(ClassDefContext(defn, base, self)) def analyze_class_keywords(self, defn: ClassDef) -> None: for value in defn.keywords.values(): From c01352e7567494d5a27d2048164b64dbbd851b55 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 11 Dec 2017 12:38:51 -0800 Subject: [PATCH 05/13] CR, also remove attrs code --- mypy/plugin.py | 142 ++++++------------------------------------------ mypy/semanal.py | 38 ++++++------- 2 files changed, 36 insertions(+), 144 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index a07c96284ba6..9afc7c898fc1 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,14 +1,12 @@ """Plugin system for extending mypy.""" +from collections import OrderedDict from abc import abstractmethod -from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict +from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar -from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ - DictExpr, TypeInfo, ClassDef, ARG_POS, ARG_OPT, Var, Argument, FuncDef, \ - Block, SymbolTableNode, MDEF, CallExpr, RefExpr, MemberExpr, NameExpr, \ - AssignmentStmt, TempNode +from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef from mypy.types import ( - Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType, + Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, AnyType, TypeList, UnboundType, TypeOfAny ) from mypy.messages import MessageBuilder @@ -62,6 +60,15 @@ class SemanticAnalyzerPluginInterface: def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance: raise NotImplementedError + @abstractmethod + def parse_bool(self, expr: Expression) -> Optional[bool]: + raise NotImplementedError + + @abstractmethod + def fail(self, msg: str, ctx: Context, serious: bool = False, *, + blocker: bool = False) -> None: + raise NotImplementedError + # A context for a function hook that infers the return type of a function with # a special signature. @@ -108,13 +115,15 @@ def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> ('context', Context), ('api', CheckerPluginInterface)]) +# A context for a class hook that modifies the class definition. ClassDefContext = NamedTuple( 'ClassDecoratorContext', [ - ('cls', ClassDef), - ('context', Optional[Expression]), + ('cls', ClassDef), # The class definition + ('reason', Expression), # The expression being applied (decorator, metaclass, base class) ('api', SemanticAnalyzerPluginInterface) ]) + class Plugin: """Base class of all type checker plugins. @@ -217,7 +226,7 @@ def get_class_metaclass_hook(self, fullname: str return self._find_hook(lambda plugin: plugin.get_class_metaclass_hook(fullname)) def get_class_base_hook(self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: + ) -> Optional[Callable[[ClassDefContext], None]]: return self._find_hook(lambda plugin: plugin.get_class_base_hook(fullname)) def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: @@ -253,12 +262,6 @@ 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 - return None - def open_callback(ctx: FunctionContext) -> Type: """Infer a better return type for 'open'. @@ -376,112 +379,3 @@ 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 diff --git a/mypy/semanal.py b/mypy/semanal.py index 9a24fea09076..5f894d77a24e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -723,37 +723,35 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: self.leave_class() def apply_class_plugin_hooks(self, defn: ClassDef) -> None: - for decorator in defn.decorators: - fullname = None - if isinstance(decorator, CallExpr): - if isinstance(decorator.callee, RefExpr): - fullname = decorator.callee.fullname - elif isinstance(decorator, (MemberExpr, NameExpr)): - fullname = decorator.fullname + """Apply a plugin hook that may infer a more precise definition for a class.""" + def get_fullname(expr: Expression) -> Optional[str]: + # We support @foo.bar(...) @foo.bar and @bar + # TODO: Support IndexExpressions? + if isinstance(expr, CallExpr): + if isinstance(expr.callee, RefExpr): + return expr.callee.fullname + elif isinstance(expr, MemberExpr): + return get_member_expr_fullname(expr) + elif isinstance(expr, NameExpr): + return expr.fullname + return None - if fullname: - hook = self.plugin.get_class_decorator_hook(fullname) + for decorator in defn.decorators: + decorator_name = get_fullname(decorator) + if decorator_name: + hook = self.plugin.get_class_decorator_hook(decorator_name) if hook: hook(ClassDefContext(defn, decorator, self)) if defn.metaclass: - metaclass_name = None - if isinstance(defn.metaclass, NameExpr): - metaclass_name = defn.metaclass.name - elif isinstance(defn.metaclass, MemberExpr): - metaclass_name = get_member_expr_fullname(defn.metaclass) + metaclass_name = get_fullname(defn.metaclass) if metaclass_name: hook = self.plugin.get_class_metaclass_hook(metaclass_name) if hook: hook(ClassDefContext(defn, defn.metaclass, self)) for base in defn.base_type_exprs: - base_name = None - if isinstance(base, NameExpr): - base_name = base.name - elif isinstance(base, MemberExpr): - base_name = get_member_expr_fullname(base) - + base_name = get_fullname(base) if base_name: hook = self.plugin.get_class_base_hook(base_name) if hook: From 819b5ba580b22609e027c9fa5f6a4315103c6d78 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 12 Dec 2017 11:00:32 -0800 Subject: [PATCH 06/13] Support base expressions correctly --- mypy/semanal.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 5f894d77a24e..60f8f439bc16 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -724,9 +724,9 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: def apply_class_plugin_hooks(self, defn: ClassDef) -> None: """Apply a plugin hook that may infer a more precise definition for a class.""" + def get_fullname(expr: Expression) -> Optional[str]: - # We support @foo.bar(...) @foo.bar and @bar - # TODO: Support IndexExpressions? + # We support foo.bar(...) foo.bar and bar if isinstance(expr, CallExpr): if isinstance(expr.callee, RefExpr): return expr.callee.fullname @@ -750,12 +750,22 @@ def get_fullname(expr: Expression) -> Optional[str]: if hook: hook(ClassDefContext(defn, defn.metaclass, self)) - for base in defn.base_type_exprs: - base_name = get_fullname(base) + for base_expr in defn.base_type_exprs: + try: + base = expr_to_unanalyzed_type(base_expr) + except TypeTranslationError: + continue # This will be reported later + if not isinstance(base, UnboundType): + continue + sym = self.lookup_qualified(base.name, base) + if sym is None or sym.node is None: + continue + base_name = sym.node.fullname() + if base_name: hook = self.plugin.get_class_base_hook(base_name) if hook: - hook(ClassDefContext(defn, base, self)) + hook(ClassDefContext(defn, base_expr, self)) def analyze_class_keywords(self, defn: ClassDef) -> None: for value in defn.keywords.values(): From 1e5ff80c0dd604c90df2bd82d27b26bdbbd140b7 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 12 Dec 2017 11:01:17 -0800 Subject: [PATCH 07/13] Comment tweak --- mypy/semanal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 60f8f439bc16..992ee8792abb 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -726,7 +726,7 @@ def apply_class_plugin_hooks(self, defn: ClassDef) -> None: """Apply a plugin hook that may infer a more precise definition for a class.""" def get_fullname(expr: Expression) -> Optional[str]: - # We support foo.bar(...) foo.bar and bar + # We support foo.bar(...), foo.bar, and bar. if isinstance(expr, CallExpr): if isinstance(expr.callee, RefExpr): return expr.callee.fullname From da9224c72bbe71625a06f41013fa59921e63303e Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 12 Dec 2017 22:10:21 -0800 Subject: [PATCH 08/13] Simplify IndexExpr lookup --- mypy/semanal.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 992ee8792abb..cb60b8de06d9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -728,12 +728,17 @@ def apply_class_plugin_hooks(self, defn: ClassDef) -> None: def get_fullname(expr: Expression) -> Optional[str]: # We support foo.bar(...), foo.bar, and bar. if isinstance(expr, CallExpr): - if isinstance(expr.callee, RefExpr): - return expr.callee.fullname - elif isinstance(expr, MemberExpr): - return get_member_expr_fullname(expr) - elif isinstance(expr, NameExpr): - return expr.fullname + return get_fullname(expr.callee) + elif isinstance(expr, (MemberExpr, NameExpr)): + if expr.fullname: + return expr.fullname + + # If we don't have a fullname look it up. + node = self.lookup_type_node(expr) + if node: + return node.node.fullname() + elif isinstance(expr, IndexExpr): + return get_fullname(expr.base) return None for decorator in defn.decorators: @@ -751,17 +756,7 @@ def get_fullname(expr: Expression) -> Optional[str]: hook(ClassDefContext(defn, defn.metaclass, self)) for base_expr in defn.base_type_exprs: - try: - base = expr_to_unanalyzed_type(base_expr) - except TypeTranslationError: - continue # This will be reported later - if not isinstance(base, UnboundType): - continue - sym = self.lookup_qualified(base.name, base) - if sym is None or sym.node is None: - continue - base_name = sym.node.fullname() - + base_name = get_fullname(base_expr) if base_name: hook = self.plugin.get_class_base_hook(base_name) if hook: From a91559a778e888b326b83ca819f46d1935eded71 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Tue, 12 Dec 2017 22:13:38 -0800 Subject: [PATCH 09/13] Remove comment --- mypy/semanal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index cb60b8de06d9..bfd5a4babf8e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -726,7 +726,6 @@ def apply_class_plugin_hooks(self, defn: ClassDef) -> None: """Apply a plugin hook that may infer a more precise definition for a class.""" def get_fullname(expr: Expression) -> Optional[str]: - # We support foo.bar(...), foo.bar, and bar. if isinstance(expr, CallExpr): return get_fullname(expr.callee) elif isinstance(expr, (MemberExpr, NameExpr)): From 99817569d687690f05dac2f33d74e076ef70d54f Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 13 Dec 2017 06:53:46 -0800 Subject: [PATCH 10/13] Fix tests --- mypy/semanal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index bfd5a4babf8e..e059fe2d362b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -733,9 +733,9 @@ def get_fullname(expr: Expression) -> Optional[str]: return expr.fullname # If we don't have a fullname look it up. - node = self.lookup_type_node(expr) - if node: - return node.node.fullname() + sym = self.lookup_type_node(expr) + if sym: + return sym.fullname elif isinstance(expr, IndexExpr): return get_fullname(expr.base) return None From 1a82e2d53a24badc69d8c93472a61b774c492e2b Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 13 Dec 2017 14:58:22 -0800 Subject: [PATCH 11/13] CR --- mypy/plugin.py | 10 +++++----- mypy/semanal.py | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 9afc7c898fc1..6ae9227973f5 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -165,8 +165,8 @@ def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return None - def get_class_metaclass_hook(self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: + def get_metaclass_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: return None def get_class_base_hook(self, fullname: str @@ -221,9 +221,9 @@ def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return self._find_hook(lambda plugin: plugin.get_class_decorator_hook(fullname)) - def get_class_metaclass_hook(self, fullname: str - ) -> Optional[Callable[[ClassDefContext], None]]: - return self._find_hook(lambda plugin: plugin.get_class_metaclass_hook(fullname)) + def get_metaclass_hook(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], None]]: + return self._find_hook(lambda plugin: plugin.get_metaclass_hook(fullname)) def get_class_base_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: diff --git a/mypy/semanal.py b/mypy/semanal.py index e059fe2d362b..27571a3170de 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -724,20 +724,20 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]: def apply_class_plugin_hooks(self, defn: ClassDef) -> None: """Apply a plugin hook that may infer a more precise definition for a class.""" - def get_fullname(expr: Expression) -> Optional[str]: if isinstance(expr, CallExpr): return get_fullname(expr.callee) - elif isinstance(expr, (MemberExpr, NameExpr)): + elif isinstance(expr, IndexExpr): + return get_fullname(expr.base) + elif isinstance(expr, RefExpr): if expr.fullname: return expr.fullname - - # If we don't have a fullname look it up. + # If we don't have a fullname look it up. This happens because base classes are + # analyzed in a different manner (see exprtotype.py) and therefore those AST + # nodes will not have full names. sym = self.lookup_type_node(expr) if sym: return sym.fullname - elif isinstance(expr, IndexExpr): - return get_fullname(expr.base) return None for decorator in defn.decorators: @@ -750,7 +750,7 @@ def get_fullname(expr: Expression) -> Optional[str]: if defn.metaclass: metaclass_name = get_fullname(defn.metaclass) if metaclass_name: - hook = self.plugin.get_class_metaclass_hook(metaclass_name) + hook = self.plugin.get_metaclass_hook(metaclass_name) if hook: hook(ClassDefContext(defn, defn.metaclass, self)) From 8ba493262b69dee2e89935268b612086ef8994f4 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Wed, 13 Dec 2017 15:15:45 -0800 Subject: [PATCH 12/13] get_base_class_hook --- mypy/plugin.py | 6 +++--- mypy/semanal.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 6ae9227973f5..463434228496 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -169,7 +169,7 @@ def get_metaclass_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return None - def get_class_base_hook(self, fullname: str + def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return None @@ -225,9 +225,9 @@ def get_metaclass_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: return self._find_hook(lambda plugin: plugin.get_metaclass_hook(fullname)) - def get_class_base_hook(self, fullname: str + def get_base_class_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - return self._find_hook(lambda plugin: plugin.get_class_base_hook(fullname)) + return self._find_hook(lambda plugin: plugin.get_base_class_hook(fullname)) def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]: for plugin in self._plugins: diff --git a/mypy/semanal.py b/mypy/semanal.py index 27571a3170de..d8ff72ef7af7 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -757,7 +757,7 @@ def get_fullname(expr: Expression) -> Optional[str]: for base_expr in defn.base_type_exprs: base_name = get_fullname(base_expr) if base_name: - hook = self.plugin.get_class_base_hook(base_name) + hook = self.plugin.get_base_class_hook(base_name) if hook: hook(ClassDefContext(defn, base_expr, self)) From 32e6e9ca2ce4a50c867607b72d3864b11588b56d Mon Sep 17 00:00:00 2001 From: David Euresti Date: Thu, 14 Dec 2017 06:45:22 -0800 Subject: [PATCH 13/13] AnalyzerPluginInterface->TypeAnalyzerPluginInterface --- mypy/plugin.py | 4 ++-- mypy/typeanal.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index 463434228496..4ffa9395afc5 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -13,7 +13,7 @@ from mypy.options import Options -class AnalyzerPluginInterface: +class TypeAnalyzerPluginInterface: """Interface for accessing semantic analyzer functionality in plugins.""" @abstractmethod @@ -40,7 +40,7 @@ def analyze_callable_args(self, arglist: TypeList) -> Optional[Tuple[List[Type], 'AnalyzeTypeContext', [ ('type', UnboundType), # Type to analyze ('context', Context), - ('api', AnalyzerPluginInterface)]) + ('api', TypeAnalyzerPluginInterface)]) class CheckerPluginInterface: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index f5cfcc472d19..1b4467d031db 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -27,7 +27,7 @@ from mypy.sametypes import is_same_type from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.subtypes import is_subtype -from mypy.plugin import Plugin, AnalyzerPluginInterface, AnalyzeTypeContext +from mypy.plugin import Plugin, TypeAnalyzerPluginInterface, AnalyzeTypeContext from mypy import nodes, messages @@ -132,7 +132,7 @@ def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str: return msg -class TypeAnalyser(SyntheticTypeVisitor[Type], AnalyzerPluginInterface): +class TypeAnalyser(SyntheticTypeVisitor[Type], TypeAnalyzerPluginInterface): """Semantic analyzer for types (semantic analysis pass 2). Converts unbound types into bound types.