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..89fd4d9ad987 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: Plugin) -> 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 + 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..d089b56a36d6 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, PluginContext, MethodSignatureHook 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. @@ -208,11 +209,33 @@ 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 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) + 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? + 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 + # 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( + 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() if not allow_none_return and isinstance(ret_type, NoneTyp): @@ -351,8 +374,10 @@ def apply_function_plugin(self, formal_to_actual: List[List[int]], args: List[Expression], num_formals: int, - fullname: Optional[str]) -> Type: - """Use special case logic to infer the return type for of a particular named function. + fullname: Optional[str], + object_type: Optional[Type], + context: Context) -> Type: + """Use special case logic to infer the return type of a specific named function/method. Return the inferred return type. """ @@ -362,11 +387,54 @@ 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) - - def check_call_expr_with_callee_type(self, callee_type: Type, - e: CallExpr, callable_name: Optional[str]) -> Type: + if object_type is None: + # Apply function plugin + callback = self.plugin.get_function_hook(fullname) + 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 ensures this + return method_callback(object_type, formal_arg_types, formal_arg_exprs, + 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: + """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 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, + callable_name: Optional[str], + object_type: Optional[Type]) -> Type: """Type check call expression. The given callee type overrides the type of the callee @@ -374,14 +442,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. @@ -389,14 +459,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): @@ -443,10 +517,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 callable_name in self.function_plugins: + + 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, context) callee = callee.copy_modified(ret_type=ret_type) return callee.ret_type, callee elif isinstance(callee, Overloaded): @@ -461,7 +537,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() @@ -1295,8 +1373,16 @@ 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? + # 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], - 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, @@ -1769,13 +1855,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" @@ -1826,6 +1913,21 @@ 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: + # 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: """Type check lambda expression.""" inferred_type, type_override = self.infer_lambda_type_using_context(e) diff --git a/mypy/funcplugins.py b/mypy/funcplugins.py deleted file mode 100644 index b1113ab30ae9..000000000000 --- a/mypy/funcplugins.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Plugins that implement special type checking rules for individual functions. - -The plugins infer better types for tricky functions such as "open". -""" - -from typing import Tuple, Dict, Callable, List - -from mypy.nodes import Expression, StrExpr -from mypy.types import Type, Instance, CallableType - - -# 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[ - [ - 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 - ], - 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, - } - - -def open_callback( - arg_types: List[List[Type]], - args: List[List[Expression]], - inferred_return_type: Type, - named_generic_type: Callable[[str, List[Type]], Type]) -> Type: - """Infer a better return type for 'open'. - - Infer TextIO or BinaryIO as the return value if the mode argument is not - given or is a literal. - """ - mode = None - if not arg_types or len(arg_types[1]) != 1: - mode = 'r' - elif isinstance(args[1][0], StrExpr): - mode = args[1][0].value - if mode is not None: - assert isinstance(inferred_return_type, Instance) - if 'b' in mode: - return named_generic_type('typing.BinaryIO', []) - else: - return named_generic_type('typing.TextIO', []) - return inferred_return_type - - -def contextmanager_callback( - arg_types: List[List[Type]], - args: List[List[Expression]], - inferred_return_type: Type, - named_generic_type: Callable[[str, List[Type]], Type]) -> Type: - """Infer a better return type for 'contextlib.contextmanager'.""" - # Be defensive, just in case. - if arg_types and len(arg_types[0]) == 1: - arg_type = arg_types[0][0] - if isinstance(arg_type, CallableType) and isinstance(inferred_return_type, CallableType): - # The stub signature doesn't preserve information about arguments so - # add them back here. - return inferred_return_type.copy_modified( - arg_types=arg_type.arg_types, - arg_kinds=arg_type.arg_kinds, - arg_names=arg_type.arg_names) - return inferred_return_type 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 new file mode 100644 index 000000000000..5015f7b4c940 --- /dev/null +++ b/mypy/plugin.py @@ -0,0 +1,229 @@ +from typing import Callable, List, Tuple, Optional, NamedTuple + +from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context +from mypy.types import ( + 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] + +# 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)]) + + +# 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. +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 + NamedInstanceCallback # Callable for constructing a named instance type + ], + 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 + 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 +] + +# 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 + 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 + PluginContext # Access to type checking context + ], + Type # Return type inferred by the 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 + + def get_method_signature_hook(self, fullname: str) -> Optional[MethodSignatureHook]: + return None + + def get_method_hook(self, fullname: str) -> Optional[MethodHook]: + return None + + # TODO: metaclass / class decorator hook + + +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 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 + elif fullname == 'builtins.int.__pow__': + return int_pow_callback + return None + + +def open_callback( + arg_types: List[List[Type]], + args: List[List[Expression]], + inferred_return_type: Type, + named_generic_type: Callable[[str, List[Type]], Type]) -> Type: + """Infer a better return type for 'open'. + + Infer TextIO or BinaryIO as the return value if the mode argument is not + given or is a literal. + """ + mode = None + if not arg_types or len(arg_types[1]) != 1: + mode = 'r' + elif isinstance(args[1][0], StrExpr): + mode = args[1][0].value + if mode is not None: + assert isinstance(inferred_return_type, Instance) + if 'b' in mode: + return named_generic_type('typing.BinaryIO', []) + else: + return named_generic_type('typing.TextIO', []) + return inferred_return_type + + +def contextmanager_callback( + arg_types: List[List[Type]], + args: List[List[Expression]], + inferred_return_type: Type, + named_generic_type: Callable[[str, List[Type]], Type]) -> Type: + """Infer a better return type for 'contextlib.contextmanager'.""" + # Be defensive, just in case. + if arg_types and len(arg_types[0]) == 1: + arg_type = arg_types[0][0] + if isinstance(arg_type, CallableType) and isinstance(inferred_return_type, CallableType): + # The stub signature doesn't preserve information about arguments so + # add them back here. + return inferred_return_type.copy_modified( + arg_types=arg_type.arg_types, + arg_kinds=arg_type.arg_kinds, + arg_names=arg_type.arg_names) + 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]], + args: List[List[Expression]], + inferred_return_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): + 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() + return inferred_return_type + + +def int_pow_callback( + object_type: Type, + arg_types: List[List[Type]], + args: List[List[Expression]], + inferred_return_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): + 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: + # 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', []) + else: + return context.named_instance('builtins.float', []) + return inferred_return_type diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index ae1498acdadd..ab2bd1e92543 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -1682,3 +1682,23 @@ 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' +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] diff --git a/test-data/unit/check-typeddict.test b/test-data/unit/check-typeddict.test index 4714ec77f3dc..c29aad48af92 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 '' -- 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,73 @@ 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 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] +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) +reveal_type(y) # 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}) +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] 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 diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index c9ee1f322a28..1035a436c53d 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 = '' +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: Revealed type is 'builtins.object*'