diff --git a/jedi/api/project.py b/jedi/api/project.py index e3d45879..615f5039 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -10,9 +10,10 @@ from jedi.api.exceptions import WrongVersion from jedi.api.completion import search_in_module from jedi.api.helpers import split_search_string, get_module_names from jedi._compatibility import force_unicode +from jedi.inference.imports import load_module_from_path from jedi.inference.sys_path import discover_buildout_paths from jedi.inference.cache import inference_state_as_method_param_cache -from jedi.inference.references import recurse_find_python_files, search_in_file_ios +from jedi.inference.references import recurse_find_python_folders_and_files, search_in_file_ios from jedi.inference import InferenceState from jedi.file_io import FolderIO from jedi.common.utils import traverse_parents @@ -184,8 +185,43 @@ class Project(object): wanted_type, wanted_names = split_search_string(string) name = wanted_names[0] - file_io_iterator = recurse_find_python_files(FolderIO(self._path)) - for module_context in search_in_file_ios(inference_state, file_io_iterator, name): + ios = recurse_find_python_folders_and_files(FolderIO(self._path)) + file_ios = [] + + for folder_io, file_io in ios: + if file_io is None: + file_name = folder_io.get_base_name() + if file_name == name: + f = folder_io.get_file_io('__init__.py') + try: + m = load_module_from_path(inference_state, f).as_context() + except FileNotFoundError: + f = folder_io.get_file_io('__init__.py') + try: + m = load_module_from_path(inference_state, f).as_context() + except FileNotFoundError: + m = namespace + else: + continue + else: + file_ios.append(file_io) + file_name = os.path.basename(file_io.path) + if file_name in (name + '.py', name + 'pyi'): + m = load_module_from_path(inference_state, file_io).as_context() + else: + continue + + for x in search_in_module( + inference_state, + m, + names=[m.name], + wanted_type=wanted_type, + wanted_names=wanted_names, + complete=complete + ): + yield x # Python 2... + + for module_context in search_in_file_ios(inference_state, file_ios, name): names = get_module_names(module_context.tree_node, all_scopes=all_scopes) for x in search_in_module( inference_state, diff --git a/jedi/file_io.py b/jedi/file_io.py index 19a8246a..c4a5d24a 100644 --- a/jedi/file_io.py +++ b/jedi/file_io.py @@ -7,6 +7,9 @@ class AbstractFolderIO(object): def __init__(self, path): self.path = path + def get_base_name(self): + raise NotImplementedError + def list(self): raise NotImplementedError diff --git a/jedi/inference/references.py b/jedi/inference/references.py index 0465ce75..91cdcac8 100644 --- a/jedi/inference/references.py +++ b/jedi/inference/references.py @@ -193,14 +193,15 @@ def gitignored_lines(folder_io, file_io): return ignored_paths, ignored_names -def recurse_find_python_files(folder_io, except_paths=()): +def recurse_find_python_folders_and_files(folder_io, except_paths=()): + except_paths = set(except_paths) for root_folder_io, folder_ios, file_ios in folder_io.walk(): # Delete folders that we don't want to iterate over. for file_io in file_ios: path = file_io.path if path.endswith('.py') or path.endswith('.pyi'): if path not in except_paths: - yield file_io + yield None, file_io if path.endswith('.gitignore'): ignored_paths, ignored_names = \ @@ -213,6 +214,14 @@ def recurse_find_python_files(folder_io, except_paths=()): if folder_io.path not in except_paths and folder_io.get_base_name() not in _IGNORE_FOLDERS ] + for folder_io in folder_ios: + yield folder_io, None + + +def recurse_find_python_files(folder_io, except_paths=()): + for folder_io, file_io in recurse_find_python_folders_and_files(folder_io, except_paths): + if file_io is not None: + yield file_io def _find_python_files_in_sys_path(inference_state, module_contexts): diff --git a/test/test_api/test_project.py b/test/test_api/test_project.py index 8a56d66f..41dd4dfe 100644 --- a/test/test_api/test_project.py +++ b/test/test_api/test_project.py @@ -46,7 +46,7 @@ def test_load_save_project(tmpdir): @pytest.mark.parametrize( 'string, full_names, kwargs', [ ('test_load_save_project', ['test_api.test_project.test_load_save_project'], {}), - ('test_load_savep', [], {'complete': True}), + ('test_load_savep', [], dict(complete=True)), ('test_load_save_p', ['test_api.test_project.test_load_save_project'], dict(complete=True)), ('test_load_save_p', ['test_api.test_project.test_load_save_project'], @@ -66,6 +66,13 @@ def test_load_save_project(tmpdir): ('foo sample_int.real', [], {}), ('def sample_int.real', ['builtins.int.real'], {}), ('function sample_int.real', ['builtins.int.real'], {}), + + # With modules + ('test_project.test_search', ['test_api.test_project.test_search'], {}), + ('test_project.test_searc', ['test_api.test_project.test_search'], dict(complete=True)), + ('test_api.test_project.test_search', ['test_api.test_project.test_search'], {}), + ('test_api.test_project.test_sear', ['test_api.test_project.test_search'], + dict(complete=True)), ] ) @pytest.mark.skipif(sys.version_info < (3, 6), reason="Ignore Python 2, because EOL")