Skip to content

Commit

Permalink
Add a py__name__ call to modules. This makes listing the qualified na…
Browse files Browse the repository at this point in the history
…mes of modules possible (in combination with the module_name_cache). Fixes #519.
  • Loading branch information
davidhalter committed Apr 14, 2015
1 parent 2f64a83 commit 8fca3f7
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 26 deletions.
13 changes: 9 additions & 4 deletions jedi/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,15 @@ def __init__(self, source=None, line=None, column=None, path=None,
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):
"""
Expand All @@ -135,12 +140,12 @@ def completions(self):
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)

Expand Down Expand Up @@ -586,7 +591,7 @@ def __init__(self, source, namespaces, **kwds):
# 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())
Expand Down
17 changes: 14 additions & 3 deletions jedi/api/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,26 @@ def type(self):
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

Expand Down
6 changes: 6 additions & 0 deletions jedi/evaluate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
56 changes: 39 additions & 17 deletions jedi/evaluate/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self, name):

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:
Expand Down Expand Up @@ -79,7 +79,7 @@ def follow(self, is_goto=False):
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)
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -280,10 +289,11 @@ def _real_follow_file_system(self):
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):
Expand Down Expand Up @@ -375,6 +385,11 @@ def follow_str(ns_path, string):
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
Expand All @@ -393,9 +408,11 @@ def follow_str(ns_path, string):
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)
Expand Down Expand Up @@ -482,13 +499,13 @@ def completion_names(self, evaluator, only_modules=False):
'__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') \
Expand All @@ -501,10 +518,15 @@ def load(source):
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):
Expand Down
3 changes: 3 additions & 0 deletions jedi/evaluate/representation.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,9 @@ def parent_callback():
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):
"""
Expand Down
4 changes: 3 additions & 1 deletion jedi/parser/user_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,14 @@ def get_position_line(self):

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):
Expand All @@ -283,6 +284,7 @@ def _parser(self):
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
Expand Down
11 changes: 11 additions & 0 deletions test/test_api/test_full_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,14 @@ def test_imports(self):
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']
2 changes: 1 addition & 1 deletion test/test_parser/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down

0 comments on commit 8fca3f7

Please sign in to comment.