diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index b543fee8..bb9be477 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -142,7 +142,11 @@ class Script(object): @cache.memoize_method def _get_module(self): - module = er.ModuleContext(self._evaluator, self._get_module_node()) + module = er.ModuleContext( + self._evaluator, + self._get_module_node(), + self.path + ) imports.add_module(self._evaluator, module.name.string_name, module) return module @@ -398,7 +402,8 @@ class Interpreter(Script): return interpreter.MixedModuleContext( self._evaluator, parser_module, - self.namespaces + self.namespaces, + path=self.path ) diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index b5b25d8d..dbfbdbfa 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -300,7 +300,7 @@ def cache_call_signatures(evaluator, context, bracket_leaf, code_lines, user_pos whole = '\n'.join(other_lines + [before_cursor]) before_bracket = re.match(r'.*\(', whole, re.DOTALL) - module_path = bracket_leaf.get_root_node().path + module_path = context.get_root_context().py__file__() if module_path is None: yield None # Don't cache! else: diff --git a/jedi/api/interpreter.py b/jedi/api/interpreter.py index c9b5a38b..9f696eb4 100644 --- a/jedi/api/interpreter.py +++ b/jedi/api/interpreter.py @@ -12,12 +12,12 @@ class MixedModuleContext(Context): resets_positions = True type = 'mixed_module' - def __init__(self, evaluator, tree_module, namespaces): + def __init__(self, evaluator, tree_module, namespaces, path): self.evaluator = evaluator self._namespaces = namespaces self._namespace_objects = [type('jedi_namespace', (), n) for n in namespaces] - self._module_context = ModuleContext(evaluator, tree_module) + self._module_context = ModuleContext(evaluator, tree_module, path=path) self.tree_node = tree_module def get_node(self): @@ -33,7 +33,7 @@ class MixedModuleContext(Context): self.evaluator, parent_context=self, compiled_object=compiled_object, - tree_name=self.tree_node.name + tree_context=self._module_context ) for filter in mixed_object.get_filters(*args, **kwargs): yield filter diff --git a/jedi/evaluate/analysis.py b/jedi/evaluate/analysis.py index 2beb3f02..29cf614b 100644 --- a/jedi/evaluate/analysis.py +++ b/jedi/evaluate/analysis.py @@ -5,8 +5,6 @@ from jedi import debug from jedi.parser.python import tree from jedi.evaluate.compiled import CompiledObject -from jedi.common import unite - CODES = { 'attribute-error': (1, AttributeError, 'Potential AttributeError.'), @@ -82,7 +80,9 @@ def add(node_context, error_name, node, message=None, typ=Error, payload=None): if _check_for_exception_catch(node_context, node, exception, payload): return - module_path = node.get_root_node().path + # TODO this path is probably not right + module_context = node_context.get_root_context() + module_path = module_context.py__file__() instance = typ(error_name, module_path, node.start_pos, message) debug.warning(str(instance), format=False) node_context.evaluator.analysis.append(instance) diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index a08fb26a..2d2b08d4 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -29,25 +29,12 @@ class MixedObject(object): fewer special cases, because we in Python you don't have the same freedoms to modify the runtime. """ - def __init__(self, evaluator, parent_context, compiled_object, tree_name): + def __init__(self, evaluator, parent_context, compiled_object, tree_context): self.evaluator = evaluator + self.parent_context = parent_context self.compiled_object = compiled_object + self._context = tree_context self.obj = compiled_object.obj - self._tree_name = tree_name - name_module = tree_name.get_root_node() - if parent_context.tree_node.get_root_node() != name_module: - from jedi.evaluate.representation import ModuleContext - module_context = ModuleContext(evaluator, name_module) - name = compiled_object.get_root_context().py__name__() - imports.add_module(evaluator, name, module_context) - else: - module_context = parent_context.get_root_context() - - self._context = module_context.create_context( - tree_name.parent, - node_is_context=True, - node_is_object=True - ) # We have to overwrite everything that has to do with trailers, name # lookups and filters to make it possible to route name lookups towards @@ -132,10 +119,10 @@ def find_syntax_node_name(evaluator, python_object): path = inspect.getsourcefile(python_object) except TypeError: # The type might not be known (e.g. class_with_dict.__weakref__) - return None + return None, None if path is None or not os.path.exists(path): # The path might not exist or be e.g. . - return None + return None, None module = _load_module(evaluator, path, python_object) @@ -143,17 +130,17 @@ def find_syntax_node_name(evaluator, python_object): # 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.name + return module, path name_str = python_object.__name__ if name_str == '': - return None # It's too hard to find lambdas. + return None, None # It's too hard to find lambdas. # Doesn't always work (e.g. os.stat_result) try: names = module.used_names[name_str] except KeyError: - return None + return None, None names = [n for n in names if n.is_definition()] try: @@ -170,22 +157,44 @@ def find_syntax_node_name(evaluator, python_object): # There's a chance that the object is not available anymore, because # the code has changed in the background. if line_names: - return line_names[-1] + return line_names[-1].parent, 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 names[-1] + return names[-1].parent, path @compiled.compiled_objects_cache('mixed_cache') def create(evaluator, obj, parent_context=None, *args): - tree_name = find_syntax_node_name(evaluator, obj) + tree_node, path = find_syntax_node_name(evaluator, obj) compiled_object = compiled.create( evaluator, obj, parent_context=parent_context.compiled_object) - if tree_name is None: + if tree_node is None: return compiled_object - return MixedObject(evaluator, parent_context, compiled_object, tree_name) + + module_node = tree_node.get_root_node() + if parent_context.tree_node.get_root_node() == module_node: + module_context = parent_context.get_root_context() + else: + from jedi.evaluate.representation import ModuleContext + module_context = ModuleContext(evaluator, module_node, path=path) + name = compiled_object.get_root_context().py__name__() + imports.add_module(evaluator, name, module_context) + + tree_context = module_context.create_context( + tree_node, + node_is_context=True, + node_is_object=True + ) + + return MixedObject( + evaluator, + parent_context, + compiled_object, + tree_context=tree_context + ) + diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index f6b533cf..566cd979 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -459,7 +459,7 @@ def _load_module(evaluator, path=None, code=None, sys_path=None, parent_module=N module_node = parse(code=code, path=path, cache=True, diff_cache=True) from jedi.evaluate.representation import ModuleContext - return ModuleContext(evaluator, module_node) + return ModuleContext(evaluator, module_node, path=path) else: return compiled.load_module(evaluator, path) @@ -489,7 +489,7 @@ def get_modules_containing_name(evaluator, modules, name): return None else: module_node = parser_cache_item.parser.get_root_node() - return er.ModuleContext(evaluator, module_node) + return er.ModuleContext(evaluator, module_node, path=path) def check_fs(path): with open(path, 'rb') as f: diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py index 098af808..c478fd46 100644 --- a/jedi/evaluate/pep0484.py +++ b/jedi/evaluate/pep0484.py @@ -150,7 +150,11 @@ def py__getitem__(context, typ, node): return context.eval_node(nodes[0]) from jedi.evaluate.representation import ModuleContext - typing = ModuleContext(context.evaluator, _get_typing_replacement_module()) + typing = ModuleContext( + context.evaluator, + module_node=_get_typing_replacement_module(), + path=None + ) factories = typing.py__getattribute__("factory") assert len(factories) == 1 factory = list(factories)[0] diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index fdb8ef0f..9d4a728b 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -57,7 +57,8 @@ from jedi.evaluate import imports from jedi.evaluate import helpers from jedi.evaluate.filters import ParserTreeFilter, FunctionExecutionFilter, \ GlobalNameFilter, DictFilter, ContextName, AbstractNameDefinition, \ - ParamName, AnonymousInstanceParamName, TreeNameDefinition + ParamName, AnonymousInstanceParamName, TreeNameDefinition, \ + ContextNameMixin from jedi.evaluate.dynamic import search_params from jedi.evaluate import context @@ -406,13 +407,26 @@ class ModuleAttributeName(AbstractNameDefinition): ) +class ModuleName(ContextNameMixin, AbstractNameDefinition): + start_pos = 1, 0 + + def __init__(self, context, name): + self._context = context + self._name = name + + @property + def string_name(self): + return self._name + + class ModuleContext(use_metaclass(CachedMetaClass, context.TreeContext)): api_type = 'module' parent_context = None - def __init__(self, evaluator, module_node): + def __init__(self, evaluator, module_node, path): super(ModuleContext, self).__init__(evaluator, parent_context=None) self.tree_node = module_node + self._path = path def get_filters(self, search_global, until_position=None, origin_scope=None): yield ParserTreeFilter( @@ -449,10 +463,21 @@ class ModuleContext(use_metaclass(CachedMetaClass, context.TreeContext)): # All the additional module attributes are strings. return dict((n, ModuleAttributeName(self, n)) for n in names) + @property + def _string_name(self): + """ This is used for the goto functions. """ + if self._path is None: + return '' # no path -> empty name + else: + sep = (re.escape(os.path.sep),) * 2 + r = re.search(r'([^%s]*?)(%s__init__)?(\.py|\.so)?$' % sep, self._path) + # Remove PEP 3149 names + return re.sub('\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) + @property @memoize_default() def name(self): - return ContextName(self, self.tree_node.name) + return ModuleName(self, self._string_name) def _get_init_directory(self): """ @@ -478,10 +503,10 @@ class ModuleContext(use_metaclass(CachedMetaClass, context.TreeContext)): """ In contrast to Python's __file__ can be None. """ - if self.tree_node.path is None: + if self._path is None: return None - return os.path.abspath(self.tree_node.path) + return os.path.abspath(self._path) def py__package__(self): if self._get_init_directory() is None: @@ -539,7 +564,7 @@ class ModuleContext(use_metaclass(CachedMetaClass, context.TreeContext)): Lists modules in the directory of this module (if this module is a package). """ - path = self.tree_node.path + path = self._path names = {} if path is not None and path.endswith(os.path.sep + '__init__.py'): mods = pkgutil.iter_modules([os.path.dirname(path)]) @@ -559,6 +584,11 @@ class ModuleContext(use_metaclass(CachedMetaClass, context.TreeContext)): def py__class__(self): return compiled.get_special_object(self.evaluator, 'MODULE_CLASS') + def __repr__(self): + return "<%s: %s@%s-%s>" % ( + self.__class__.__name__, self._string_name, + self.tree_node.start_pos[0], self.tree_node.end_pos[0]) + class ImplicitNSName(AbstractNameDefinition): """ diff --git a/jedi/evaluate/sys_path.py b/jedi/evaluate/sys_path.py index c3ff7aab..513c32be 100644 --- a/jedi/evaluate/sys_path.py +++ b/jedi/evaluate/sys_path.py @@ -222,7 +222,7 @@ def _get_paths_from_buildout_script(evaluator, buildout_script_path): return from jedi.evaluate.representation import ModuleContext - for path in _check_module(ModuleContext(evaluator, module_node)): + for path in _check_module(ModuleContext(evaluator, module_node, buildout_script_path)): yield path diff --git a/jedi/parser/python/parser.py b/jedi/parser/python/parser.py index f5aac412..077058de 100644 --- a/jedi/parser/python/parser.py +++ b/jedi/parser/python/parser.py @@ -144,8 +144,6 @@ class ParserWithRecovery(Parser): :param grammar: The grammar object of pgen2. Loaded by load_grammar. :param source: The codebase for the parser. Must be unicode. - :param module_path: The path of the module in the file system, may be None. - :type module_path: str """ def __init__(self, grammar, source, module_path=None): super(ParserWithRecovery, self).__init__( @@ -155,7 +153,6 @@ class ParserWithRecovery(Parser): self.syntax_errors = [] self._omit_dedent_list = [] self._indent_counter = 0 - self._module_path = module_path # TODO do print absolute import detection here. # try: @@ -169,7 +166,6 @@ class ParserWithRecovery(Parser): def parse(self, tokens): root_node = super(ParserWithRecovery, self).parse(self._tokenize(tokens)) - root_node.path = self._module_path return root_node def error_recovery(self, grammar, stack, arcs, typ, value, start_pos, prefix, diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 3dd2283a..c6d8dd76 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -32,8 +32,7 @@ For static analysis purposes there exists a method called ``nodes_to_execute`` on all nodes and leaves. It's documented in the static anaylsis documentation. """ -import os -import re + from inspect import cleandoc from itertools import chain import textwrap @@ -277,7 +276,7 @@ class Name(_LeafWithoutNewlines): return False stmt = self.get_definition() - if stmt.type in ('funcdef', 'classdef', 'file_input', 'param'): + if stmt.type in ('funcdef', 'classdef', 'param'): return self == stmt.name elif stmt.type == 'for_stmt': return self.start_pos < stmt.children[2].start_pos @@ -413,12 +412,9 @@ class Scope(PythonBaseNode, DocstringMixin): def __repr__(self): try: - name = self.path + name = self.name except AttributeError: - try: - name = self.name - except AttributeError: - name = self.command + name = '' return "<%s: %s@%s-%s>" % (type(self).__name__, name, self.start_pos[0], self.end_pos[0]) @@ -442,38 +438,13 @@ class Module(Scope): Depending on the underlying parser this may be a full module or just a part of a module. """ - __slots__ = ('path', '_used_names', '_name') + __slots__ = ('_used_names', '_name') type = 'file_input' def __init__(self, children): - """ - Initialize :class:`Module`. - - :type path: str - :arg path: File path to this module. - - .. todo:: Document `top_module`. - """ super(Module, self).__init__(children) - self.path = None # Set later. self._used_names = None - @property - def name(self): - """ This is used for the goto functions. """ - if self.path is None: - string = '' # no path -> empty name - else: - sep = (re.escape(os.path.sep),) * 2 - r = re.search(r'([^%s]*?)(%s__init__)?(\.py|\.so)?$' % sep, self.path) - # Remove PEP 3149 names - string = re.sub('\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) - # Positions are not real, but a module starts at (1, 0) - p = (1, 0) - name = Name(string, p) - name.parent = self - return name - @property def has_explicit_absolute_import(self): """ diff --git a/test/test_api/test_classes.py b/test/test_api/test_classes.py index bd2526a9..c1a76219 100644 --- a/test/test_api/test_classes.py +++ b/test/test_api/test_classes.py @@ -338,7 +338,7 @@ class TestGotoAssignments(TestCase): n = nms[0].goto_assignments()[0] assert n.name == 'json' assert n.type == 'module' - assert n._name.tree_name.get_definition().type == 'file_input' + assert n._name._context.tree_node.type == 'file_input' assert nms[1].name == 'foo' assert nms[1].type == 'module' @@ -347,7 +347,7 @@ class TestGotoAssignments(TestCase): assert len(ass) == 1 assert ass[0].name == 'json' assert ass[0].type == 'module' - assert ass[0]._name.tree_name.get_definition().type == 'file_input' + assert ass[0]._name._context.tree_node.type == 'file_input' def test_added_equals_to_params(): diff --git a/test/test_evaluate/test_buildout_detection.py b/test/test_evaluate/test_buildout_detection.py index 64e5ec76..7c187d81 100644 --- a/test/test_evaluate/test_buildout_detection.py +++ b/test/test_evaluate/test_buildout_detection.py @@ -15,7 +15,7 @@ from ..helpers import cwd_at def check_module_test(code): grammar = load_grammar() - module_context = ModuleContext(Evaluator(grammar), parse(code)) + module_context = ModuleContext(Evaluator(grammar), parse(code), path=None) return _check_module(module_context) @@ -68,7 +68,7 @@ def test_sys_path_with_modifications(): path = os.path.abspath(os.path.join(os.curdir, 'module_name.py')) grammar = load_grammar() module_node = parse(code, path=path) - module_context = ModuleContext(Evaluator(grammar), module_node) + module_context = ModuleContext(Evaluator(grammar), module_node, path=path) paths = sys_path_with_modifications(module_context.evaluator, module_context) assert '/tmp/.buildout/eggs/important_package.egg' in paths diff --git a/test/test_parser/test_parser.py b/test/test_parser/test_parser.py index 505d8daa..71d78699 100644 --- a/test/test_parser/test_parser.py +++ b/test/test_parser/test_parser.py @@ -88,20 +88,6 @@ class TestImports(): assert imp.end_pos == (1, len('import math')) -def test_module(): - module = parse('asdf', path='example.py') - name = module.name - assert str(name) == 'example' - assert name.start_pos == (1, 0) - assert name.end_pos == (1, 7) - - module = parse('asdf') - name = module.name - assert str(name) == '' - assert name.start_pos == (1, 0) - assert name.end_pos == (1, 0) - - def test_end_pos(): s = dedent(''' x = ['a', 'b', 'c']