From a340fe077e553baf956a25c62fc6e3c25041029a Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 28 Apr 2021 19:12:58 +0200 Subject: [PATCH] Fixed ZIP completion. --- AUTHORS.txt | 1 + .../compiled/subprocess/functions.py | 35 ++++++++++++++----- test/test_inference/test_imports.py | 10 ++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 6ffe9dfd..b8b10a93 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -61,6 +61,7 @@ Code Contributors - Vladislav Serebrennikov (@endilll) - Andrii Kolomoiets (@muffinmad) - Leo Ryu (@Leo-Ryu) +- Joseph Birkner (@josephbirkner) And a few more "anonymous" contributors. diff --git a/jedi/inference/compiled/subprocess/functions.py b/jedi/inference/compiled/subprocess/functions.py index 33d6c1fa..5070c664 100644 --- a/jedi/inference/compiled/subprocess/functions.py +++ b/jedi/inference/compiled/subprocess/functions.py @@ -4,7 +4,8 @@ import inspect import importlib import warnings from pathlib import Path -from zipimport import zipimporter +from zipfile import ZipFile +from zipimport import zipimporter, ZipImportError from importlib.machinery import all_suffixes from jedi.inference.compiled import access @@ -92,15 +93,22 @@ def _iter_module_names(inference_state, paths): # Python modules/packages for path in paths: try: - dirs = os.scandir(path) + dir_entries = ((entry.name, entry.is_dir()) for entry in os.scandir(path)) except OSError: - # The file might not exist or reading it might lead to an error. - debug.warning("Not possible to list directory: %s", path) - continue - for dir_entry in dirs: - name = dir_entry.name + try: + zip_import_info = zipimporter(path) + # Unfortunately, there is no public way to access zipimporter's + # private _files member. We therefore have to use a + # custom function to iterate over the files. + dir_entries = _zip_list_subdirectory( + zip_import_info.archive, zip_import_info.prefix) + except ZipImportError: + # The file might not exist or reading it might lead to an error. + debug.warning("Not possible to list directory: %s", path) + continue + for name, is_dir in dir_entries: # First Namespaces then modules/stubs - if dir_entry.is_dir(): + if is_dir: # pycache is obviously not an interesting namespace. Also the # name must be a valid identifier. if name != '__pycache__' and name.isidentifier(): @@ -229,6 +237,17 @@ def _get_source(loader, fullname): name=fullname) +def _zip_list_subdirectory(zip_path, zip_subdir_path): + zip_file = ZipFile(zip_path) + zip_subdir_path = Path(zip_subdir_path) + zip_content_file_paths = zip_file.namelist() + for raw_file_name in zip_content_file_paths: + file_path = Path(raw_file_name) + if file_path.parent == zip_subdir_path: + file_path = file_path.relative_to(zip_subdir_path) + yield file_path.name, raw_file_name.endswith("/") + + class ImplicitNSInfo: """Stores information returned from an implicit namespace spec""" def __init__(self, name, paths): diff --git a/test/test_inference/test_imports.py b/test/test_inference/test_imports.py index 7dceef51..51e65474 100644 --- a/test/test_inference/test_imports.py +++ b/test/test_inference/test_imports.py @@ -101,6 +101,16 @@ def test_correct_zip_package_behavior(Script, inference_state, environment, code assert value.py__package__() == [] +@pytest.mark.parametrize("code,names", [ + ("from pkg.", {"module", "nested", "namespace"}), + ("from pkg.nested.", {"nested_module"}) +]) +def test_zip_package_import_complete(Script, environment, code, names): + sys_path = environment.get_sys_path() + [str(pkg_zip_path)] + completions = Script(code, project=Project('.', sys_path=sys_path)).complete() + assert names == {c.name for c in completions} + + def test_find_module_not_package_zipped(Script, inference_state, environment): path = get_example_dir('zipped_imports', 'not_pkg.zip') sys_path = environment.get_sys_path() + [path]