Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bug finding implicit namespace packages #1784

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
59f1253
Fix bug finding implicit namespace packages
bbugyi200 Jul 2, 2021
d17c1a1
Add type hints to _find_module() function
bbugyi200 Jul 3, 2021
f979b2a
Add type signatures to get_module_info() function
bbugyi200 Jul 3, 2021
39b8e07
Add type annotations to import_module*() functions
bbugyi200 Jul 3, 2021
915bc00
Add inline comment
bbugyi200 Jul 3, 2021
cf0ae8b
Improve return type annotations
bbugyi200 Jul 3, 2021
48bcaa8
Add tests for bugfix
bbugyi200 Jul 3, 2021
0396e72
Revert 'sorted' back to 'set'
bbugyi200 Jul 3, 2021
120be69
Fix tests that were broken by adding new example directories
bbugyi200 Jul 3, 2021
ca4ebe2
Fix unrelated test_find_module_not_package_zipped() test
bbugyi200 Jul 3, 2021
53980af
Remove unused function
bbugyi200 Jul 3, 2021
708eb3b
Remove type signature from import_module_by_names()
bbugyi200 Jul 4, 2021
6d8b66e
Change type on sys_path to Sequence[str]
bbugyi200 Jul 4, 2021
6aad95a
Add bullet to changelog
bbugyi200 Jul 4, 2021
fe0e9f6
Add my name to AUTHORS.txt
bbugyi200 Jul 4, 2021
3fa026a
Reverse PR/issue links in changelog bullet
bbugyi200 Jul 4, 2021
a51f1e8
Simplify main fix in _find_module() function
bbugyi200 Jul 4, 2021
ead07aa
Simplify changes to _find_module() further
bbugyi200 Jul 4, 2021
68161e5
Last simplification
bbugyi200 Jul 4, 2021
44706a3
Remove unused imports
bbugyi200 Jul 4, 2021
149148c
Change name from 'path' to 'paths'
bbugyi200 Jul 4, 2021
daecd8b
Rename 'paths' back to 'path' since that's what importlib uses
bbugyi200 Jul 4, 2021
33d5a6e
Add type annotations to two more functions in _functions.py
bbugyi200 Jul 4, 2021
06bac01
Rename last 'paths' to 'path'
bbugyi200 Jul 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Code Contributors
- Andrii Kolomoiets (@muffinmad)
- Leo Ryu (@Leo-Ryu)
- Joseph Birkner (@josephbirkner)
- Bryan Bugyi (@bbugyi200) <[email protected]>

And a few more "anonymous" contributors.

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Unreleased
++++++++++

- Implict namespaces are now a separate types in ``Name().type``
- Fix bug finding implicit namespace packages (`Issue:#1759 <https://github.com/davidhalter/jedi/issues/1759>`_, `PR:#1784 <https://github.com/davidhalter/jedi/pull/1784>`_)

0.18.0 (2020-12-25)
+++++++++++++++++++
Expand Down
74 changes: 53 additions & 21 deletions jedi/inference/compiled/subprocess/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,29 @@
from pathlib import Path
from zipfile import ZipFile
from zipimport import zipimporter, ZipImportError
from importlib.machinery import all_suffixes
from io import FileIO
from typing import (
Any,
Optional,
Sequence,
Tuple,
TYPE_CHECKING,
Union,
)

from jedi.inference.compiled import access
from jedi import debug
from jedi import parser_utils
from jedi.file_io import KnownContentFileIO, ZipFileIO


if TYPE_CHECKING:
from jedi.inference import InferenceState


ModuleInfoResult = Tuple[Union[Any, FileIO, None], Optional[bool]]


def get_sys_path():
return sys.path

Expand All @@ -31,14 +46,29 @@ def create_simple_object(inference_state, obj):
return access.create_access_path(inference_state, obj)


def get_module_info(inference_state, sys_path=None, full_name=None, **kwargs):
def get_module_info(
inference_state: "InferenceState",
*,
string: str,
sys_path: Sequence[str] = None,
full_name: str = None,
path: Sequence[str] = None,
is_global_search: bool = True,
) -> ModuleInfoResult:
"""
Returns Tuple[Union[NamespaceInfo, FileIO, None], Optional[bool]]
"""
del inference_state

if sys_path is not None:
sys.path, temp = sys_path, sys.path
sys.path, temp = list(sys_path), sys.path
try:
return _find_module(full_name=full_name, **kwargs)
return _find_module(
string=string,
full_name=full_name,
path=path,
is_global_search=is_global_search,
)
except ImportError:
return None, None
finally:
Expand Down Expand Up @@ -69,18 +99,6 @@ def _test_print(inference_state, stderr=None, stdout=None):
sys.stdout.flush()


def _get_init_path(directory_path):
"""
The __init__ file can be searched in a directory. If found return it, else
None.
"""
for suffix in all_suffixes():
path = os.path.join(directory_path, '__init__' + suffix)
if os.path.exists(path):
return path
return None


def safe_literal_eval(inference_state, value):
return parser_utils.safe_literal_eval(value)

Expand Down Expand Up @@ -124,7 +142,12 @@ def _iter_module_names(inference_state, paths):
yield modname


def _find_module(string, path=None, full_name=None, is_global_search=True):
def _find_module(
string: str,
path: Sequence[str] = None,
full_name: str = None,
is_global_search: bool = True,
) -> ModuleInfoResult:
"""
Provides information about a module.

Expand All @@ -138,10 +161,11 @@ def _find_module(string, path=None, full_name=None, is_global_search=True):
loader = None

for finder in sys.meta_path:
if is_global_search and finder != importlib.machinery.PathFinder:
if is_global_search and finder != importlib.machinery.PathFinder: # type: ignore
p = None
else:
p = path

try:
find_spec = finder.find_spec
except AttributeError:
Expand All @@ -155,14 +179,22 @@ def _find_module(string, path=None, full_name=None, is_global_search=True):
if loader is None and not spec.has_location:
# This is a namespace package.
full_name = string if not path else full_name
implicit_ns_info = ImplicitNSInfo(full_name, spec.submodule_search_locations._path)
implicit_ns_info = ImplicitNSInfo(
full_name,
spec.submodule_search_locations._path, # type: ignore
)
return implicit_ns_info, True

break

return _find_module_py33(string, path, loader)


def _find_module_py33(string, path=None, loader=None, full_name=None, is_global_search=True):
def _find_module_py33(
string: str,
path: Sequence[str] = None,
loader: Any = None,
) -> ModuleInfoResult:
loader = loader or importlib.machinery.PathFinder.find_module(string, path)

if loader is None and path is None: # Fallback to find builtins
Expand All @@ -185,7 +217,7 @@ def _find_module_py33(string, path=None, loader=None, full_name=None, is_global_
return _from_loader(loader, string)


def _from_loader(loader, string):
def _from_loader(loader: Any, string: str) -> ModuleInfoResult:
try:
is_package_method = loader.is_package
except AttributeError:
Expand Down
33 changes: 18 additions & 15 deletions jedi/inference/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""
import os
from pathlib import Path
from typing import Any, Sequence, TYPE_CHECKING

from parso.python import tree
from parso.tree import search_ancestor
Expand All @@ -32,6 +33,10 @@
from jedi.plugins import plugin_manager


if TYPE_CHECKING:
from jedi.inference import InferenceState


class ModuleCache:
def __init__(self):
self._name_cache = {}
Expand Down Expand Up @@ -394,7 +399,12 @@ def import_module_by_names(inference_state, import_names, sys_path=None,

@plugin_manager.decorate()
@import_module_decorator
def import_module(inference_state, import_names, parent_module_value, sys_path):
def import_module(
inference_state: "InferenceState",
import_names: Sequence[str],
parent_module_value: Any,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the Any hints, they are pretty much useless (and also wrong).

sys_path: Sequence[str],
) -> ValueSet:
"""
This method is very similar to importlib's `_gcd_import`.
"""
Expand Down Expand Up @@ -422,20 +432,13 @@ def import_module(inference_state, import_names, parent_module_value, sys_path):
# The module might not be a package.
return NO_VALUES

for path in paths:
# At the moment we are only using one path. So this is
# not important to be correct.
if not isinstance(path, list):
path = [path]
file_io_or_ns, is_pkg = inference_state.compiled_subprocess.get_module_info(
string=import_names[-1],
path=path,
full_name=module_name,
is_global_search=False,
)
if is_pkg is not None:
break
else:
file_io_or_ns, is_pkg = inference_state.compiled_subprocess.get_module_info(
string=import_names[-1],
path=paths,
full_name=module_name,
is_global_search=False,
)
if is_pkg is None:
return NO_VALUES

if isinstance(file_io_or_ns, ImplicitNSInfo):
Expand Down
Empty file.
Empty file.
3 changes: 2 additions & 1 deletion test/test_api/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def test_load_save_project(tmpdir):
('examples.implicit_namespace_package.ns1.pkg.ns1_file',
['examples.implicit_namespace_package.ns1.pkg.ns1_file'], {}),
('implicit_namespace_package.ns1.pkg.',
['examples.implicit_namespace_package.ns1.pkg.ns1_file'],
['examples.implicit_namespace_package.ns1.pkg.ns1_file',
'examples.implicit_namespace_package.ns1.pkg.subpkg'],
dict(complete=True)),
('implicit_namespace_package.',
['examples.implicit_namespace_package.ns1',
Expand Down
43 changes: 34 additions & 9 deletions test/test_inference/test_implicit_namespace_package.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from test.helpers import get_example_dir, example_dir
from test.helpers import example_dir, get_example_dir
from typing import Any, Iterable

import pytest

from jedi import Project


Expand Down Expand Up @@ -28,7 +32,7 @@ def script_with_path(*args, **kwargs):
# completion
completions = script_with_path('from pkg import ').complete()
names = [c.name for c in completions]
compare = ['ns1_file', 'ns2_file']
compare = ['ns1_file', 'ns2_file', 'subpkg']
# must at least contain these items, other items are not important
assert set(compare) == set(names)

Expand Down Expand Up @@ -68,19 +72,40 @@ def test_implicit_namespace_package_import_autocomplete(Script):
assert [c.name for c in compl] == ['implicit_namespace_package']


def test_namespace_package_in_multiple_directories_autocompletion(Script):
code = 'from pkg.'
@pytest.mark.parametrize(
'code,expected',
[
('from pkg.', ['ns1_file', 'ns2_file', 'subpkg']),
('from pkg.subpkg.', ['ns3_file', 'ns4_file']),
]
)
def test_namespace_package_in_multiple_directories_autocompletion(
code: str,
expected: Iterable[str],
Script: Any,
) -> None:
sys_path = [get_example_dir('implicit_namespace_package', 'ns1'),
get_example_dir('implicit_namespace_package', 'ns2')]

project = Project('.', sys_path=sys_path)
script = Script(code, project=project)
compl = script.complete()
assert set(c.name for c in compl) == set(['ns1_file', 'ns2_file'])


def test_namespace_package_in_multiple_directories_goto_definition(Script):
code = 'from pkg import ns1_file'
assert set(c.name for c in compl) == set(expected)


@pytest.mark.parametrize(
'code',
[
'from pkg import ns1_file',
'from pkg import ns2_file',
'from pkg.subpkg import ns3_file',
'from pkg.subpkg import ns4_file',
]
)
def test_namespace_package_in_multiple_directories_goto_definition(
code: str,
Script: Any,
) -> None:
sys_path = [get_example_dir('implicit_namespace_package', 'ns1'),
get_example_dir('implicit_namespace_package', 'ns2')]
project = Project('.', sys_path=sys_path)
Expand Down
2 changes: 1 addition & 1 deletion test/test_inference/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def test_find_module_not_package_zipped(Script, inference_state, environment):
assert len(script.complete()) == 1

file_io, is_package = inference_state.compiled_subprocess.get_module_info(
sys_path=map(str, sys_path),
sys_path=list(map(str, sys_path)),
davidhalter marked this conversation as resolved.
Show resolved Hide resolved
string='not_pkg',
full_name='not_pkg'
)
Expand Down