diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 5edcc3d0..c69a0dec 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -120,6 +120,7 @@ class Script(object): ) debug.speed('parsed') self._code_lines = parso.split_lines(source) + self._code = source line = max(len(self._code_lines), 1) if line is None else line if not (0 < line <= len(self._code_lines)): raise ValueError('`line` parameter is not in a valid range.') @@ -141,7 +142,10 @@ class Script(object): if n is not None: name = n - module = ModuleContext(self._evaluator, self._module_node, self.path) + module = ModuleContext( + self._evaluator, self._module_node, self.path, + code_lines=self._code_lines + ) imports.add_module(self._evaluator, name, module) return module @@ -374,7 +378,8 @@ class Interpreter(Script): self._evaluator, self._module_node, self.namespaces, - path=self.path + path=self.path, + code_lines=self._code_lines, ) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 995ccf25..321b0e18 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -377,8 +377,7 @@ class BaseDefinition(object): if self.in_builtin_module(): return '' - path = self._name.get_root_context().py__file__() - lines = parser_cache[self._evaluator.grammar._hashed][path].lines + lines = self._name.get_root_context().code_lines index = self._name.start_pos[0] - 1 start_index = max(index - before, 0) diff --git a/jedi/api/interpreter.py b/jedi/api/interpreter.py index 365f88e9..c9b7bd69 100644 --- a/jedi/api/interpreter.py +++ b/jedi/api/interpreter.py @@ -21,15 +21,18 @@ class NamespaceObject(object): class MixedModuleContext(Context): - resets_positions = True type = 'mixed_module' - def __init__(self, evaluator, tree_module, namespaces, path): + def __init__(self, evaluator, tree_module, namespaces, path, code_lines): self.evaluator = evaluator self._namespaces = namespaces self._namespace_objects = [NamespaceObject(n) for n in namespaces] - self._module_context = ModuleContext(evaluator, tree_module, path=path) + self._module_context = ModuleContext( + evaluator, tree_module, + path=path, + code_lines=code_lines + ) self.tree_node = tree_module def get_node(self): @@ -50,5 +53,9 @@ class MixedModuleContext(Context): for filter in mixed_object.get_filters(*args, **kwargs): yield filter + @property + def code_lines(self): + return self._module_context.code_lines + def __getattr__(self, name): return getattr(self._module_context, name) diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index 6278dd8a..76366d55 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -5,6 +5,8 @@ Used only for REPL Completion. import inspect import os +from jedi.parser_utils import get_cached_code_lines + from jedi import settings from jedi.evaluate import compiled from jedi.cache import underscore_memoization @@ -140,10 +142,10 @@ def _find_syntax_node_name(evaluator, access_handle): path = inspect.getsourcefile(python_object) except TypeError: # The type might not be known (e.g. class_with_dict.__weakref__) - return None, None, None + return None if path is None or not os.path.exists(path): # The path might not exist or be e.g. . - return None, None, None + return None module_node = _load_module(evaluator, path) @@ -151,22 +153,23 @@ def _find_syntax_node_name(evaluator, access_handle): # We don't need to check names for modules, because there's not really # a way to write a module in a module in Python (and also __name__ can # be something like ``email.utils``). - return module_node, module_node, path + code_lines = get_cached_code_lines(evaluator.grammar, path) + return module_node, module_node, path, code_lines try: name_str = python_object.__name__ except AttributeError: # Stuff like python_function.__code__. - return None, None, None + return None if name_str == '': - return None, None, None # It's too hard to find lambdas. + return None # It's too hard to find lambdas. # Doesn't always work (e.g. os.stat_result) try: names = module_node.get_used_names()[name_str] except KeyError: - return None, None, None + return None names = [n for n in names if n.is_definition()] try: @@ -183,29 +186,36 @@ def _find_syntax_node_name(evaluator, access_handle): # There's a chance that the object is not available anymore, because # the code has changed in the background. if line_names: - return module_node, line_names[-1].parent, path + names = line_names + code_lines = get_cached_code_lines(evaluator.grammar, path) # It's really hard to actually get the right definition, here as a last # resort we just return the last one. This chance might lead to odd # completions at some points but will lead to mostly correct type # inference, because people tend to define a public name in a module only # once. - return module_node, names[-1].parent, path + return module_node, names[-1].parent, path, code_lines @compiled_objects_cache('mixed_cache') def _create(evaluator, access_handle, parent_context, *args): - module_node, tree_node, path = _find_syntax_node_name(evaluator, access_handle) - compiled_object = create_cached_compiled_object( evaluator, access_handle, parent_context=parent_context.compiled_object) - if tree_node is None: + + result = _find_syntax_node_name(evaluator, access_handle) + if result is None: return compiled_object + module_node, tree_node, path, code_lines = result + if parent_context.tree_node.get_root_node() == module_node: module_context = parent_context.get_root_context() else: - module_context = ModuleContext(evaluator, module_node, path=path) + module_context = ModuleContext( + evaluator, module_node, + path=path, + code_lines=code_lines, + ) # TODO this __name__ is probably wrong. name = compiled_object.get_root_context().py__name__() imports.add_module(evaluator, name, module_context) diff --git a/jedi/evaluate/context/module.py b/jedi/evaluate/context/module.py index cde8cac3..42dcbd92 100644 --- a/jedi/evaluate/context/module.py +++ b/jedi/evaluate/context/module.py @@ -43,10 +43,11 @@ class ModuleContext(TreeContext): api_type = u'module' parent_context = None - def __init__(self, evaluator, module_node, path): + def __init__(self, evaluator, module_node, path, code_lines): super(ModuleContext, self).__init__(evaluator, parent_context=None) self.tree_node = module_node self._path = path + self.code_lines = code_lines def get_filters(self, search_global, until_position=None, origin_scope=None): yield MergedFilter( diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 5178b29d..8c9a726a 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -20,6 +20,7 @@ from parso import python_bytes_to_unicode from jedi._compatibility import unicode, ImplicitNSInfo, force_unicode from jedi import debug from jedi import settings +from jedi.parser_utils import get_cached_code_lines from jedi.evaluate import sys_path from jedi.evaluate import helpers from jedi.evaluate import compiled @@ -492,7 +493,11 @@ def _load_module(evaluator, path=None, code=None, sys_path=None, cache_path=settings.cache_directory) from jedi.evaluate.context import ModuleContext - module = ModuleContext(evaluator, module_node, path=path) + module = ModuleContext( + evaluator, module_node, + path=path, + code_lines=get_cached_code_lines(evaluator.grammar, path), + ) else: module = compiled.load_module(evaluator, path=path, sys_path=sys_path) add_module(evaluator, module_name, module, safe=safe_module_name) diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py index e4b80e61..da0fa7e3 100644 --- a/jedi/evaluate/pep0484.py +++ b/jedi/evaluate/pep0484.py @@ -22,7 +22,7 @@ x support for type hint comments for functions, `# type: (int, str) -> int`. import os import re -from parso import ParserSyntaxError, parse +from parso import ParserSyntaxError, parse, split_lines from parso.python import tree from jedi._compatibility import unicode, force_unicode @@ -216,6 +216,7 @@ def infer_return_types(function_context): _typing_module = None +_typing_module_code_lines = None def _get_typing_replacement_module(grammar): @@ -223,14 +224,15 @@ def _get_typing_replacement_module(grammar): The idea is to return our jedi replacement for the PEP-0484 typing module as discussed at https://github.com/davidhalter/jedi/issues/663 """ - global _typing_module + global _typing_module, _typing_module_code_lines if _typing_module is None: typing_path = \ os.path.abspath(os.path.join(__file__, "../jedi_typing.py")) with open(typing_path) as f: code = unicode(f.read()) _typing_module = grammar.parse(code) - return _typing_module + _typing_module_code_lines = split_lines(code) + return _typing_module, _typing_module_code_lines def py__getitem__(context, typ, node): @@ -260,10 +262,12 @@ def py__getitem__(context, typ, node): # check for the instance typing._Optional (Python 3.6). return context.eval_node(nodes[0]) + module_node, code_lines = _get_typing_replacement_module(context.evaluator.latest_grammar) typing = ModuleContext( context.evaluator, - module_node=_get_typing_replacement_module(context.evaluator.latest_grammar), - path=None + module_node=module_node, + path=None, + code_lines=code_lines, ) factories = typing.py__getattribute__("factory") assert len(factories) == 1 diff --git a/jedi/evaluate/stdlib.py b/jedi/evaluate/stdlib.py index 5f21a283..c702ed5b 100644 --- a/jedi/evaluate/stdlib.py +++ b/jedi/evaluate/stdlib.py @@ -11,6 +11,8 @@ compiled module that returns the types for C-builtins. """ import re +import parso + from jedi._compatibility import force_unicode from jedi import debug from jedi.evaluate.arguments import ValuesArguments @@ -293,8 +295,8 @@ def collections_namedtuple(evaluator, obj, arguments): base = next(iter(_class_template_set)).get_safe_value() base += _NAMEDTUPLE_INIT - # Build source - source = base.format( + # Build source code + code = base.format( typename=name, field_names=tuple(fields), num_fields=len(fields), @@ -304,10 +306,13 @@ def collections_namedtuple(evaluator, obj, arguments): for index, name in enumerate(fields)) ) - # Parse source - module = evaluator.grammar.parse(source) + # Parse source code + module = evaluator.grammar.parse(code) generated_class = next(module.iter_classdefs()) - parent_context = ModuleContext(evaluator, module, '') + parent_context = ModuleContext( + evaluator, module, None, + code_lines=parso.split_lines(code), + ) return ContextSet(ClassContext(evaluator, parent_context, generated_class)) diff --git a/jedi/evaluate/sys_path.py b/jedi/evaluate/sys_path.py index 7fa9beef..d765a665 100644 --- a/jedi/evaluate/sys_path.py +++ b/jedi/evaluate/sys_path.py @@ -5,6 +5,7 @@ from jedi.evaluate.cache import evaluator_method_cache from jedi.evaluate.base_context import ContextualizedNode from jedi.evaluate.helpers import is_string from jedi.common.utils import traverse_parents +from jedi.parser_utils import get_cached_code_lines from jedi import settings from jedi import debug @@ -150,7 +151,10 @@ def _get_paths_from_buildout_script(evaluator, buildout_script_path): return from jedi.evaluate.context import ModuleContext - module = ModuleContext(evaluator, module_node, buildout_script_path) + module = ModuleContext( + evaluator, module_node, buildout_script_path, + code_lines=get_cached_code_lines(evaluator.grammar, buildout_script_path), + ) for path in check_sys_path_modifications(module): yield path diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 990e6cca..e6302653 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -1,8 +1,10 @@ import textwrap from inspect import cleandoc -from jedi._compatibility import literal_eval, force_unicode from parso.python import tree +from parso.cache import parser_cache + +from jedi._compatibility import literal_eval, force_unicode _EXECUTE_NODES = {'funcdef', 'classdef', 'import_from', 'import_name', 'test', 'or_test', 'and_test', 'not_test', 'comparison', 'expr', @@ -238,3 +240,11 @@ def get_parent_scope(node, include_flows=False): break scope = scope.parent return scope + + +def get_cached_code_lines(grammar, path): + """ + Basically access the cached code lines in parso. This is not the nicest way + to do this, but we avoid splitting all the lines again. + """ + return parser_cache[grammar._hashed][path].lines diff --git a/test/test_evaluate/test_stdlib.py b/test/test_evaluate/test_stdlib.py index d2252fb0..ff49f89a 100644 --- a/test/test_evaluate/test_stdlib.py +++ b/test/test_evaluate/test_stdlib.py @@ -71,3 +71,22 @@ def test_nested_namedtuples(Script): train_x.train.''' )) assert 'data' in [c.name for c in s.completions()] + + +def test_namedtuple_goto_definitions(Script): + source = dedent(""" + from collections import namedtuple + + Foo = namedtuple('Foo', 'id timestamp gps_timestamp attributes') + Foo""") + + from jedi.api import Script + + lines = source.split("\n") + d1, = Script(source).goto_definitions() + + print(d1) + print(d1.line) + print(d1.module_path) + print(d1.get_line_code()) + assert d1.get_line_code() == lines[-1]