diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 4120c5bc..d65d897b 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -278,20 +278,17 @@ class Importer(object): # We can take the first element, because only the os special # case yields multiple modules, which is not important for # further imports. - base = list(bases)[0] + parent_module = list(bases)[0] # This is a huge exception, we follow a nested import # ``os.path``, because it's a very important one in Python # that is being achieved by messing with ``sys.modules`` in # ``os``. if [str(i) for i in import_path] == ['os', 'path']: - return self._evaluator.find_types(base, 'path') + return self._evaluator.find_types(parent_module, 'path') try: - # It's possible that by giving it always the sys path (and not - # the __path__ attribute of the parent, we get wrong results - # and nested namespace packages don't work. But I'm not sure. - paths = base.py__path__(sys_path) + paths = parent_module.py__path__() except AttributeError: # The module is not a package. _add_error(self._evaluator, import_path[-1]) @@ -311,6 +308,7 @@ class Importer(object): _add_error(self._evaluator, import_path[-1]) return set() else: + parent_module = None try: debug.dbg('search_module %s in %s', import_parts[-1], self.file_path) # Override the sys.path. It works only good that way. @@ -341,7 +339,7 @@ class Importer(object): if module_file is None and not module_path.endswith(('.py', '.zip', '.egg')): module = compiled.load_module(self._evaluator, module_path) else: - module = _load_module(self._evaluator, module_path, source, sys_path) + module = _load_module(self._evaluator, module_path, source, sys_path, parent_module) if module is None: # The file might raise an ImportError e.g. and therefore not be @@ -404,7 +402,7 @@ class Importer(object): # namespace packages if isinstance(scope, tree.Module) and scope.path.endswith('__init__.py'): - paths = scope.py__path__(self.sys_path_with_modifications()) + paths = scope.py__path__() names += self._get_module_names(paths) if only_modules: @@ -437,7 +435,7 @@ class Importer(object): return names -def _load_module(evaluator, path=None, source=None, sys_path=None): +def _load_module(evaluator, path=None, source=None, sys_path=None, parent_module=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', '.zip', '.egg')) \ @@ -450,7 +448,8 @@ def _load_module(evaluator, path=None, source=None, sys_path=None): p = path p = fast.FastParser(evaluator.grammar, common.source_to_unicode(source), p) save_parser(path, p) - return p.module + from jedi.evaluate.representation import ModuleWrapper + return ModuleWrapper(evaluator, p.module, parent_module) if sys_path is None: sys_path = evaluator.sys_path diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index 8e57bb36..4502d10f 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -806,9 +806,10 @@ class GlobalName(helpers.FakeName): class ModuleWrapper(use_metaclass(CachedMetaClass, tree.Module, Wrapper)): - def __init__(self, evaluator, module): + def __init__(self, evaluator, module, parent_module=None): self._evaluator = evaluator self.base = self._module = module + self._parent_module = parent_module def names_dicts(self, search_global): yield self.base.names_dict @@ -854,6 +855,10 @@ class ModuleWrapper(use_metaclass(CachedMetaClass, tree.Module, Wrapper)): return helpers.FakeName(unicode(self.base.name), self, (1, 0)) def _get_init_directory(self): + """ + :return: The path to the directory of a package. None in case it's not + a package. + """ for suffix, _, _ in imp.get_suffixes(): ending = '__init__' + suffix py__file__ = self.py__file__() @@ -884,6 +889,30 @@ class ModuleWrapper(use_metaclass(CachedMetaClass, tree.Module, Wrapper)): else: return self.py__name__() + def _py__path__(self): + if self._parent_module is None: + search_path = self._evaluator.sys_path + else: + search_path = self._parent_module.py__path__() + init_path = self.py__file__() + if os.path.basename(init_path) == '__init__.py': + with open(init_path, 'rb') as f: + content = common.source_to_unicode(f.read()) + # these are strings that need to be used for namespace packages, + # the first one is ``pkgutil``, the second ``pkg_resources``. + options = ('declare_namespace(__name__)', 'extend_path(__path__') + if options[0] in content or options[1] in content: + # It is a namespace, now try to find the rest of the + # modules on sys_path or whatever the search_path is. + paths = set() + for s in search_path: + other = os.path.join(s, unicode(self.name)) + if os.path.isdir(other): + paths.add(other) + return list(paths) + # Default to this. + return [self._get_init_directory()] + @property def py__path__(self): """ @@ -896,33 +925,12 @@ class ModuleWrapper(use_metaclass(CachedMetaClass, tree.Module, Wrapper)): is a list of paths (strings). Raises an AttributeError if the module is not a package. """ - def return_value(search_path): - init_path = self.py__file__() - if os.path.basename(init_path) == '__init__.py': - - with open(init_path, 'rb') as f: - content = common.source_to_unicode(f.read()) - # these are strings that need to be used for namespace packages, - # the first one is ``pkgutil``, the second ``pkg_resources``. - options = ('declare_namespace(__name__)', 'extend_path(__path__') - if options[0] in content or options[1] in content: - # It is a namespace, now try to find the rest of the - # modules on sys_path or whatever the search_path is. - paths = set() - for s in search_path: - other = os.path.join(s, unicode(self.name)) - if os.path.isdir(other): - paths.add(other) - return list(paths) - # Default to this. - return [path] - path = self._get_init_directory() if path is None: raise AttributeError('Only packages have __path__ attributes.') else: - return return_value + return self._py__path__ @memoize_default() def _sub_modules_dict(self):