Fixed ZIP completion.

This commit is contained in:
Joseph Birkner
2021-04-28 19:12:58 +02:00
parent dcea842ac2
commit a340fe077e
3 changed files with 38 additions and 8 deletions

View File

@@ -61,6 +61,7 @@ Code Contributors
- Vladislav Serebrennikov (@endilll) - Vladislav Serebrennikov (@endilll)
- Andrii Kolomoiets (@muffinmad) - Andrii Kolomoiets (@muffinmad)
- Leo Ryu (@Leo-Ryu) - Leo Ryu (@Leo-Ryu)
- Joseph Birkner (@josephbirkner)
And a few more "anonymous" contributors. And a few more "anonymous" contributors.

View File

@@ -4,7 +4,8 @@ import inspect
import importlib import importlib
import warnings import warnings
from pathlib import Path from pathlib import Path
from zipimport import zipimporter from zipfile import ZipFile
from zipimport import zipimporter, ZipImportError
from importlib.machinery import all_suffixes from importlib.machinery import all_suffixes
from jedi.inference.compiled import access from jedi.inference.compiled import access
@@ -92,15 +93,22 @@ def _iter_module_names(inference_state, paths):
# Python modules/packages # Python modules/packages
for path in paths: for path in paths:
try: try:
dirs = os.scandir(path) dir_entries = ((entry.name, entry.is_dir()) for entry in os.scandir(path))
except OSError: except OSError:
# The file might not exist or reading it might lead to an error. try:
debug.warning("Not possible to list directory: %s", path) zip_import_info = zipimporter(path)
continue # Unfortunately, there is no public way to access zipimporter's
for dir_entry in dirs: # private _files member. We therefore have to use a
name = dir_entry.name # 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 # First Namespaces then modules/stubs
if dir_entry.is_dir(): if is_dir:
# pycache is obviously not an interesting namespace. Also the # pycache is obviously not an interesting namespace. Also the
# name must be a valid identifier. # name must be a valid identifier.
if name != '__pycache__' and name.isidentifier(): if name != '__pycache__' and name.isidentifier():
@@ -229,6 +237,17 @@ def _get_source(loader, fullname):
name=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: class ImplicitNSInfo:
"""Stores information returned from an implicit namespace spec""" """Stores information returned from an implicit namespace spec"""
def __init__(self, name, paths): def __init__(self, name, paths):

View File

@@ -101,6 +101,16 @@ def test_correct_zip_package_behavior(Script, inference_state, environment, code
assert value.py__package__() == [] 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): def test_find_module_not_package_zipped(Script, inference_state, environment):
path = get_example_dir('zipped_imports', 'not_pkg.zip') path = get_example_dir('zipped_imports', 'not_pkg.zip')
sys_path = environment.get_sys_path() + [path] sys_path = environment.get_sys_path() + [path]