From b242f7e4067819e6d219573affb434f467e7de62 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Sep 2019 18:28:45 +0100 Subject: [PATCH 01/10] Some refactoring --- mypy/suggestions.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 6e987d455ae4..59f249bcb2ed 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -174,19 +174,21 @@ def __init__(self, fgmanager: FineGrainedBuildManager, def suggest(self, function: str) -> str: """Suggest an inferred type for function.""" - with self.restore_after(function): + mod, func_name, node = self.find_node(function) + + with self.restore_after(mod): with self.with_export_types(): - suggestion = self.get_suggestion(function) + suggestion = self.get_suggestion(mod, node) if self.give_json: - return self.json_suggestion(function, suggestion) + return self.json_suggestion(mod, func_name, node, suggestion) else: return self.format_signature(suggestion) def suggest_callsites(self, function: str) -> str: """Find a list of call sites of function.""" - with self.restore_after(function): - _, _, node = self.find_node(function) + mod, _, node = self.find_node(function) + with self.restore_after(mod): callsites, _ = self.get_callsites(node) return '\n'.join(dedup( @@ -195,7 +197,7 @@ def suggest_callsites(self, function: str) -> str: )) @contextmanager - def restore_after(self, target: str) -> Iterator[None]: + def restore_after(self, module: str) -> Iterator[None]: """Context manager that reloads a module after executing the body. This should undo any damage done to the module state while mucking around. @@ -203,9 +205,7 @@ def restore_after(self, target: str) -> Iterator[None]: try: yield finally: - module = module_prefix(self.graph, target) - if module: - self.reload(self.graph[module]) + self.reload(self.graph[module]) @contextmanager def with_export_types(self) -> Iterator[None]: @@ -321,13 +321,12 @@ def find_best(self, func: FuncDef, guesses: List[CallableType]) -> Tuple[Callabl key=lambda s: (count_errors(errors[s]), self.score_callable(s))) return best, count_errors(errors[best]) - def get_suggestion(self, function: str) -> PyAnnotateSignature: + def get_suggestion(self, mod: str, node: FuncDef) -> PyAnnotateSignature: """Compute a suggestion for a function. Return the type and whether the first argument should be ignored. """ graph = self.graph - mod, _, node = self.find_node(function) callsites, orig_errors = self.get_callsites(node) if self.no_errors and orig_errors: @@ -493,9 +492,9 @@ def ensure_loaded(self, state: State) -> MypyFile: def builtin_type(self, s: str) -> Instance: return self.manager.semantic_analyzer.builtin_type(s) - def json_suggestion(self, function: str, suggestion: PyAnnotateSignature) -> str: + def json_suggestion(self, mod: str, func_name: str, node: FuncDef, + suggestion: PyAnnotateSignature) -> str: """Produce a json blob for a suggestion suitable for application by pyannotate.""" - mod, func_name, node = self.find_node(function) # pyannotate irritatingly drops class names for class and static methods if node.is_class or node.is_static: func_name = func_name.split('.', 1)[-1] From 9d4eb25829d5fcb76fa60f650d087d9e8a89c7df Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Sep 2019 20:12:07 +0100 Subject: [PATCH 02/10] Add feature support --- mypy/nodes.py | 3 ++ mypy/semanal_main.py | 3 +- mypy/suggestions.py | 76 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 79343f4ad212..98152e52538a 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -247,6 +247,8 @@ class MypyFile(SymbolNode): # If the value is empty, ignore all errors; otherwise, the list contains all # error codes to ignore. ignored_lines = None # type: Dict[int, List[str]] + # This map allow find quickly a top level function/method by its line number. + line_node_map = None # type: Dict[int, Union[FuncDef, OverloadedFuncDef, Decorator]] # Is this file represented by a stub file (.pyi)? is_stub = False # Is this loaded from the cache and thus missing the actual body of the file? @@ -274,6 +276,7 @@ def __init__(self, self.ignored_lines = ignored_lines else: self.ignored_lines = {} + self.line_node_map = {} def local_definitions(self) -> Iterator[Definition]: """Return all definitions within the module (including nested). diff --git a/mypy/semanal_main.py b/mypy/semanal_main.py index cc28d00f250e..577a84eaf4e0 100644 --- a/mypy/semanal_main.py +++ b/mypy/semanal_main.py @@ -250,6 +250,8 @@ def process_top_level_function(analyzer: 'SemanticAnalyzer', Process the body of the function (including nested functions) again and again, until all names have been resolved (ot iteration limit reached). """ + assert state.tree is not None + state.tree.line_node_map[node.line] = node # We need one more iteration after incomplete is False (e.g. to report errors, if any). final_iteration = False incomplete = True @@ -263,7 +265,6 @@ def process_top_level_function(analyzer: 'SemanticAnalyzer', iteration += 1 if iteration == MAX_ITERATIONS: # Just pick some module inside the current SCC for error context. - assert state.tree is not None with analyzer.file_context(state.tree, state.options): analyzer.report_hang() break diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 59f249bcb2ed..029a809ed741 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -42,7 +42,9 @@ reverse_builtin_aliases, ) from mypy.server.update import FineGrainedBuildManager -from mypy.util import module_prefix, split_target +from mypy.util import split_target +from mypy.find_sources import SourceFinder, InvalidSourceList +from mypy.modulefinder import PYTHON_EXTENSIONS from mypy.plugin import Plugin, FunctionContext, MethodContext from mypy.traverser import TraverserVisitor from mypy.checkexpr import has_any_type @@ -162,6 +164,7 @@ def __init__(self, fgmanager: FineGrainedBuildManager, self.manager = fgmanager.manager self.plugin = self.manager.plugin self.graph = fgmanager.graph + self.finder = SourceFinder(self.manager.fscache) self.give_json = json self.no_errors = no_errors @@ -385,15 +388,45 @@ def format_args(self, return "(%s)" % (", ".join(args)) def find_node(self, key: str) -> Tuple[str, str, FuncDef]: - """From a target name, return module/target names and the func def.""" + """From a target name, return module/target names and the func def. + + The 'key' argument can be in one of two formats: + * As the function full name, e.g., package.module.Cls.method + * As the function location as file and line separated by column, + e.g., path/to/file.py:42 + """ # TODO: Also return OverloadedFuncDef -- currently these are ignored. - graph = self.fgmanager.graph - target = split_target(graph, key) - if not target: - raise SuggestionFailure("Cannot find module for %s" % (key,)) - modname, tail = target + node = None # type: Optional[SymbolNode] + if ':' in key: + file, line = key.split(':') + if not line.isdigit(): + raise SuggestionFailure('Line number must be a number. Got {}'.format(line)) + line_number = int(line) + modname, node = self.find_node_by_file_and_line(file, line_number) + tail = node.fullname()[len(modname) + 1:] # add one to account for '.' + else: + target = split_target(self.fgmanager.graph, key) + if not target: + raise SuggestionFailure("Cannot find module for %s" % (key,)) + modname, tail = target + node = self.find_node_by_module_and_name(modname, tail) + + 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) - tree = self.ensure_loaded(graph[modname]) + return modname, tail, node + + def find_node_by_module_and_name(self, modname: str, tail: str) -> Optional[SymbolNode]: + """Find symbol node by module id and qualified name. + + Raise SuggestionFailure if can't find one. + """ + tree = self.ensure_loaded(self.fgmanager.graph[modname]) # N.B. This is reimplemented from update's lookup_target # basically just to produce better error messages. @@ -415,18 +448,29 @@ def find_node(self, key: str) -> Tuple[str, str, FuncDef]: # Look for the actual function/method funcname = components[-1] if funcname not in names: + key = modname + '.' + tail 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) + return names[funcname].node - if not isinstance(node, FuncDef): - raise SuggestionFailure("Object %s is not a function" % key) + def find_node_by_file_and_line(self, file: str, line: int) -> Tuple[str, SymbolNode]: + """Find symbol node by path to file and line number. - return (modname, tail, node) + Return module id and the node found. Raise SuggestionFailure if can't find one. + """ + if not any(file.endswith(ext) for ext in PYTHON_EXTENSIONS): + raise SuggestionFailure('Source file is not a Python file') + try: + modname, _ = self.finder.crawl_up(os.path.normpath(file)) + except InvalidSourceList: + raise SuggestionFailure('Invalid source file name: ' + file) + if modname not in self.graph: + raise SuggestionFailure('Unknown module: ' + modname) + tree = self.ensure_loaded(self.fgmanager.graph[modname]) + if line not in tree.line_node_map: + raise SuggestionFailure('Cannot find a function at line {}'.format(line)) + node = tree.line_node_map[line] + return modname, node def extract_from_decorator(self, node: Decorator) -> Optional[FuncDef]: for dec in node.decorators: From d84344ef50b4d8b75e8fa6f918eee92e61b012d2 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Sep 2019 20:48:11 +0100 Subject: [PATCH 03/10] Start adding tests --- mypy/test/testfinegrained.py | 2 +- test-data/unit/fine-grained-suggest.test | 47 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 736cfe000bb0..1fb6c91ad945 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -291,7 +291,7 @@ def maybe_suggest(self, step: int, server: Server, src: str) -> List[str]: def get_suggest(self, program_text: str, incremental_step: int) -> List[Tuple[str, str]]: step_bit = '1?' if incremental_step == 1 else str(incremental_step) - regex = '# suggest{}: (--[a-zA-Z0-9_\\-./=?^ ]+ )*([a-zA-Z0-9_./?^ ]+)$'.format(step_bit) + regex = '# suggest{}: (--[a-zA-Z0-9_\\-./=?^ ]+ )*([a-zA-Z0-9_.:/?^ ]+)$'.format(step_bit) m = re.findall(regex, program_text, flags=re.MULTILINE) return m diff --git a/test-data/unit/fine-grained-suggest.test b/test-data/unit/fine-grained-suggest.test index d24a2909559b..0c0a82d71a06 100644 --- a/test-data/unit/fine-grained-suggest.test +++ b/test-data/unit/fine-grained-suggest.test @@ -607,3 +607,50 @@ def bar(iany) -> None: (int, int) -> int (str, int) -> str == + +[case testSuggestColonBasic] +# suggest: tmp/foo.py:1 +# suggest: tmp/bar/baz.py:2 +[file foo.py] +def func(arg): + return 0 +func('test') +from bar.baz import C +C().method('test') +[file bar/__init__.py] +[file bar/baz.py] +class C: + def method(self, x): + return 0 +[out] +(str) -> int +(str) -> int +== + +[case testSuggestColonBadLine] +# suggest: +[out] + +[case testSuggestColonBadFiles] +# suggest: +[out] + +[case testSuggestColonUnknownLine] +# suggest: +[out] + +[case testSuggestColonClass] +# suggest: +[out] + +[case testSuggestColonDecorator] +# suggest: +[out] + +[case testSuggestColonMethod] +# suggest: +[out] + +[case testSuggestColonMethodJSON] +# suggest: +[out] From bcdbf00446160a6b4fa1ea1ec1eb97815aadb24f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Sep 2019 21:33:24 +0100 Subject: [PATCH 04/10] More tests; some fixes --- mypy/nodes.py | 1 + mypy/semanal_main.py | 9 ++- mypy/test/testfinegrained.py | 8 ++- test-data/unit/fine-grained-suggest.test | 76 +++++++++++++++++++++--- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 98152e52538a..988d55692a99 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -248,6 +248,7 @@ class MypyFile(SymbolNode): # error codes to ignore. ignored_lines = None # type: Dict[int, List[str]] # This map allow find quickly a top level function/method by its line number. + # Decorators are added both under the first decorator line and the function line. line_node_map = None # type: Dict[int, Union[FuncDef, OverloadedFuncDef, Decorator]] # Is this file represented by a stub file (.pyi)? is_stub = False diff --git a/mypy/semanal_main.py b/mypy/semanal_main.py index 577a84eaf4e0..7a3f8a208c76 100644 --- a/mypy/semanal_main.py +++ b/mypy/semanal_main.py @@ -251,7 +251,7 @@ def process_top_level_function(analyzer: 'SemanticAnalyzer', until all names have been resolved (ot iteration limit reached). """ assert state.tree is not None - state.tree.line_node_map[node.line] = node + add_node_to_map(state.tree, node) # We need one more iteration after incomplete is False (e.g. to report errors, if any). final_iteration = False incomplete = True @@ -390,6 +390,13 @@ def calculate_class_properties(graph: 'Graph', scc: List[str], errors: Errors) - add_type_promotion(node.node, tree.names, graph[module].options) +def add_node_to_map(tree: MypyFile, + node: Union[FuncDef, Decorator, OverloadedFuncDef]) -> None: + if isinstance(node, Decorator): + tree.line_node_map[node.func.line] = node + tree.line_node_map[node.line] = node + + def check_blockers(graph: 'Graph', scc: List[str]) -> None: for module in scc: graph[module].check_blockers() diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 1fb6c91ad945..35205144ea54 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -96,7 +96,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if messages: a.extend(normalize_messages(messages)) - a.extend(self.maybe_suggest(step, server, main_src)) + a.extend(self.maybe_suggest(step, server, main_src, testcase.tmpdir.name)) if server.fine_grained_manager: if CHECK_CONSISTENCY: @@ -155,7 +155,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: a.append('==') a.extend(new_messages) - a.extend(self.maybe_suggest(step, server, main_src)) + a.extend(self.maybe_suggest(step, server, main_src, testcase.tmpdir.name)) # Normalize paths in test output (for Windows). a = [line.replace('\\', '/') for line in a] @@ -268,7 +268,7 @@ def parse_sources(self, program_text: str, return [base] + create_source_list([test_temp_dir], options, allow_empty_dir=True) - def maybe_suggest(self, step: int, server: Server, src: str) -> List[str]: + def maybe_suggest(self, step: int, server: Server, src: str, tmp_dir: str) -> List[str]: output = [] # type: List[str] targets = self.get_suggest(src, step) for flags, target in targets: @@ -285,6 +285,8 @@ def maybe_suggest(self, step: int, server: Server, src: str) -> List[str]: try_text=try_text, flex_any=flex_any, callsites=callsites)) val = res['error'] if 'error' in res else res['out'] + res['err'] + if json: + val = val.replace(tmp_dir + '/', '') output.extend(val.strip().split('\n')) return normalize_messages(output) diff --git a/test-data/unit/fine-grained-suggest.test b/test-data/unit/fine-grained-suggest.test index 0c0a82d71a06..e9114938e48d 100644 --- a/test-data/unit/fine-grained-suggest.test +++ b/test-data/unit/fine-grained-suggest.test @@ -628,29 +628,87 @@ class C: == [case testSuggestColonBadLine] -# suggest: +# suggest: tmp/foo.py:bad +[file foo.py] [out] +Line number must be a number. Got bad +== -[case testSuggestColonBadFiles] -# suggest: +[case testSuggestColonBadFile] +# suggest: tmp/foo.txt:1 +[file foo.txt] +def f(): pass [out] +Source file is not a Python file +== [case testSuggestColonUnknownLine] -# suggest: +# suggest: tmp/foo.py:42 +[file foo.py] +def func(x): + return 0 +func('test') [out] +Cannot find a function at line 42 +== [case testSuggestColonClass] -# suggest: +# suggest: tmp/foo.py:1 +[file foo.py] +class C: + pass [out] +Cannot find a function at line 1 +== [case testSuggestColonDecorator] -# suggest: +# suggest: tmp/foo.py:6 +[file foo.py] +from typing import TypeVar, Callable, Any +F = TypeVar('F', bound=Callable[..., Any]) +def deco(f: F) -> F: ... + +@deco +def func(arg): + return 0 +func('test') [out] +(str) -> int +== [case testSuggestColonMethod] -# suggest: -[out] +# suggest: tmp/foo.py:3 +[file foo.py] +class Out: + class In: + def method(self, x): + return Out() +x: Out.In +x.method(x) +[out] +(foo:Out.In) -> foo.Out +== [case testSuggestColonMethodJSON] -# suggest: +# suggest: --json tmp/foo.py:3 +[file foo.py] +class Out: + class In: + def method(self, x): + return Out() +x: Out.In +x.method(x) +[out] +[[{"func_name": "Out.In.method", "line": 3, "path": "tmp/foo.py", "samples": 0, "signature": {"arg_types": ["foo:Out.In"], "return_type": "foo.Out"}}] +== + +[case testSuggestColonNonPackageDir] +# cmd: mypy foo/bar/baz.py +# suggest: tmp/foo/bar/baz.py:1 +[file foo/bar/baz.py] +def func(arg): + return 0 +func('test') [out] +(str) -> int +== From c96836e1a08aad4fdfc738f32d766706125ddc52 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Sep 2019 22:03:22 +0100 Subject: [PATCH 05/10] Some tweaks --- mypy/suggestions.py | 7 ++++--- mypy/test/testfinegrained.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 029a809ed741..5f515428343e 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -466,7 +466,8 @@ def find_node_by_file_and_line(self, file: str, line: int) -> Tuple[str, SymbolN raise SuggestionFailure('Invalid source file name: ' + file) if modname not in self.graph: raise SuggestionFailure('Unknown module: ' + modname) - tree = self.ensure_loaded(self.fgmanager.graph[modname]) + # We must be sure about any edits in this file as this might affect the line numbers. + tree = self.ensure_loaded(self.fgmanager.graph[modname], force=True) if line not in tree.line_node_map: raise SuggestionFailure('Cannot find a function at line {}'.format(line)) node = tree.line_node_map[line] @@ -526,9 +527,9 @@ def reload(self, state: State, check_errors: bool = False) -> List[str]: raise SuggestionFailure("Error while trying to load %s" % state.id) return res - def ensure_loaded(self, state: State) -> MypyFile: + def ensure_loaded(self, state: State, force: bool = False) -> MypyFile: """Make sure that the module represented by state is fully loaded.""" - if not state.tree or state.tree.is_cache_skeleton: + if not state.tree or state.tree.is_cache_skeleton or force: self.reload(state, check_errors=True) assert state.tree is not None return state.tree diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 35205144ea54..10576936f1da 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -96,6 +96,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: if messages: a.extend(normalize_messages(messages)) + assert testcase.tmpdir a.extend(self.maybe_suggest(step, server, main_src, testcase.tmpdir.name)) if server.fine_grained_manager: @@ -155,6 +156,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: a.append('==') a.extend(new_messages) + assert testcase.tmpdir a.extend(self.maybe_suggest(step, server, main_src, testcase.tmpdir.name)) # Normalize paths in test output (for Windows). From 9cc74343ba4598ea204b6661ad93beb07abaf771 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 7 Sep 2019 23:34:59 +0100 Subject: [PATCH 06/10] Try fixing Windows build --- mypy/test/testfinegrained.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 10576936f1da..08776b99f3e0 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -288,7 +288,7 @@ def maybe_suggest(self, step: int, server: Server, src: str, tmp_dir: str) -> Li callsites=callsites)) val = res['error'] if 'error' in res else res['out'] + res['err'] if json: - val = val.replace(tmp_dir + '/', '') + val = val.replace(tmp_dir + os.path.sep, '') output.extend(val.strip().split('\n')) return normalize_messages(output) From 46cadb1c9bd592ca6cd2aa9d326d790b417962ec Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 8 Sep 2019 02:36:12 +0100 Subject: [PATCH 07/10] Slower but more robust against astmerge way to find node --- mypy/nodes.py | 4 ---- mypy/semanal_main.py | 10 +--------- mypy/suggestions.py | 12 ++++++++++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 988d55692a99..79343f4ad212 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -247,9 +247,6 @@ class MypyFile(SymbolNode): # If the value is empty, ignore all errors; otherwise, the list contains all # error codes to ignore. ignored_lines = None # type: Dict[int, List[str]] - # This map allow find quickly a top level function/method by its line number. - # Decorators are added both under the first decorator line and the function line. - line_node_map = None # type: Dict[int, Union[FuncDef, OverloadedFuncDef, Decorator]] # Is this file represented by a stub file (.pyi)? is_stub = False # Is this loaded from the cache and thus missing the actual body of the file? @@ -277,7 +274,6 @@ def __init__(self, self.ignored_lines = ignored_lines else: self.ignored_lines = {} - self.line_node_map = {} def local_definitions(self) -> Iterator[Definition]: """Return all definitions within the module (including nested). diff --git a/mypy/semanal_main.py b/mypy/semanal_main.py index 7a3f8a208c76..cc28d00f250e 100644 --- a/mypy/semanal_main.py +++ b/mypy/semanal_main.py @@ -250,8 +250,6 @@ def process_top_level_function(analyzer: 'SemanticAnalyzer', Process the body of the function (including nested functions) again and again, until all names have been resolved (ot iteration limit reached). """ - assert state.tree is not None - add_node_to_map(state.tree, node) # We need one more iteration after incomplete is False (e.g. to report errors, if any). final_iteration = False incomplete = True @@ -265,6 +263,7 @@ def process_top_level_function(analyzer: 'SemanticAnalyzer', iteration += 1 if iteration == MAX_ITERATIONS: # Just pick some module inside the current SCC for error context. + assert state.tree is not None with analyzer.file_context(state.tree, state.options): analyzer.report_hang() break @@ -390,13 +389,6 @@ def calculate_class_properties(graph: 'Graph', scc: List[str], errors: Errors) - add_type_promotion(node.node, tree.names, graph[module].options) -def add_node_to_map(tree: MypyFile, - node: Union[FuncDef, Decorator, OverloadedFuncDef]) -> None: - if isinstance(node, Decorator): - tree.line_node_map[node.func.line] = node - tree.line_node_map[node.line] = node - - def check_blockers(graph: 'Graph', scc: List[str]) -> None: for module in scc: graph[module].check_blockers() diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 5f515428343e..ca3ca207c1b3 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -468,9 +468,17 @@ def find_node_by_file_and_line(self, file: str, line: int) -> Tuple[str, SymbolN raise SuggestionFailure('Unknown module: ' + modname) # We must be sure about any edits in this file as this might affect the line numbers. tree = self.ensure_loaded(self.fgmanager.graph[modname], force=True) - if line not in tree.line_node_map: + node = None # type: Optional[SymbolNode] + for _, sym, _ in tree.local_definitions(): + if isinstance(sym.node, FuncDef) and sym.node.line == line: + node = sym.node + break + elif isinstance(sym.node, Decorator) and sym.node.func.line == line: + node = sym.node + break + # TODO: add support for OverloadedFuncDef. + if not node: raise SuggestionFailure('Cannot find a function at line {}'.format(line)) - node = tree.line_node_map[line] return modname, node def extract_from_decorator(self, node: Decorator) -> Optional[FuncDef]: From 4886d2e4e26e2c74ebd13b275d8a39a40800d0a4 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 8 Sep 2019 09:05:44 +0100 Subject: [PATCH 08/10] Windows diagnostics --- mypy/test/testfinegrained.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 08776b99f3e0..26e93c04ae2e 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -288,6 +288,7 @@ def maybe_suggest(self, step: int, server: Server, src: str, tmp_dir: str) -> Li callsites=callsites)) val = res['error'] if 'error' in res else res['out'] + res['err'] if json: + print(val, tmp_dir, os.path.sep, os.path.altsep) val = val.replace(tmp_dir + os.path.sep, '') output.extend(val.strip().split('\n')) return normalize_messages(output) From 076f1592542507c8415ca819d033ff6cefce5f17 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 8 Sep 2019 09:49:47 +0100 Subject: [PATCH 09/10] A better Windows fix --- mypy/test/testfinegrained.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 26e93c04ae2e..868dcfa39871 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -288,7 +288,8 @@ def maybe_suggest(self, step: int, server: Server, src: str, tmp_dir: str) -> Li callsites=callsites)) val = res['error'] if 'error' in res else res['out'] + res['err'] if json: - print(val, tmp_dir, os.path.sep, os.path.altsep) + # JSON contains already escaped \ on Windows, so requires a bit of care. + val = val.replace('\\\\', '\\') val = val.replace(tmp_dir + os.path.sep, '') output.extend(val.strip().split('\n')) return normalize_messages(output) From efab9d8b5d79a6a1752eebc46e1c67b21fd43820 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 10 Sep 2019 17:24:47 +0100 Subject: [PATCH 10/10] Avoid crashing on bad input --- mypy/suggestions.py | 4 ++++ test-data/unit/fine-grained-suggest.test | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/mypy/suggestions.py b/mypy/suggestions.py index ca3ca207c1b3..19879dee0cda 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -398,6 +398,10 @@ def find_node(self, key: str) -> Tuple[str, str, FuncDef]: # TODO: Also return OverloadedFuncDef -- currently these are ignored. node = None # type: Optional[SymbolNode] if ':' in key: + if key.count(':') > 1: + raise SuggestionFailure( + 'Malformed location for function: {}. Must be either' + ' package.module.Class.method or path/to/file.py:line'.format(key)) file, line = key.split(':') if not line.isdigit(): raise SuggestionFailure('Line number must be a number. Got {}'.format(line)) diff --git a/test-data/unit/fine-grained-suggest.test b/test-data/unit/fine-grained-suggest.test index e9114938e48d..09ee7e1a215c 100644 --- a/test-data/unit/fine-grained-suggest.test +++ b/test-data/unit/fine-grained-suggest.test @@ -627,6 +627,13 @@ class C: (str) -> int == +[case testSuggestColonBadLocation] +# suggest: tmp/foo.py:7:8:9 +[file foo.py] +[out] +Malformed location for function: tmp/foo.py:7:8:9. Must be either package.module.Class.method or path/to/file.py:line +== + [case testSuggestColonBadLine] # suggest: tmp/foo.py:bad [file foo.py] pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy