diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 4f10b0f2..da2b7ed2 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -107,10 +107,15 @@ class Script(object): self._grammar = load_grammar('grammar%s.%s' % sys.version_info[:2]) self._user_context = UserContext(self.source, self._pos) self._parser = UserContextParser(self._grammar, self.source, path, - self._pos, self._user_context) + self._pos, self._user_context, + self._parsed_callback) self._evaluator = Evaluator(self._grammar) debug.speed('init') + def _parsed_callback(self, parser): + module = er.wrap(self._evaluator, parser.module) + self._evaluator.module_name_cache[module] = unicode(module.name) + @property def source_path(self): """ @@ -135,12 +140,12 @@ class Script(object): def get_completions(user_stmt, bs): # TODO this closure is ugly. it also doesn't work with # simple_complete (used for Interpreter), somehow redo. - module = self._parser.module() + module = self._evaluator.wrap(self._parser.module()) names, level, only_modules, unfinished_dotted = \ helpers.check_error_statements(module, self._pos) completion_names = [] if names is not None: - imp_names = [n for n in names if n.end_pos < self._pos] + imp_names = tuple(n for n in names if n.end_pos < self._pos) i = imports.get_importer(self._evaluator, imp_names, module, level) completion_names = i.completion_names(self._evaluator, only_modules) @@ -586,7 +591,7 @@ class Interpreter(Script): # changing). self._parser = UserContextParser(self._grammar, self.source, self._orig_path, self._pos, - self._user_context, + self._user_context, self._parsed_callback, use_fast_parser=False) interpreter.add_namespaces_to_parser(self._evaluator, namespaces, self._parser.module()) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 7bd1e336..3643d987 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -162,15 +162,26 @@ class BaseDefinition(object): return string def _path(self): - """The module path.""" + """The path to a module/class/function definition.""" path = [] par = self._definition while par is not None: if isinstance(par, pr.Import): path += imports.ImportWrapper(self._evaluator, self._name).import_path break - with common.ignored(AttributeError): - path.insert(0, par.name) + try: + name = par.name + except AttributeError: + pass + else: + if isinstance(par, er.ModuleWrapper): + #module = er.wrap(self._evaluator, par) + # TODO just make the path dotted from the beginning, we + # shouldn't really split here. + path[0:0] = par.py__name__().split('.') + break + else: + path.insert(0, unicode(name)) par = par.parent return path diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index 69708270..d2e21322 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -82,12 +82,18 @@ class Evaluator(object): def __init__(self, grammar): self.grammar = grammar self.memoize_cache = {} # for memoize decorators + # To memorize module names (that are assigned to modules by the import + # logic) -> a ``__name__`` is given. + self.module_name_cache = {} self.import_cache = {} # like `sys.modules`. self.compiled_cache = {} # see `compiled.create()` self.recursion_detector = recursion.RecursionDetector() self.execution_recursion_detector = recursion.ExecutionRecursionDetector() self.analysis = [] + def wrap(self, element): + return er.wrap(self, element) + def find_types(self, scope, name_str, position=None, search_global=False, is_goto=False): """ diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 16071608..365b72d1 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -40,7 +40,7 @@ class ModuleNotFound(Exception): def completion_names(evaluator, imp, pos): name = imp.name_for_position(pos) - module = imp.get_parent_until() + module = evaluator.wrap(imp.get_parent_until()) if name is None: level = 0 for node in imp.children: @@ -79,7 +79,7 @@ class ImportWrapper(pr.Base): return [] try: - module = self._import.get_parent_until() + module = self._evaluator.wrap(self._import.get_parent_until()) import_path = self._import.path_for_name(self._name) importer = get_importer(self._evaluator, tuple(import_path), module, self._import.level) @@ -157,16 +157,25 @@ def get_importer(evaluator, import_path, module, level=0): Checks the evaluator caches first, which resembles the ``sys.modules`` cache and speeds up libraries like ``numpy``. """ - import_path = tuple(import_path) # We use it as hash in the import cache. - if level != 0: - # Only absolute imports should be cached. Otherwise we have a mess. - # TODO Maybe calculate the absolute import and save it here? - return _Importer(evaluator, import_path, module, level) + if level: + base = module.py__name__().split('.') + if level > len(base): + # TODO add import error. + debug.warning('Attempted relative import beyond top-level package.') + # TODO this is just in the wrong place. + return _Importer(evaluator, import_path, module, level) + else: + # Here we basically rewrite the level to 0. + import_path = tuple(base) + import_path + + + + check_import_path = tuple(unicode(i) for i in import_path) try: - return evaluator.import_cache[import_path] + return evaluator.import_cache[check_import_path] except KeyError: - importer = _Importer(evaluator, import_path, module, level) - evaluator.import_cache[import_path] = importer + importer = _Importer(evaluator, import_path, module, level=0) + evaluator.import_cache[check_import_path] = importer return importer @@ -280,10 +289,11 @@ class _Importer(object): else: sys_path_mod = list(get_sys_path()) - from jedi.evaluate.representation import ModuleWrapper module, rest = self._follow_sys_path(sys_path_mod) if isinstance(module, pr.Module): - return ModuleWrapper(self._evaluator, module), rest + # TODO this looks strange. do we really need to check and should + # this transformation happen here? + return self._evaluator.wrap(module), rest return module, rest def namespace_packages(self, found_path, import_path): @@ -375,6 +385,11 @@ class _Importer(object): path = current_namespace[1] is_package_directory = current_namespace[2] + module_names = list(self.str_import_path) + for _ in rest: + module_names.pop() + module_name = '.'.join(module_names) + f = None if is_package_directory or current_namespace[0]: # is a directory module @@ -393,9 +408,11 @@ class _Importer(object): else: source = current_namespace[0].read() current_namespace[0].close() - return _load_module(self._evaluator, path, source, sys_path=sys_path), rest + return _load_module(self._evaluator, path, source, + sys_path=sys_path, module_name=module_name), rest else: - return _load_module(self._evaluator, name=path, sys_path=sys_path), rest + return _load_module(self._evaluator, name=path, + sys_path=sys_path, module_name=module_name), rest def _generate_name(self, name): return helpers.FakeName(name, parent=self.module) @@ -482,13 +499,13 @@ class _Importer(object): '__init__.py') if os.path.exists(rel_path): module = _load_module(self._evaluator, rel_path) - module = er.wrap(self._evaluator, module) + module = self._evaluator.wrap(module) for names_dict in module.names_dicts(search_global=False): names += chain.from_iterable(names_dict.values()) return names -def _load_module(evaluator, path=None, source=None, name=None, sys_path=None): +def _load_module(evaluator, path=None, source=None, name=None, sys_path=None, module_name=None): def load(source): dotted_path = path and compiled.dotted_from_fs_path(path, sys_path) if path is not None and path.endswith('.py') \ @@ -501,10 +518,15 @@ def _load_module(evaluator, path=None, source=None, name=None, sys_path=None): p = path or name p = fast.FastParser(evaluator.grammar, common.source_to_unicode(source), p) cache.save_parser(path, name, p) + return p.module cached = cache.load_parser(path, name) - return load(source) if cached is None else cached.module + module = load(source) if cached is None else cached.module + # TODO return mod instead of just something. + module = evaluator.wrap(module) + evaluator.module_name_cache[module] = module_name + return module def get_modules_containing_name(evaluator, mods, name): diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index 7801e877..894798fc 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -759,6 +759,9 @@ class ModuleWrapper(use_metaclass(CachedMetaClass, pr.Module, Wrapper)): def name(self): return helpers.FakeName(unicode(self.base.name), self, (1, 0)) + def py__name__(self): + return self._evaluator.module_name_cache[self] + @memoize_default() def _sub_modules_dict(self): """ diff --git a/jedi/parser/user_context.py b/jedi/parser/user_context.py index 3597c577..1e12d64d 100644 --- a/jedi/parser/user_context.py +++ b/jedi/parser/user_context.py @@ -266,13 +266,14 @@ class UserContext(object): class UserContextParser(object): def __init__(self, grammar, source, path, position, user_context, - use_fast_parser=True): + parser_done_callback, use_fast_parser=True): self._grammar = grammar self._source = source self._path = path and os.path.abspath(path) self._position = position self._user_context = user_context self._use_fast_parser = use_fast_parser + self._parser_done_callback = parser_done_callback @cache.underscore_memoization def _parser(self): @@ -283,6 +284,7 @@ class UserContextParser(object): cache.save_parser(self._path, None, parser, pickling=False) else: parser = Parser(self._grammar, self._source, self._path) + self._parser_done_callback(parser) return parser @cache.underscore_memoization diff --git a/test/test_api/test_full_name.py b/test/test_api/test_full_name.py index 58b77d05..b96c5481 100644 --- a/test/test_api/test_full_name.py +++ b/test/test_api/test_full_name.py @@ -78,3 +78,14 @@ class TestFullDefinedName(TestCase): from os.path import join from os import path as opath """, ['os', 'os.path', 'os.path.join', 'os.path']) + + +def test_sub_module(): + """ + ``full_name needs to check sys.path to actually find it's real path module + path. + """ + defs = jedi.Script('from jedi.api import classes; classes').goto_definitions() + assert [d.full_name for d in defs] == ['jedi.api.classes'] + defs = jedi.Script('import jedi.api; jedi.api').goto_definitions() + assert [d.full_name for d in defs] == ['jedi.api'] diff --git a/test/test_parser/test_parser.py b/test/test_parser/test_parser.py index f1a0da70..a2c896a7 100644 --- a/test/test_parser/test_parser.py +++ b/test/test_parser/test_parser.py @@ -15,7 +15,7 @@ def test_user_statement_on_import(): " time)") for pos in [(2, 1), (2, 4)]: - p = UserContextParser(load_grammar(), s, None, pos, None).user_stmt() + p = UserContextParser(load_grammar(), s, None, pos, None, lambda x: 1).user_stmt() assert isinstance(p, pt.Import) assert [str(n) for n in p.get_defined_names()] == ['time']