From 7f7752b98498b8a72e91333767fd063ea8cbe4c9 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 24 May 2019 13:46:03 -0700 Subject: [PATCH 1/3] dmypy suggest: handful of bug fixes/improvements * Fix --no-any to not spuriously reject all methods * Support inferring types of decorated methods if the decorator doesn't change the type * Refuse to infer functions with then or more arguments to avoid spending forever calculating an exponentially large number of possibilities before deciding there are too many possibilities. --- mypy/suggestions.py | 40 +++++++++- test-data/unit/fine-grained-suggest.test | 97 +++++++++++++++++++++++- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 6bdfe1fe0b73..6d1e5dbf5e76 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -33,8 +33,9 @@ ) from mypy.build import State, Graph from mypy.nodes import ( - ARG_STAR, ARG_NAMED, ARG_STAR2, ARG_NAMED_OPT, FuncDef, MypyFile, SymbolTable, - SymbolNode, TypeInfo, Expression, ReturnStmt, + ARG_POS, ARG_STAR, ARG_NAMED, ARG_STAR2, ARG_NAMED_OPT, FuncDef, MypyFile, SymbolTable, + Decorator, RefExpr, + SymbolNode, TypeInfo, Node, Expression, ReturnStmt, CallExpr, reverse_builtin_aliases, ) from mypy.server.update import FineGrainedBuildManager @@ -220,8 +221,9 @@ def get_args(self, is_method: bool, types = [] # type: List[List[Type]] for i in range(len(base.arg_kinds)): # Make self args Any but this will get overriden somewhere in the checker + # We call this a special form so that has_any_type doesn't consider it to be a real any if i == 0 and is_method: - types.append([AnyType(TypeOfAny.explicit)]) + types.append([AnyType(TypeOfAny.special_form)]) continue all_arg_types = [] @@ -307,6 +309,9 @@ def get_suggestion(self, function: str) -> str: # FIXME: what about static and class methods? is_method = bool(node.info) + if len(node.arg_names) >= 10: + raise SuggestionFailure("Too many arguments") + with strict_optional_set(graph[mod].options.strict_optional): guesses = self.get_guesses( is_method, @@ -388,11 +393,40 @@ def find_node(self, key: str) -> Tuple[str, str, FuncDef]: raise SuggestionFailure("Unknown %s %s" % ("method" if len(components) > 1 else "function", key)) node = names[funcname].node + if isinstance(node, Decorator): + node = self.extract_from_decorator(node) + if not node: + raise SuggestionFailure("Object %s is a decorator we can't handle" % key) + if not isinstance(node, FuncDef): raise SuggestionFailure("Object %s is not a function" % key) return (modname, tail, node) + def extract_from_decorator(self, node: Decorator) -> Optional[FuncDef]: + for dec in node.decorators: + print(dec) + typ = None + if (isinstance(dec, RefExpr) + and isinstance(dec.node, FuncDef)): + typ = dec.node.type + elif (isinstance(dec, CallExpr) + and isinstance(dec.callee, RefExpr) + and isinstance(dec.callee.node, FuncDef) + and isinstance(dec.callee.node.type, CallableType) + ): + typ = dec.callee.node.type.ret_type + + if not isinstance(typ, FunctionLike): + return None + for ct in typ.items(): + if not (len(ct.arg_types) == 1 + and isinstance(ct.arg_types[0], TypeVarType) + and ct.arg_types[0] == ct.ret_type): + return None + + return node.func + def try_type(self, func: FuncDef, typ: Type) -> List[str]: """Recheck a function while assuming it has type typ. diff --git a/test-data/unit/fine-grained-suggest.test b/test-data/unit/fine-grained-suggest.test index 534252d42471..108fbc075a2d 100644 --- a/test-data/unit/fine-grained-suggest.test +++ b/test-data/unit/fine-grained-suggest.test @@ -177,7 +177,7 @@ Foo('lol') [case testSuggestInferMethod1] # flags: --strict-optional -# suggest: foo.Foo.foo +# suggest: --no-any foo.Foo.foo [file foo.py] class Foo: def __init__(self) -> None: @@ -348,3 +348,98 @@ def baz() -> None: (Union[str, int, None], Union[int, None]) -> object (Union[str, int, None], Union[int, None]) -> None == + +[case testSuggestInferFuncDecorator1] +# flags: --strict-optional +# suggest: foo.foo +[file foo.py] +from typing import TypeVar +F = TypeVar('F') + +def dec(x: F) -> F: + return x + +@dec +def foo(arg): + return arg +[file bar.py] +from foo import foo +def bar() -> None: + foo('abc') +[builtins fixtures/isinstancelist.pyi] +[out] +(str) -> str +== + +[case testSuggestInferFuncDecorator2] +# flags: --strict-optional +# suggest: foo.foo +[file foo.py] +from typing import TypeVar, Callable, Any +F = TypeVar('F', bound=Callable[..., Any]) + +def dec(x: F) -> F: + return x + +@dec +def foo(arg): + return arg +[file bar.py] +from foo import foo +def bar() -> None: + foo('abc') +[builtins fixtures/isinstancelist.pyi] +[out] +(str) -> str +== + +[case testSuggestInferFuncDecorator3] +# flags: --strict-optional +# suggest: foo.foo +[file foo.py] +from typing import TypeVar, Callable, Any +F = TypeVar('F', bound=Callable[..., Any]) + +def dec(s: str) -> Callable[[F], F]: + def f(x: F) -> F: + return x + return f + +@dec('lol') +def foo(arg): + return arg +[file bar.py] +from foo import foo +def bar() -> None: + foo('abc') +[builtins fixtures/isinstancelist.pyi] +[out] +(str) -> str +== + +[case testSuggestInferFuncDecorator4] +# flags: --strict-optional +# suggest: foo.foo +[file dec.py] +from typing import TypeVar, Callable, Any +F = TypeVar('F', bound=Callable[..., Any]) + +def dec(s: str) -> Callable[[F], F]: + def f(x: F) -> F: + return x + return f + +[file foo.py] +import dec + +@dec.dec('lol') +def foo(arg): + return arg +[file bar.py] +from foo import foo +def bar() -> None: + foo('abc') +[builtins fixtures/isinstancelist.pyi] +[out] +(str) -> str +== From 1940fde5296a5dbdb9746cc307b03262f08a878b Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 14 Jun 2019 17:01:28 -0700 Subject: [PATCH 2/3] lint; I am *not* convinced about these continuation line requirements --- mypy/suggestions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 6d1e5dbf5e76..399c19bb13d8 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -29,13 +29,14 @@ from mypy.state import strict_optional_set from mypy.types import ( Type, AnyType, TypeOfAny, CallableType, UnionType, NoneType, Instance, TupleType, is_optional, + TypeVarType, FunctionLike, TypeStrVisitor, ) from mypy.build import State, Graph from mypy.nodes import ( - ARG_POS, ARG_STAR, ARG_NAMED, ARG_STAR2, ARG_NAMED_OPT, FuncDef, MypyFile, SymbolTable, + ARG_STAR, ARG_NAMED, ARG_STAR2, ARG_NAMED_OPT, FuncDef, MypyFile, SymbolTable, Decorator, RefExpr, - SymbolNode, TypeInfo, Node, Expression, ReturnStmt, CallExpr, + SymbolNode, TypeInfo, Expression, ReturnStmt, CallExpr, reverse_builtin_aliases, ) from mypy.server.update import FineGrainedBuildManager @@ -413,16 +414,15 @@ def extract_from_decorator(self, node: Decorator) -> Optional[FuncDef]: elif (isinstance(dec, CallExpr) and isinstance(dec.callee, RefExpr) and isinstance(dec.callee.node, FuncDef) - and isinstance(dec.callee.node.type, CallableType) - ): + and isinstance(dec.callee.node.type, CallableType)): typ = dec.callee.node.type.ret_type if not isinstance(typ, FunctionLike): return None for ct in typ.items(): if not (len(ct.arg_types) == 1 - and isinstance(ct.arg_types[0], TypeVarType) - and ct.arg_types[0] == ct.ret_type): + and isinstance(ct.arg_types[0], TypeVarType) + and ct.arg_types[0] == ct.ret_type): return None return node.func From 05697b6a71cacd363d8ab525c596b85e90f2e06a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 17 Jun 2019 16:35:11 -0700 Subject: [PATCH 3/3] clean up stray debug print --- mypy/suggestions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 399c19bb13d8..cc616decee1e 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -406,7 +406,6 @@ def find_node(self, key: str) -> Tuple[str, str, FuncDef]: def extract_from_decorator(self, node: Decorator) -> Optional[FuncDef]: for dec in node.decorators: - print(dec) typ = None if (isinstance(dec, RefExpr) and isinstance(dec.node, FuncDef)):