diff --git a/jedi/api/completion.py b/jedi/api/completion.py index ef95f740..c6776b90 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -19,7 +19,7 @@ from jedi.inference.base_value import ValueSet from jedi.inference.helpers import infer_call_of_leaf, parse_dotted_names from jedi.inference.context import get_global_filters from jedi.inference.value import TreeInstance, ModuleValue -from jedi.inference.names import ParamNameWrapper +from jedi.inference.names import ParamNameWrapper, SubModuleName from jedi.inference.gradual.conversion import convert_values, convert_names from jedi.parser_utils import cut_value_at_position from jedi.plugins import plugin_manager @@ -597,7 +597,10 @@ def search_in_module(inference_state, module_context, names, wanted_names, string = n.string_name.lower() if complete and helpers.match(string, last_name, fuzzy=fuzzy) \ or not complete and string == last_name: - names = [n] + if isinstance(n, SubModuleName): + names = [v.name for v in n.infer()] + else: + names = [n] if convert: names = convert_names(names) for n2 in names: diff --git a/jedi/api/project.py b/jedi/api/project.py index c04d703c..ae837b90 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -16,7 +16,7 @@ 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_folders_and_files, search_in_file_ios -from jedi.file_io import FolderIO, FileIO +from jedi.file_io import FolderIO from jedi.common.utils import traverse_parents _CONFIG_FOLDER = '.jedi' @@ -25,6 +25,23 @@ _CONTAINS_POTENTIAL_PROJECT = 'setup.py', '.git', '.hg', 'requirements.txt', 'MA _SERIALIZER_VERSION = 1 +def _try_to_skip_duplicates(func): + def wrapper(*args, **kwargs): + found_tree_nodes = [] + found_modules = [] + for definition in func(*args, **kwargs): + tree_node = definition._name.tree_name + if tree_node is not None and tree_node in found_tree_nodes: + continue + if definition.type == 'module' and definition.module_path is not None: + if definition.module_path in found_modules: + continue + found_modules.append(definition.module_path) + yield definition + found_tree_nodes.append(tree_node) + return wrapper + + def _remove_duplicates_from_path(path): used = set() for p in path: @@ -174,6 +191,7 @@ class Project(object): self._environment = get_cached_default_environment() return self._environment + @_try_to_skip_duplicates def search(self, string, complete=False, all_scopes=False): """ Returns a generator of names @@ -215,7 +233,7 @@ class Project(object): else: file_ios.append(file_io) file_name = os.path.basename(file_io.path) - if file_name in (name + '.py', name + 'pyi'): + if file_name in (name + '.py', name + '.pyi'): m = load_module_from_path(inference_state, file_io).as_context() else: continue @@ -229,16 +247,19 @@ class Project(object): wanted_names=wanted_names, complete=complete, convert=True, + ignore_imports=True, ): yield x # Python 2... # 2. Search for identifiers in the project. 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) + names = [module_context.create_name(n) for n in names] + names = _remove_imports(names) for x in search_in_module( inference_state, module_context, - names=[module_context.create_name(n) for n in names], + names=names, wanted_type=wanted_type, wanted_names=wanted_names, complete=complete, @@ -327,10 +348,8 @@ def get_default_project(path=None): return Project(curdir) -def _get_sys_path_folder_and_file_ios(sys_path): - for p in sys_path: - for dir_entry in scandir(p): - if dir_entry.is_dir(): - yield FolderIO(p), None - else: - yield None, FileIO(p) +def _remove_imports(names): + return [ + n for n in names + if n.tree_name is None or n.api_type != 'module' + ] diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index c1ec688d..3432e171 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -16,7 +16,7 @@ import os from parso.python import tree from parso.tree import search_ancestor -from jedi._compatibility import ImplicitNSInfo, force_unicode +from jedi._compatibility import ImplicitNSInfo, force_unicode, FileNotFoundError from jedi import debug from jedi import settings from jedi.parser_utils import get_cached_code_lines @@ -28,7 +28,7 @@ from jedi.inference.utils import unite from jedi.inference.cache import inference_state_method_cache from jedi.inference.names import ImportName, SubModuleName from jedi.inference.base_value import ValueSet, NO_VALUES -from jedi.inference.gradual.typeshed import import_module_decorator +from jedi.inference.gradual.typeshed import import_module_decorator, create_stub_module from jedi.inference.value.module import iter_module_names as module_iter_module_names from jedi.plugins import plugin_manager @@ -437,10 +437,18 @@ def _load_python_module(inference_state, file_io, use_latest_grammar=is_stub, ) - from jedi.inference.gradual.typeshed import create_stub_module if is_stub: + folder_io = file_io.get_parent_folder() + python_file_io = folder_io.get_file_io(import_names[-1] + '.py') + try: + v = load_module_from_path(inference_state, python_file_io, import_names[:-1]) + values = ValueSet([v]) + except FileNotFoundError: + values = NO_VALUES + return create_stub_module( - inference_state, NO_VALUES, module_node, file_io, import_names) + inference_state, values, module_node, file_io, import_names) + from jedi.inference.value import ModuleValue return ModuleValue( inference_state, module_node, diff --git a/jedi/inference/value/namespace.py b/jedi/inference/value/namespace.py index ec41e6f1..48a09e4a 100644 --- a/jedi/inference/value/namespace.py +++ b/jedi/inference/value/namespace.py @@ -35,6 +35,9 @@ class ImplicitNamespaceValue(Value, SubModuleDictMixin): def get_filters(self, origin_scope=None): yield DictFilter(self.sub_modules_dict()) + def get_qualified_names(self): + return () + @property @inference_state_method_cache() def name(self): diff --git a/test/test_api/test_project.py b/test/test_api/test_project.py index dbd5cd37..100f974c 100644 --- a/test/test_api/test_project.py +++ b/test/test_api/test_project.py @@ -90,17 +90,27 @@ def test_load_save_project(tmpdir): dict(complete=True)), # With namespace & stub - ('with_python.module', ['examples.stub_packages.with_python.module'], {}), - ('with_python.modul', ['examples.stub_packages.with_python.module'], + ('with_python.module', ['stub:examples.stub_packages.with_python.module', + 'examples.stub_packages.with_python.module'], {}), + ('with_python.modul', ['stub:examples.stub_packages.with_python.module', + 'examples.stub_packages.with_python.module'], dict(complete=True)), ('no_python.foo', ['stub:examples.stub_packages.no_python.foo'], {}), ('no_python.fo', ['stub:examples.stub_packages.no_python.foo'], dict(complete=True)), ('with_python-stubs.module', [], {}), ('no_python-stubs.foo', [], {}), - ('with_python', ['stub:examples.stub_packages.with_python', 'examples.stub_packages.with_python'], {}), + # Both locations are given, because they live in separate folders (one + # suffixed with -stubs. + ('with_python', ['stub:examples.stub_packages.with_python', + 'examples.stub_packages.with_python'], {}), ('no_python', ['stub:examples.stub_packages.no_python'], {}), + ('stub_only', ['stub:completion.stub_folder.stub_only', + 'stub:examples.stub_packages.with_python.stub_only'], {}), + ('with_stub', ['completion.stub_folder.with_stub'], {}), + + # On sys path ('sys.path', ['stub:sys.path'], {}), ('json.dumps', ['json.dumps'], {}), # stdlib + stub