From 0c03d3f982fe687b20f95fe078831e2cceea7785 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Fri, 26 May 2017 17:48:03 +0100 Subject: [PATCH 01/11] Refactor plugin system Prepare for supporting more general plugins, for things other than just functions. The new design also makes it easier to add support for user-defined plugins. --- mypy/build.py | 4 ++- mypy/checker.py | 10 ++++-- mypy/checkexpr.py | 17 ++++++---- mypy/{funcplugins.py => plugin.py} | 54 +++++++++++++++++++++--------- 4 files changed, 59 insertions(+), 26 deletions(-) rename mypy/{funcplugins.py => plugin.py} (61%) diff --git a/mypy/build.py b/mypy/build.py index f803929a8fdf..41434adf6c79 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -42,6 +42,7 @@ from mypy.stats import dump_type_stats from mypy.types import Type from mypy.version import __version__ +from mypy.plugin import DefaultPlugin # We need to know the location of this file to load data, but @@ -1505,8 +1506,9 @@ def type_check_first_pass(self) -> None: if self.options.semantic_analysis_only: return with self.wrap_context(): + plugin = DefaultPlugin(self.options.python_version) self.type_checker = TypeChecker(manager.errors, manager.modules, self.options, - self.tree, self.xpath) + self.tree, self.xpath, plugin) self.type_checker.check_first_pass() def type_check_second_pass(self) -> bool: diff --git a/mypy/checker.py b/mypy/checker.py index fcd334edbcce..89730f894d24 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -56,6 +56,7 @@ from mypy.binder import ConditionalTypeBinder, get_declaration from mypy.meet import is_overlapping_types from mypy.options import Options +from mypy.plugin import Plugin from mypy import experiments @@ -127,8 +128,12 @@ class TypeChecker(NodeVisitor[None]): # directly or indirectly. module_refs = None # type: Set[str] + # Plugin that provides special type checking rules for specific library + # functions such as open(), etc. + plugin = None # type: Plugin + def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options, - tree: MypyFile, path: str) -> None: + tree: MypyFile, path: str, plugin: Optional[Plugin] = None) -> None: """Construct a type checker. Use errors to report type check errors. @@ -139,7 +144,8 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option self.tree = tree self.path = path self.msg = MessageBuilder(errors, modules) - self.expr_checker = mypy.checkexpr.ExpressionChecker(self, self.msg) + self.plugin = plugin or Plugin(options.python_version) + self.expr_checker = mypy.checkexpr.ExpressionChecker(self, self.msg, self.plugin) self.scope = Scope(tree) self.binder = ConditionalTypeBinder() self.globals = tree.names diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 074438e53761..04d8f18de0e3 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -44,7 +44,7 @@ from mypy.util import split_module_names from mypy.typevars import fill_typevars from mypy.visitor import ExpressionVisitor -from mypy.funcplugins import get_function_plugin_callbacks, PluginCallback +from mypy.plugin import Plugin from mypy.typeanal import make_optional_type from mypy import experiments @@ -105,17 +105,18 @@ class ExpressionChecker(ExpressionVisitor[Type]): type_context = None # type: List[Optional[Type]] strfrm_checker = None # type: StringFormatterChecker - function_plugins = None # type: Dict[str, PluginCallback] + plugin = None # type: Plugin def __init__(self, chk: 'mypy.checker.TypeChecker', - msg: MessageBuilder) -> None: + msg: MessageBuilder, + plugin: Plugin) -> None: """Construct an expression type checker.""" self.chk = chk self.msg = msg + self.plugin = plugin self.type_context = [None] self.strfrm_checker = StringFormatterChecker(self, self.chk, self.msg) - self.function_plugins = get_function_plugin_callbacks(self.chk.options.python_version) def visit_name_expr(self, e: NameExpr) -> Type: """Type check a name expression. @@ -362,8 +363,10 @@ def apply_function_plugin(self, for actual in actuals: formal_arg_types[formal].append(arg_types[actual]) formal_arg_exprs[formal].append(args[actual]) - return self.function_plugins[fullname]( - formal_arg_types, formal_arg_exprs, inferred_ret_type, self.chk.named_generic_type) + callback = self.plugin.get_function_hook(fullname) + assert callback is not None # Assume that caller ensure this + return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type, + self.chk.named_generic_type) def check_call_expr_with_callee_type(self, callee_type: Type, e: CallExpr, callable_name: Optional[str]) -> Type: @@ -443,7 +446,7 @@ def check_call(self, callee: Type, args: List[Expression], if callable_node: # Store the inferred callable type. self.chk.store_type(callable_node, callee) - if callable_name in self.function_plugins: + if self.plugin.get_function_hook(callable_name): ret_type = self.apply_function_plugin( arg_types, callee.ret_type, arg_kinds, formal_to_actual, args, len(callee.arg_types), callable_name) diff --git a/mypy/funcplugins.py b/mypy/plugin.py similarity index 61% rename from mypy/funcplugins.py rename to mypy/plugin.py index b1113ab30ae9..fea751410bf4 100644 --- a/mypy/funcplugins.py +++ b/mypy/plugin.py @@ -1,40 +1,62 @@ -"""Plugins that implement special type checking rules for individual functions. +"""Plugin architecture for custom type checking rules for specific functions, etc. -The plugins infer better types for tricky functions such as "open". +A plugin can, for example, infer better types for tricky functions such as "open". """ -from typing import Tuple, Dict, Callable, List +from typing import Callable, List, Tuple, Optional from mypy.nodes import Expression, StrExpr from mypy.types import Type, Instance, CallableType +# Create an Instance given full name of class and type arguments. +NamedInstanceCallback = Callable[[str, List[Type]], Type] + # A callback that infers the return type of a function with a special signature. # # A no-op callback would just return the inferred return type, but a useful callback # at least sometimes can infer a more precise type. -PluginCallback = Callable[ +FunctionHook = Callable[ [ List[List[Type]], # List of types caller provides for each formal argument List[List[Expression]], # Actual argument expressions for each formal argument Type, # Return type for call inferred using the regular signature - Callable[[str, List[Type]], Type] # Callable for constructing a named instance type + NamedInstanceCallback # Callable for constructing a named instance type ], Type # Return type inferred by the callback ] -def get_function_plugin_callbacks(python_version: Tuple[int, int]) -> Dict[str, PluginCallback]: - """Return all available function plugins for a given Python version.""" - if python_version[0] == 3: - return { - 'builtins.open': open_callback, - 'contextlib.contextmanager': contextmanager_callback, - } - else: - return { - 'contextlib.contextmanager': contextmanager_callback, - } +class Plugin: + """Base class of type checker plugins. + + This defines a no-op plugin. Subclasses can override some methods to + provide some actual functionality. + + All get_ methods are treated as pure functions (you should assume that + results might be cached). + """ + + # TODO: Way of chaining multiple plugins + + def __init__(self, python_version: Tuple[int, int]) -> None: + self.python_version = python_version + + def get_function_hook(self, fullname: str) -> Optional[FunctionHook]: + return None + + # TODO: method / metaclass / class decorator hooks + + +class DefaultPlugin(Plugin): + """Type checker plugin that is enabled by default.""" + + def get_function_hook(self, fullname: str) -> Optional[FunctionHook]: + if fullname == 'contextlib.contextmanager': + return contextmanager_callback + elif fullname == 'builtins.open' and self.python_version[0] == 3: + return open_callback + return None def open_callback( From d82aa69ad9d588d3cf11a41251426b5947b2b969 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Mon, 5 Jun 2017 16:37:21 +0100 Subject: [PATCH 02/11] Special case type checking of TypedDict get and int.__pow__ Implement a general-purpose way of extending type inference of methods. --- mypy/checkexpr.py | 82 +++++++++++++------ mypy/nodes.py | 6 ++ mypy/plugin.py | 104 ++++++++++++++++++++++-- test-data/unit/check-expressions.test | 17 ++++ test-data/unit/check-typeddict.test | 27 +++++- test-data/unit/fixtures/ops.pyi | 1 + test-data/unit/fixtures/typing-full.pyi | 6 +- 7 files changed, 209 insertions(+), 34 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 04d8f18de0e3..941243bf5988 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -209,11 +209,20 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: isinstance(callee_type, CallableType) and callee_type.implicit): return self.msg.untyped_function_call(callee_type, e) + object_type = None if not isinstance(e.callee, RefExpr): fullname = None else: fullname = e.callee.fullname - ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname) + if (fullname is None + and isinstance(e.callee, MemberExpr)): + callee_expr_type = self.chk.type_map.get(e.callee.expr) + if isinstance(callee_expr_type, TypedDictType): + info = callee_expr_type.fallback.type.get_containing_type_info(e.callee.name) + if info: + fullname = '{}.{}'.format(info.fullname(), e.callee.name) + object_type = callee_expr_type + ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname, object_type) if isinstance(ret_type, UninhabitedType): self.chk.binder.unreachable() if not allow_none_return and isinstance(ret_type, NoneTyp): @@ -352,7 +361,8 @@ def apply_function_plugin(self, formal_to_actual: List[List[int]], args: List[Expression], num_formals: int, - fullname: Optional[str]) -> Type: + fullname: Optional[str], + object_type: Optional[Type]) -> Type: """Use special case logic to infer the return type for of a particular named function. Return the inferred return type. @@ -363,13 +373,22 @@ def apply_function_plugin(self, for actual in actuals: formal_arg_types[formal].append(arg_types[actual]) formal_arg_exprs[formal].append(args[actual]) - callback = self.plugin.get_function_hook(fullname) - assert callback is not None # Assume that caller ensure this - return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type, - self.chk.named_generic_type) - - def check_call_expr_with_callee_type(self, callee_type: Type, - e: CallExpr, callable_name: Optional[str]) -> Type: + if object_type is None: + callback = self.plugin.get_function_hook(fullname) + assert callback is not None # Assume that caller ensure this + return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type, + self.chk.named_generic_type) + else: + callback = self.plugin.get_method_hook(fullname) + assert callback is not None # Assume that caller ensure this + return callback(object_type, formal_arg_types, formal_arg_exprs, inferred_ret_type, + self.chk.named_generic_type) + + def check_call_expr_with_callee_type(self, + callee_type: Type, + e: CallExpr, + callable_name: Optional[str], + object_type: Optional[Type]) -> Type: """Type check call expression. The given callee type overrides the type of the callee @@ -377,14 +396,16 @@ def check_call_expr_with_callee_type(self, callee_type: Type, """ return self.check_call(callee_type, e.args, e.arg_kinds, e, e.arg_names, callable_node=e.callee, - callable_name=callable_name)[0] + callable_name=callable_name, + object_type=object_type)[0] def check_call(self, callee: Type, args: List[Expression], arg_kinds: List[int], context: Context, arg_names: List[str] = None, callable_node: Expression = None, arg_messages: MessageBuilder = None, - callable_name: Optional[str] = None) -> Tuple[Type, Type]: + callable_name: Optional[str] = None, + object_type: Optional[Type] = None) -> Tuple[Type, Type]: """Type check a call. Also infer type arguments if the callee is a generic function. @@ -392,14 +413,18 @@ def check_call(self, callee: Type, args: List[Expression], Return (result type, inferred callee type). Arguments: - callee: type of the called value - args: actual argument expressions - arg_kinds: contains nodes.ARG_* constant for each argument in args - describing whether the argument is positional, *arg, etc. - arg_names: names of arguments (optional) - callable_node: associate the inferred callable type to this node, - if specified - arg_messages: TODO + callee: type of the called value + args: actual argument expressions + arg_kinds: contains nodes.ARG_* constant for each argument in args + describing whether the argument is positional, *arg, etc. + arg_names: names of arguments (optional) + callable_node: associate the inferred callable type to this node, + if specified + arg_messages: TODO + callable_name: Fully-qualified name of the function/method to call, + or None if unavaiable (examples: 'builtins.open', 'typing.Mapping.get') + object_type: If callable_name refers to a method, the type of the object + on which the method is being called """ arg_messages = arg_messages or self.msg if isinstance(callee, CallableType): @@ -446,10 +471,12 @@ def check_call(self, callee: Type, args: List[Expression], if callable_node: # Store the inferred callable type. self.chk.store_type(callable_node, callee) - if self.plugin.get_function_hook(callable_name): + + if ((object_type is None and self.plugin.get_function_hook(callable_name)) + or (object_type is not None and self.plugin.get_method_hook(callable_name))): ret_type = self.apply_function_plugin( arg_types, callee.ret_type, arg_kinds, formal_to_actual, - args, len(callee.arg_types), callable_name) + args, len(callee.arg_types), callable_name, object_type) callee = callee.copy_modified(ret_type=ret_type) return callee.ret_type, callee elif isinstance(callee, Overloaded): @@ -464,7 +491,9 @@ def check_call(self, callee: Type, args: List[Expression], callee, context, messages=arg_messages) return self.check_call(target, args, arg_kinds, context, arg_names, - arg_messages=arg_messages) + arg_messages=arg_messages, + callable_name=callable_name, + object_type=object_type) elif isinstance(callee, AnyType) or not self.chk.in_checked_function(): self.infer_arg_types_in_context(None, args) return AnyType(), AnyType() @@ -1298,8 +1327,15 @@ def check_op_local(self, method: str, base_type: Type, arg: Expression, method_type = analyze_member_access(method, base_type, context, False, False, True, self.named_type, self.not_ready_callback, local_errors, original_type=base_type, chk=self.chk) + callable_name = None + object_type = None + if isinstance(base_type, Instance): + # TODO: Find out in which class the method was defined originally? + callable_name = '{}.{}'.format(base_type.type.fullname(), method) + object_type = base_type return self.check_call(method_type, [arg], [nodes.ARG_POS], - context, arg_messages=local_errors) + context, arg_messages=local_errors, + callable_name=callable_name, object_type=object_type) def check_op(self, method: str, base_type: Type, arg: Expression, context: Context, diff --git a/mypy/nodes.py b/mypy/nodes.py index a5cb96007750..48715662d9d6 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2037,6 +2037,12 @@ def get(self, name: str) -> Optional['SymbolTableNode']: return n return None + def get_containing_type_info(self, name: str) -> Optional['TypeInfo']: + for cls in self.mro: + if name in cls.names: + return cls + return None + def __getitem__(self, name: str) -> 'SymbolTableNode': n = self.get(name) if n: diff --git a/mypy/plugin.py b/mypy/plugin.py index fea751410bf4..176c2246eac2 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,12 +1,7 @@ -"""Plugin architecture for custom type checking rules for specific functions, etc. - -A plugin can, for example, infer better types for tricky functions such as "open". -""" - from typing import Callable, List, Tuple, Optional -from mypy.nodes import Expression, StrExpr -from mypy.types import Type, Instance, CallableType +from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr +from mypy.types import Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp # Create an Instance given full name of class and type arguments. @@ -26,6 +21,38 @@ Type # Return type inferred by the callback ] +MethodHook = Callable[ + [ + Type, # Receiver object type + List[List[Type]], # List of types caller provides for each formal argument + List[List[Expression]], # Actual argument expressions for each formal argument + Type, # Return type for call inferred using the regular signature + NamedInstanceCallback # Callable for constructing a named instance type + ], + Type # Return type inferred by the callback +] + +# Used to provide a custom syntax for a type. +# +# TODO: Maybe we should allow more stuff here, such as arbitrary string and int literals? +#TypeAnalyzeHook = Callable[ +# [ +# Expression, # The <expression> in C[<expression>] +# Callable[[Type], Type], # Callback for running semantic analysis +# NamedInstanceCallback +# ], +# Type # Representation of the type +#] + +# Used to provide a custom string representation for a class. +#TypeToStrHook = Callable[ +# [ +# Type, +# Callable[[Type], str], # Callback for ordinary pretty printing +# ], +# str +#] + class Plugin: """Base class of type checker plugins. @@ -45,7 +72,16 @@ def __init__(self, python_version: Tuple[int, int]) -> None: def get_function_hook(self, fullname: str) -> Optional[FunctionHook]: return None - # TODO: method / metaclass / class decorator hooks + def get_method_hook(self, fullname: str) -> Optional[MethodHook]: + return None + + #def get_type_analyze_hook(self, fullname: str) -> Optional[TypeAnalyzeHook]: + # return None + + #def get_type_to_str_hook(self, fullname: str) -> Optional[TypeToStrHook]: + # return None + + # TODO: metaclass / class decorator hook class DefaultPlugin(Plugin): @@ -58,6 +94,13 @@ def get_function_hook(self, fullname: str) -> Optional[FunctionHook]: return open_callback return None + def get_method_hook(self, fullname: str) -> Optional[MethodHook]: + if fullname == 'typing.Mapping.get': + return typed_dict_get_callback + elif fullname == 'builtins.int.__pow__': + return int_pow_callback + return None + def open_callback( arg_types: List[List[Type]], @@ -100,3 +143,48 @@ def contextmanager_callback( arg_kinds=arg_type.arg_kinds, arg_names=arg_type.arg_names) return inferred_return_type + + +def typed_dict_get_callback( + object_type: Type, + arg_types: List[List[Type]], + args: List[List[Expression]], + inferred_return_type: Type, + named_generic_type: Callable[[str, List[Type]], Type]) -> Type: + """Infer a precise return type for TypedDict.get with literal first argument.""" + if (isinstance(object_type, TypedDictType) + and len(arg_types) >= 1 + and len(arg_types[0]) == 1 + and isinstance(args[0][0], StrExpr)): + key = args[0][0].value + value_type = object_type.items.get(key) + if value_type: + if len(arg_types) == 1: + return UnionType.make_simplified_union([value_type, NoneTyp()]) + elif len(arg_types) == 2 and len(arg_types[1]) == 1: + return UnionType.make_simplified_union([value_type, arg_types[1][0]]) + return inferred_return_type + + +def int_pow_callback( + object_type: Type, + arg_types: List[List[Type]], + args: List[List[Expression]], + inferred_return_type: Type, + named_generic_type: Callable[[str, List[Type]], Type]) -> Type: + """Infer a more precise return type for int.__pow__.""" + print(arg_types) + if (len(arg_types) == 1 + and len(arg_types[0]) == 1): + arg = args[0][0] + if isinstance(arg, IntExpr): + exponent = arg.value + elif isinstance(arg, UnaryExpr) and arg.op == '-' and isinstance(arg.expr, IntExpr): + exponent = -arg.expr.value + else: + return inferred_return_type + if exponent >= 0: + return named_generic_type('builtins.int', []) + else: + return named_generic_type('builtins.float', []) + return inferred_return_type diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index ae1498acdadd..d562a0ee189a 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1682,3 +1682,20 @@ d = {**a, **b, 'c': 3} e = {1: 'a', **a} # E: Argument 1 to "update" of "dict" has incompatible type Dict[str, int]; expected Mapping[int, str] f = {**b} # type: Dict[int, int] # E: List item 0 has incompatible type Dict[str, int] [builtins fixtures/dict.pyi] + + +-- Type checker default plugin +-- --------------------------- + + +[case testIntPow] +a = 1 +b = a + 2 +reveal_type(a**0) # E: Revealed type is 'builtins.int' +reveal_type(a**1) # E: Revealed type is 'builtins.int' +reveal_type(a**2) # E: Revealed type is 'builtins.int' +reveal_type(a**(-0)) # E: Revealed type is 'builtins.int' +reveal_type(a**(-1)) # E: Revealed type is 'builtins.float' +reveal_type(a**(-2)) # E: Revealed type is 'builtins.float' +reveal_type(a**b) # E: Revealed type is 'Any' +[builtins fixtures/ops.pyi] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 4714ec77f3dc..b56cac8c7eca 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -528,7 +528,7 @@ reveal_type(f(g)) # E: Revealed type is '<nothing>' -- Methods --- TODO: iter() doesn't accept TypedDictType as an argument type. Figure out why. +-- TODO: iter() does not accept TypedDictType as an argument type. Figure out why. --[case testCanCallMappingMethodsOnTypedDict] --from mypy_extensions import TypedDict --Cell = TypedDict('Cell', {'value': int}) @@ -727,7 +727,6 @@ f(dict(x=1, y=3, z=4)) # E: Expected items ['x', 'y'] but found ['x', 'y', 'z'] [builtins fixtures/dict.pyi] - [case testTypedDictExplicitTypes] from mypy_extensions import TypedDict @@ -744,3 +743,27 @@ p3 = {'x': 'hi'} # E: Expected items ['x', 'y'] but found ['x']. p4: Point = {'x': 1, 'y': 2} [builtins fixtures/dict.pyi] + + +-- Other TypedDict methods + +[case testTypedDictGetMethod] +# flags: --strict-optional +from mypy_extensions import TypedDict +class A: pass +D = TypedDict('D', {'x': int, 'y': str}) +d: D +reveal_type(d.get('x')) # E: Revealed type is 'Union[builtins.int, builtins.None]' +reveal_type(d.get('y')) # E: Revealed type is 'Union[builtins.str, builtins.None]' +reveal_type(d.get('x', A())) # E: Revealed type is 'Union[builtins.int, __main__.A]' +reveal_type(d.get('x', 1)) # E: Revealed type is 'builtins.int' +reveal_type(d.get('y', None)) # E: Revealed type is 'Union[builtins.str, builtins.None]' +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypedDictMissingMethod] +from mypy_extensions import TypedDict +D = TypedDict('D', {'x': int, 'y': str}) +d: D +d.bad(1) # E: "D" has no attribute "bad" +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index 2eb6f4d0a945..8e18aeae2afd 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -39,6 +39,7 @@ class int: def __mul__(self, x: 'int') -> 'int': pass def __mod__(self, x: 'int') -> 'int': pass def __floordiv__(self, x: 'int') -> 'int': pass + def __pow__(self, x: 'int') -> Any: pass def __pos__(self) -> 'int': pass def __neg__(self) -> 'int': pass def __eq__(self, x: object) -> bool: pass diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index 87b51cd0d340..463b117db48d 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -103,7 +103,11 @@ class Sequence(Iterable[T], Generic[T]): @abstractmethod def __getitem__(self, n: Any) -> T: pass -class Mapping(Generic[T, U]): pass +class Mapping(Generic[T, U]): + @overload + def get(self, k: T) -> Optional[U]: ... + @overload + def get(self, k: T, default: Union[U, V]) -> Union[U, V]: ... class MutableMapping(Generic[T, U]): pass From 56613397c673b888de54639f47b53f58ecf350fa Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 12:14:46 +0100 Subject: [PATCH 03/11] Fix argument type context for TypedDict get --- mypy/checkexpr.py | 44 +++++++++++++++++++---- mypy/plugin.py | 54 +++++++++++++++++++++++++++-- test-data/unit/check-typeddict.test | 26 ++++++++++++++ 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 941243bf5988..205c851d6ce0 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -44,7 +44,7 @@ from mypy.util import split_module_names from mypy.typevars import fill_typevars from mypy.visitor import ExpressionVisitor -from mypy.plugin import Plugin +from mypy.plugin import Plugin, MethodSignatureHook from mypy.typeanal import make_optional_type from mypy import experiments @@ -215,13 +215,18 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: else: fullname = e.callee.fullname if (fullname is None - and isinstance(e.callee, MemberExpr)): + and isinstance(e.callee, MemberExpr) + and isinstance(callee_type, FunctionLike)): callee_expr_type = self.chk.type_map.get(e.callee.expr) if isinstance(callee_expr_type, TypedDictType): info = callee_expr_type.fallback.type.get_containing_type_info(e.callee.name) if info: fullname = '{}.{}'.format(info.fullname(), e.callee.name) object_type = callee_expr_type + signature_hook = self.plugin.get_method_signature_hook(fullname) + if signature_hook: + callee_type = self.apply_method_signature_hook( + e, callee_type, object_type, signature_hook) ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname, object_type) if isinstance(ret_type, UninhabitedType): self.chk.binder.unreachable() @@ -379,10 +384,37 @@ def apply_function_plugin(self, return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type, self.chk.named_generic_type) else: - callback = self.plugin.get_method_hook(fullname) - assert callback is not None # Assume that caller ensure this - return callback(object_type, formal_arg_types, formal_arg_exprs, inferred_ret_type, - self.chk.named_generic_type) + method_callback = self.plugin.get_method_hook(fullname) + assert method_callback is not None # Assume that caller ensure this + return method_callback(object_type, formal_arg_types, formal_arg_exprs, + inferred_ret_type, self.chk.named_generic_type) + + def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_type: Type, + signature_hook: MethodSignatureHook) -> FunctionLike: + """Apply a plugin hook that may infer a more precise signature for a method.""" + if isinstance(callee, CallableType): + arg_kinds = e.arg_kinds + arg_names = e.arg_names + args = e.args + num_formals = len(callee.arg_kinds) + formal_to_actual = map_actuals_to_formals( + arg_kinds, arg_names, + callee.arg_kinds, callee.arg_names, + lambda i: self.accept(args[i])) + formal_arg_exprs = [[] for _ in range(num_formals)] # type: List[List[Expression]] + for formal, actuals in enumerate(formal_to_actual): + for actual in actuals: + formal_arg_exprs[formal].append(args[actual]) + return signature_hook(object_type, formal_arg_exprs, callee, + self.chk.named_generic_type) + else: + assert isinstance(callee, Overloaded) + items = [] + for item in callee.items(): + adjusted = self.apply_method_signature_hook(e, item, object_type, signature_hook) + assert isinstance(adjusted, CallableType) + items.append(adjusted) + return Overloaded(items) def check_call_expr_with_callee_type(self, callee_type: Type, diff --git a/mypy/plugin.py b/mypy/plugin.py index 176c2246eac2..fd3b0a0033f8 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,7 +1,9 @@ from typing import Callable, List, Tuple, Optional from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr -from mypy.types import Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp +from mypy.types import ( + Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType +) # Create an Instance given full name of class and type arguments. @@ -21,9 +23,19 @@ Type # Return type inferred by the callback ] +MethodSignatureHook = Callable[ + [ + Type, # Base object type + List[List[Expression]], # Actual argument expressions for each formal argument + CallableType, # Original signature of the method + NamedInstanceCallback # Callable for constructing a named instance type + ], + CallableType # Potentially more precise signature inferred for the method +] + MethodHook = Callable[ [ - Type, # Receiver object type + Type, # Base object type List[List[Type]], # List of types caller provides for each formal argument List[List[Expression]], # Actual argument expressions for each formal argument Type, # Return type for call inferred using the regular signature @@ -72,6 +84,9 @@ def __init__(self, python_version: Tuple[int, int]) -> None: def get_function_hook(self, fullname: str) -> Optional[FunctionHook]: return None + def get_method_signature_hook(self, fullname: str) -> Optional[MethodSignatureHook]: + return None + def get_method_hook(self, fullname: str) -> Optional[MethodHook]: return None @@ -94,6 +109,11 @@ def get_function_hook(self, fullname: str) -> Optional[FunctionHook]: return open_callback return None + def get_method_signature_hook(self, fullname: str) -> Optional[MethodSignatureHook]: + if fullname == 'typing.Mapping.get': + return typed_dict_get_signature_callback + return None + def get_method_hook(self, fullname: str) -> Optional[MethodHook]: if fullname == 'typing.Mapping.get': return typed_dict_get_callback @@ -145,6 +165,35 @@ def contextmanager_callback( return inferred_return_type +def typed_dict_get_signature_callback( + object_type: Type, + args: List[List[Expression]], + signature: CallableType, + named_generic_type: Callable[[str, List[Type]], Type]) -> CallableType: + """Try to infer a better signature type for TypedDict.get. + + This is used to get better type context for the second argument that + depends on a TypedDict value type. + """ + if (isinstance(object_type, TypedDictType) + and len(args) == 2 + and len(args[0]) == 1 + and isinstance(args[0][0], StrExpr) + and len(signature.arg_types) == 2 + and len(signature.variables) == 1): + key = args[0][0].value + value_type = object_type.items.get(key) + if value_type: + # Tweak the signature to include the value type as context. It's + # only needed for type inference since there's a union with a type + # variable that accepts everything. + tv = TypeVarType(signature.variables[0]) + return signature.copy_modified( + arg_types=[signature.arg_types[0], + UnionType.make_simplified_union([value_type, tv])]) + return signature + + def typed_dict_get_callback( object_type: Type, arg_types: List[List[Type]], @@ -173,7 +222,6 @@ def int_pow_callback( inferred_return_type: Type, named_generic_type: Callable[[str, List[Type]], Type]) -> Type: """Infer a more precise return type for int.__pow__.""" - print(arg_types) if (len(arg_types) == 1 and len(arg_types[0]) == 1): arg = args[0][0] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index b56cac8c7eca..3c979b097eae 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -761,6 +761,32 @@ reveal_type(d.get('y', None)) # E: Revealed type is 'Union[builtins.str, builtin [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] +[case testTypedDictGetMethodTypeContext] +# flags: --strict-optional +from typing import List +from mypy_extensions import TypedDict +class A: pass +D = TypedDict('D', {'x': List[int], 'y': int}) +d: D +reveal_type(d.get('x', [])) # E: Revealed type is 'builtins.list[builtins.int]' +d.get('x', ['x']) # E: List item 0 has incompatible type "str" +a = [''] +reveal_type(d.get('x', a)) # E: Revealed type is 'Union[builtins.list[builtins.int], builtins.list[builtins.str*]]' +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypedDictGetMethodInvalidArgs] +from mypy_extensions import TypedDict +D = TypedDict('D', {'x': int, 'y': str}) +d: D +d.get() # E: No overload variant of "get" of "Mapping" matches argument types [] +d.get('x', 1, 2) # E: No overload variant of "get" of "Mapping" matches argument types [builtins.str, builtins.int, builtins.int] +reveal_type(d.get('z')) # E: Revealed type is 'builtins.object*' +s = '' +reveal_type(d.get(s)) # E: # E: Revealed type is 'builtins.object*' +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + [case testTypedDictMissingMethod] from mypy_extensions import TypedDict D = TypedDict('D', {'x': int, 'y': str}) From 753d52c58b664317743df687280acafd35e0dc9a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 13:16:13 +0100 Subject: [PATCH 04/11] Report invalid TypedDict get key argument --- mypy/checkexpr.py | 12 ++++--- mypy/plugin.py | 49 +++++++++++++++++++---------- test-data/unit/check-typeddict.test | 6 ++-- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 205c851d6ce0..4435b647e7b8 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -44,7 +44,7 @@ from mypy.util import split_module_names from mypy.typevars import fill_typevars from mypy.visitor import ExpressionVisitor -from mypy.plugin import Plugin, MethodSignatureHook +from mypy.plugin import Plugin, PluginContext, MethodSignatureHook from mypy.typeanal import make_optional_type from mypy import experiments @@ -367,7 +367,8 @@ def apply_function_plugin(self, args: List[Expression], num_formals: int, fullname: Optional[str], - object_type: Optional[Type]) -> Type: + object_type: Optional[Type], + context: Context) -> Type: """Use special case logic to infer the return type for of a particular named function. Return the inferred return type. @@ -387,7 +388,7 @@ def apply_function_plugin(self, method_callback = self.plugin.get_method_hook(fullname) assert method_callback is not None # Assume that caller ensure this return method_callback(object_type, formal_arg_types, formal_arg_exprs, - inferred_ret_type, self.chk.named_generic_type) + inferred_ret_type, self.create_plugin_context(context)) def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_type: Type, signature_hook: MethodSignatureHook) -> FunctionLike: @@ -416,6 +417,9 @@ def apply_method_signature_hook(self, e: CallExpr, callee: FunctionLike, object_ items.append(adjusted) return Overloaded(items) + def create_plugin_context(self, context: Context) -> PluginContext: + return PluginContext(self.chk.named_generic_type, self.msg, context) + def check_call_expr_with_callee_type(self, callee_type: Type, e: CallExpr, @@ -508,7 +512,7 @@ def check_call(self, callee: Type, args: List[Expression], or (object_type is not None and self.plugin.get_method_hook(callable_name))): ret_type = self.apply_function_plugin( arg_types, callee.ret_type, arg_kinds, formal_to_actual, - args, len(callee.arg_types), callable_name, object_type) + args, len(callee.arg_types), callable_name, object_type, context) callee = callee.copy_modified(ret_type=ret_type) return callee.ret_type, callee elif isinstance(callee, Overloaded): diff --git a/mypy/plugin.py b/mypy/plugin.py index fd3b0a0033f8..c8fe39910d53 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -1,14 +1,23 @@ -from typing import Callable, List, Tuple, Optional +from typing import Callable, List, Tuple, Optional, NamedTuple -from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr +from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context from mypy.types import ( - Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType + Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, + AnyType ) +from mypy.messages import MessageBuilder # Create an Instance given full name of class and type arguments. NamedInstanceCallback = Callable[[str, List[Type]], Type] +# Objects and callbacks that plugins use to get information from type checking +# context or report errors. +PluginContext = NamedTuple('PluginContext', [('named_instance', NamedInstanceCallback), + ('msg', MessageBuilder), + ('context', Context)]) + + # A callback that infers the return type of a function with a special signature. # # A no-op callback would just return the inferred return type, but a useful callback @@ -39,7 +48,7 @@ List[List[Type]], # List of types caller provides for each formal argument List[List[Expression]], # Actual argument expressions for each formal argument Type, # Return type for call inferred using the regular signature - NamedInstanceCallback # Callable for constructing a named instance type + PluginContext # Access to type checking context ], Type # Return type inferred by the callback ] @@ -199,19 +208,25 @@ def typed_dict_get_callback( arg_types: List[List[Type]], args: List[List[Expression]], inferred_return_type: Type, - named_generic_type: Callable[[str, List[Type]], Type]) -> Type: + context: PluginContext) -> Type: """Infer a precise return type for TypedDict.get with literal first argument.""" if (isinstance(object_type, TypedDictType) and len(arg_types) >= 1 - and len(arg_types[0]) == 1 - and isinstance(args[0][0], StrExpr)): - key = args[0][0].value - value_type = object_type.items.get(key) - if value_type: - if len(arg_types) == 1: - return UnionType.make_simplified_union([value_type, NoneTyp()]) - elif len(arg_types) == 2 and len(arg_types[1]) == 1: - return UnionType.make_simplified_union([value_type, arg_types[1][0]]) + and len(arg_types[0]) == 1): + if isinstance(args[0][0], StrExpr): + key = args[0][0].value + value_type = object_type.items.get(key) + if value_type: + if len(arg_types) == 1: + return UnionType.make_simplified_union([value_type, NoneTyp()]) + elif len(arg_types) == 2 and len(arg_types[1]) == 1: + return UnionType.make_simplified_union([value_type, arg_types[1][0]]) + else: + context.msg.typeddict_item_name_not_found(object_type, key, context.context) + return AnyType() + else: + context.msg.typeddict_item_name_must_be_string_literal(object_type, context.context) + return AnyType() return inferred_return_type @@ -220,7 +235,7 @@ def int_pow_callback( arg_types: List[List[Type]], args: List[List[Expression]], inferred_return_type: Type, - named_generic_type: Callable[[str, List[Type]], Type]) -> Type: + context: PluginContext) -> Type: """Infer a more precise return type for int.__pow__.""" if (len(arg_types) == 1 and len(arg_types[0]) == 1): @@ -232,7 +247,7 @@ def int_pow_callback( else: return inferred_return_type if exponent >= 0: - return named_generic_type('builtins.int', []) + return context.named_instance('builtins.int', []) else: - return named_generic_type('builtins.float', []) + return context.named_instance('builtins.float', []) return inferred_return_type diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 3c979b097eae..568c1ff95d96 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -781,9 +781,11 @@ D = TypedDict('D', {'x': int, 'y': str}) d: D d.get() # E: No overload variant of "get" of "Mapping" matches argument types [] d.get('x', 1, 2) # E: No overload variant of "get" of "Mapping" matches argument types [builtins.str, builtins.int, builtins.int] -reveal_type(d.get('z')) # E: Revealed type is 'builtins.object*' +x = d.get('z') # E: 'z' is not a valid item name; expected one of ['x', 'y'] +reveal_type(x) # E: Revealed type is 'Any' s = '' -reveal_type(d.get(s)) # E: # E: Revealed type is 'builtins.object*' +y = d.get(s) # E: Cannot prove expression is a valid item name; expected one of ['x', 'y'] +reveal_type(y) # E: Revealed type is 'Any' [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] From 166a911ce4b1b10e0dea74c2bad9c37b48505bcc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 13:22:43 +0100 Subject: [PATCH 05/11] Add test case that uses typeshed stubs --- test-data/unit/pythoneval.test | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index c9ee1f322a28..91c92a39209f 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1336,3 +1336,23 @@ _program.py:13: error: Revealed type is 'def (x: builtins.int) -> contextlib.Gen _program.py:14: error: Revealed type is 'def (*x: builtins.str) -> contextlib.GeneratorContextManager[builtins.int*]' _program.py:16: error: Argument 1 to "f" has incompatible type "str"; expected "int" _program.py:17: error: Revealed type is 'builtins.str*' + +[case testTypedDictGet] +# Test that TypedDict get plugin works with typeshed stubs +# TODO: Make it possible to use strict optional here +from mypy_extensions import TypedDict +class A: pass +D = TypedDict('D', {'x': int, 'y': str}) +d: D +reveal_type(d.get('x')) +reveal_type(d.get('y')) +d.get('z') +d.get() +s = '' +d.get(s) +[out] +_testTypedDictGet.py:7: error: Revealed type is 'builtins.int' +_testTypedDictGet.py:8: error: Revealed type is 'builtins.str' +_testTypedDictGet.py:9: error: 'z' is not a valid item name; expected one of ['x', 'y'] +_testTypedDictGet.py:10: error: No overload variant of "get" of "Mapping" matches argument types [] +_testTypedDictGet.py:12: error: Cannot prove expression is a valid item name; expected one of ['x', 'y'] From b02599146a257a47e80805893ae8bebb15d50027 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 13:38:50 +0100 Subject: [PATCH 06/11] Generalize method hook to work with Instances --- mypy/checkexpr.py | 20 ++++++++++++-------- test-data/unit/check-expressions.test | 1 + 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 4435b647e7b8..28cbbb6a861b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -218,15 +218,19 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: and isinstance(e.callee, MemberExpr) and isinstance(callee_type, FunctionLike)): callee_expr_type = self.chk.type_map.get(e.callee.expr) - if isinstance(callee_expr_type, TypedDictType): + info = None + # TODO: Support fallbacks of other kinds of types as well? + if isinstance(callee_expr_type, Instance): + info = callee_expr_type.type + elif isinstance(callee_expr_type, TypedDictType): info = callee_expr_type.fallback.type.get_containing_type_info(e.callee.name) - if info: - fullname = '{}.{}'.format(info.fullname(), e.callee.name) - object_type = callee_expr_type - signature_hook = self.plugin.get_method_signature_hook(fullname) - if signature_hook: - callee_type = self.apply_method_signature_hook( - e, callee_type, object_type, signature_hook) + if info: + fullname = '{}.{}'.format(info.fullname(), e.callee.name) + object_type = callee_expr_type + signature_hook = self.plugin.get_method_signature_hook(fullname) + if signature_hook: + callee_type = self.apply_method_signature_hook( + e, callee_type, object_type, signature_hook) ret_type = self.check_call_expr_with_callee_type(callee_type, e, fullname, object_type) if isinstance(ret_type, UninhabitedType): self.chk.binder.unreachable() diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index d562a0ee189a..9f5fc395f712 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1698,4 +1698,5 @@ reveal_type(a**(-0)) # E: Revealed type is 'builtins.int' reveal_type(a**(-1)) # E: Revealed type is 'builtins.float' reveal_type(a**(-2)) # E: Revealed type is 'builtins.float' reveal_type(a**b) # E: Revealed type is 'Any' +reveal_type(a.__pow__(2)) # E: Revealed type is 'builtins.int' [builtins fixtures/ops.pyi] From 2cf5cd72057c20a06d521d1354f625ac808d9d76 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 14:55:57 +0100 Subject: [PATCH 07/11] TypedDict get tweaks Some of the tests are adapted from #2620 by @rowillia. --- mypy/checkexpr.py | 20 +++++++++++++++++--- mypy/plugin.py | 3 --- test-data/unit/check-typeddict.test | 22 ++++++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 28cbbb6a861b..a0a4fa90e122 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1848,13 +1848,14 @@ def visit_dict_expr(self, e: DictExpr) -> Type: # an error, but returns the TypedDict type that matches the literal it found # that would cause a second error when that TypedDict type is returned upstream # to avoid the second error, we always return TypedDict type that was requested - if isinstance(self.type_context[-1], TypedDictType): + typeddict_context = self.find_typeddict_context(self.type_context[-1]) + if typeddict_context: self.check_typeddict_call_with_dict( - callee=self.type_context[-1], + callee=typeddict_context, kwargs=e, context=e ) - return self.type_context[-1].copy_modified() + return typeddict_context.copy_modified() # Collect function arguments, watching out for **expr. args = [] # type: List[Expression] # Regular "key: value" @@ -1905,6 +1906,19 @@ def visit_dict_expr(self, e: DictExpr) -> Type: self.check_call(method, [arg], [nodes.ARG_POS], arg) return rv + def find_typeddict_context(self, context: Type) -> Optional[TypedDictType]: + if isinstance(context, TypedDictType): + return context + elif isinstance(context, UnionType): + items = [] + for item in context.items: + item_context = self.find_typeddict_context(item) + if item_context: + items.append(item_context) + if len(items) == 1: + return items[0] + return None + def visit_lambda_expr(self, e: LambdaExpr) -> Type: """Type check lambda expression.""" inferred_type, type_override = self.infer_lambda_type_using_context(e) diff --git a/mypy/plugin.py b/mypy/plugin.py index c8fe39910d53..adc4074b4ce3 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -224,9 +224,6 @@ def typed_dict_get_callback( else: context.msg.typeddict_item_name_not_found(object_type, key, context.context) return AnyType() - else: - context.msg.typeddict_item_name_must_be_string_literal(object_type, context.context) - return AnyType() return inferred_return_type diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 568c1ff95d96..c29aad48af92 100644 --- a/test-data/unit/check-typeddict.test +++ b/test-data/unit/check-typeddict.test @@ -784,8 +784,8 @@ d.get('x', 1, 2) # E: No overload variant of "get" of "Mapping" matches argument x = d.get('z') # E: 'z' is not a valid item name; expected one of ['x', 'y'] reveal_type(x) # E: Revealed type is 'Any' s = '' -y = d.get(s) # E: Cannot prove expression is a valid item name; expected one of ['x', 'y'] -reveal_type(y) # E: Revealed type is 'Any' +y = d.get(s) +reveal_type(y) # E: Revealed type is 'builtins.object*' [builtins fixtures/dict.pyi] [typing fixtures/typing-full.pyi] @@ -795,3 +795,21 @@ D = TypedDict('D', {'x': int, 'y': str}) d: D d.bad(1) # E: "D" has no attribute "bad" [builtins fixtures/dict.pyi] + +[case testTypedDictChainedGetMethodWithDictFallback] +from mypy_extensions import TypedDict +D = TypedDict('D', {'x': int, 'y': str}) +E = TypedDict('E', {'d': D}) +p = E(d=D(x=0, y='')) +reveal_type(p.get('d', {'x': 1, 'y': ''})) # E: Revealed type is 'TypedDict(x=builtins.int, y=builtins.str, _fallback=__main__.D)' +p.get('d', {}) # E: Expected items ['x', 'y'] but found []. +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypedDictGetDefaultParameterStillTypeChecked] +from mypy_extensions import TypedDict +TaggedPoint = TypedDict('TaggedPoint', {'type': str, 'x': int, 'y': int}) +p = TaggedPoint(type='2d', x=42, y=1337) +p.get('x', 1 + 'y') # E: Unsupported operand types for + ("int" and "str") +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] From abe29a96610377e8b0e742a6779072663bffd1a7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 15:18:54 +0100 Subject: [PATCH 08/11] Remove commented-out code --- mypy/plugin.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/mypy/plugin.py b/mypy/plugin.py index adc4074b4ce3..1dc211d97132 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -53,27 +53,6 @@ Type # Return type inferred by the callback ] -# Used to provide a custom syntax for a type. -# -# TODO: Maybe we should allow more stuff here, such as arbitrary string and int literals? -#TypeAnalyzeHook = Callable[ -# [ -# Expression, # The <expression> in C[<expression>] -# Callable[[Type], Type], # Callback for running semantic analysis -# NamedInstanceCallback -# ], -# Type # Representation of the type -#] - -# Used to provide a custom string representation for a class. -#TypeToStrHook = Callable[ -# [ -# Type, -# Callable[[Type], str], # Callback for ordinary pretty printing -# ], -# str -#] - class Plugin: """Base class of type checker plugins. @@ -99,12 +78,6 @@ def get_method_signature_hook(self, fullname: str) -> Optional[MethodSignatureHo def get_method_hook(self, fullname: str) -> Optional[MethodHook]: return None - #def get_type_analyze_hook(self, fullname: str) -> Optional[TypeAnalyzeHook]: - # return None - - #def get_type_to_str_hook(self, fullname: str) -> Optional[TypeToStrHook]: - # return None - # TODO: metaclass / class decorator hook From 8a240dc294718e84d4f13195852e88698c80e1e9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 15:36:27 +0100 Subject: [PATCH 09/11] Various tweaks --- mypy/checkexpr.py | 15 ++++++++++++--- mypy/plugin.py | 10 ++++++++-- test-data/unit/check-expressions.test | 2 ++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index a0a4fa90e122..d089b56a36d6 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -209,6 +209,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: isinstance(callee_type, CallableType) and callee_type.implicit): return self.msg.untyped_function_call(callee_type, e) + # Figure out the full name of the callee for plugin loopup. object_type = None if not isinstance(e.callee, RefExpr): fullname = None @@ -217,6 +218,8 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: if (fullname is None and isinstance(e.callee, MemberExpr) and isinstance(callee_type, FunctionLike)): + # For method calls we include the defining class for the method + # in the full name (example: 'typing.Mapping.get'). callee_expr_type = self.chk.type_map.get(e.callee.expr) info = None # TODO: Support fallbacks of other kinds of types as well? @@ -227,6 +230,7 @@ def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type: if info: fullname = '{}.{}'.format(info.fullname(), e.callee.name) object_type = callee_expr_type + # Apply plugin signature hook that may generate a better signature. signature_hook = self.plugin.get_method_signature_hook(fullname) if signature_hook: callee_type = self.apply_method_signature_hook( @@ -373,7 +377,7 @@ def apply_function_plugin(self, fullname: Optional[str], object_type: Optional[Type], context: Context) -> Type: - """Use special case logic to infer the return type for of a particular named function. + """Use special case logic to infer the return type of a specific named function/method. Return the inferred return type. """ @@ -384,13 +388,15 @@ def apply_function_plugin(self, formal_arg_types[formal].append(arg_types[actual]) formal_arg_exprs[formal].append(args[actual]) if object_type is None: + # Apply function plugin callback = self.plugin.get_function_hook(fullname) - assert callback is not None # Assume that caller ensure this + assert callback is not None # Assume that caller ensures this return callback(formal_arg_types, formal_arg_exprs, inferred_ret_type, self.chk.named_generic_type) else: + # Apply method plugin method_callback = self.plugin.get_method_hook(fullname) - assert method_callback is not None # Assume that caller ensure this + assert method_callback is not None # Assume that caller ensures this return method_callback(object_type, formal_arg_types, formal_arg_exprs, inferred_ret_type, self.create_plugin_context(context)) @@ -1371,6 +1377,7 @@ def check_op_local(self, method: str, base_type: Type, arg: Expression, object_type = None if isinstance(base_type, Instance): # TODO: Find out in which class the method was defined originally? + # TODO: Support non-Instance types. callable_name = '{}.{}'.format(base_type.type.fullname(), method) object_type = base_type return self.check_call(method_type, [arg], [nodes.ARG_POS], @@ -1916,7 +1923,9 @@ def find_typeddict_context(self, context: Type) -> Optional[TypedDictType]: if item_context: items.append(item_context) if len(items) == 1: + # Only one union item is TypedDict, so use the context as it's unambiguous. return items[0] + # No TypedDict type in context. return None def visit_lambda_expr(self, e: LambdaExpr) -> Type: diff --git a/mypy/plugin.py b/mypy/plugin.py index 1dc211d97132..5015f7b4c940 100644 --- a/mypy/plugin.py +++ b/mypy/plugin.py @@ -11,8 +11,8 @@ # Create an Instance given full name of class and type arguments. NamedInstanceCallback = Callable[[str, List[Type]], Type] -# Objects and callbacks that plugins use to get information from type checking -# context or report errors. +# Some objects and callbacks that plugins can use to get information from the +# type checker or to report errors. PluginContext = NamedTuple('PluginContext', [('named_instance', NamedInstanceCallback), ('msg', MessageBuilder), ('context', Context)]) @@ -32,6 +32,8 @@ Type # Return type inferred by the callback ] +# A callback that may infer a better signature for a method. Note that argument types aren't +# available yet. If you need them, you have to use a MethodHook instead. MethodSignatureHook = Callable[ [ Type, # Base object type @@ -42,6 +44,9 @@ CallableType # Potentially more precise signature inferred for the method ] +# A callback that infers the return type of a method with a special signature. +# +# This is pretty similar to FunctionHook. MethodHook = Callable[ [ Type, # Base object type @@ -215,6 +220,7 @@ def int_pow_callback( elif isinstance(arg, UnaryExpr) and arg.op == '-' and isinstance(arg.expr, IntExpr): exponent = -arg.expr.value else: + # Right operand not an int literal or a negated literal -- give up. return inferred_return_type if exponent >= 0: return context.named_instance('builtins.int', []) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 9f5fc395f712..cc8c10945589 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1699,4 +1699,6 @@ reveal_type(a**(-1)) # E: Revealed type is 'builtins.float' reveal_type(a**(-2)) # E: Revealed type is 'builtins.float' reveal_type(a**b) # E: Revealed type is 'Any' reveal_type(a.__pow__(2)) # E: Revealed type is 'builtins.int' +reveal_type(a.__pow__(a)) # E: Revealed type is 'Any' +a.__pow__() # E: Too few arguments for "__pow__" of "int" [builtins fixtures/ops.pyi] From e43d94a60d716c47c1a7e5981c8d3fdd4fa74dc9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Tue, 6 Jun 2017 15:41:50 +0100 Subject: [PATCH 10/11] Fix test case --- test-data/unit/pythoneval.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 91c92a39209f..1035a436c53d 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1349,10 +1349,10 @@ reveal_type(d.get('y')) d.get('z') d.get() s = '' -d.get(s) +reveal_type(d.get(s)) [out] _testTypedDictGet.py:7: error: Revealed type is 'builtins.int' _testTypedDictGet.py:8: error: Revealed type is 'builtins.str' _testTypedDictGet.py:9: error: 'z' is not a valid item name; expected one of ['x', 'y'] _testTypedDictGet.py:10: error: No overload variant of "get" of "Mapping" matches argument types [] -_testTypedDictGet.py:12: error: Cannot prove expression is a valid item name; expected one of ['x', 'y'] +_testTypedDictGet.py:12: error: Revealed type is 'builtins.object*' From 6db7d204172722e03ebf453eed4ed1008bc2624e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo <jukka.lehtosalo@iki.fi> Date: Wed, 7 Jun 2017 17:29:54 +0100 Subject: [PATCH 11/11] Address review feedback --- mypy/checker.py | 4 ++-- test-data/unit/check-expressions.test | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 89730f894d24..89fd4d9ad987 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -133,7 +133,7 @@ class TypeChecker(NodeVisitor[None]): plugin = None # type: Plugin def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options, - tree: MypyFile, path: str, plugin: Optional[Plugin] = None) -> None: + tree: MypyFile, path: str, plugin: Plugin) -> None: """Construct a type checker. Use errors to report type check errors. @@ -144,7 +144,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option self.tree = tree self.path = path self.msg = MessageBuilder(errors, modules) - self.plugin = plugin or Plugin(options.python_version) + self.plugin = plugin self.expr_checker = mypy.checkexpr.ExpressionChecker(self, self.msg, self.plugin) self.scope = Scope(tree) self.binder = ConditionalTypeBinder() diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index cc8c10945589..ab2bd1e92543 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1694,8 +1694,8 @@ b = a + 2 reveal_type(a**0) # E: Revealed type is 'builtins.int' reveal_type(a**1) # E: Revealed type is 'builtins.int' reveal_type(a**2) # E: Revealed type is 'builtins.int' -reveal_type(a**(-0)) # E: Revealed type is 'builtins.int' -reveal_type(a**(-1)) # E: Revealed type is 'builtins.float' +reveal_type(a**-0) # E: Revealed type is 'builtins.int' +reveal_type(a**-1) # E: Revealed type is 'builtins.float' reveal_type(a**(-2)) # E: Revealed type is 'builtins.float' reveal_type(a**b) # E: Revealed type is 'Any' reveal_type(a.__pow__(2)) # E: Revealed type is 'builtins.int'