diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index b11881a4..162c9589 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -5,133 +5,8 @@ different Python versions. import errno import sys import os -import warnings import subprocess import pickle -import importlib -from zipimport import zipimporter - -from jedi.file_io import KnownContentFileIO, ZipFileIO - - -def find_module(string, path=None, full_name=None, is_global_search=True): - """ - Provides information about a module. - - This function isolates the differences in importing libraries introduced with - python 3.3 on; it gets a module name and optionally a path. It will return a - tuple containin an open file for the module (if not builtin), the filename - or the name of the module if it is a builtin one and a boolean indicating - if the module is contained in a package. - """ - spec = None - loader = None - - for finder in sys.meta_path: - if is_global_search and finder != importlib.machinery.PathFinder: - p = None - else: - p = path - try: - find_spec = finder.find_spec - except AttributeError: - # These are old-school clases that still have a different API, just - # ignore those. - continue - - spec = find_spec(string, p) - if spec is not None: - loader = spec.loader - 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) - 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): - loader = loader or importlib.machinery.PathFinder.find_module(string, path) - - if loader is None and path is None: # Fallback to find builtins - try: - with warnings.catch_warnings(record=True): - # Mute "DeprecationWarning: Use importlib.util.find_spec() - # instead." While we should replace that in the future, it's - # probably good to wait until we deprecate Python 3.3, since - # it was added in Python 3.4 and find_loader hasn't been - # removed in 3.6. - loader = importlib.find_loader(string) - except ValueError as e: - # See #491. Importlib might raise a ValueError, to avoid this, we - # just raise an ImportError to fix the issue. - raise ImportError("Originally " + repr(e)) - - if loader is None: - raise ImportError("Couldn't find a loader for {}".format(string)) - - return _from_loader(loader, string) - - -def _from_loader(loader, string): - try: - is_package_method = loader.is_package - except AttributeError: - is_package = False - else: - is_package = is_package_method(string) - try: - get_filename = loader.get_filename - except AttributeError: - return None, is_package - else: - module_path = cast_path(get_filename(string)) - - # To avoid unicode and read bytes, "overwrite" loader.get_source if - # possible. - try: - f = type(loader).get_source - except AttributeError: - raise ImportError("get_source was not defined on loader") - - if f is not importlib.machinery.SourceFileLoader.get_source: - # Unfortunately we are reading unicode here, not bytes. - # It seems hard to get bytes, because the zip importer - # logic just unpacks the zip file and returns a file descriptor - # that we cannot as easily access. Therefore we just read it as - # a string in the cases where get_source was overwritten. - code = loader.get_source(string) - else: - code = _get_source(loader, string) - - if code is None: - return None, is_package - if isinstance(loader, zipimporter): - return ZipFileIO(module_path, code, cast_path(loader.archive)), is_package - - return KnownContentFileIO(module_path, code), is_package - - -def _get_source(loader, fullname): - """ - This method is here as a replacement for SourceLoader.get_source. That - method returns unicode, but we prefer bytes. - """ - path = loader.get_filename(fullname) - try: - return loader.get_data(path) - except OSError: - raise ImportError('source not available through get_data()', - name=fullname) - - -class ImplicitNSInfo(object): - """Stores information returned from an implicit namespace spec""" - def __init__(self, name, paths): - self.name = name - self.paths = paths def cast_path(string): diff --git a/jedi/inference/compiled/subprocess/functions.py b/jedi/inference/compiled/subprocess/functions.py index 5c1513f9..baa5c328 100644 --- a/jedi/inference/compiled/subprocess/functions.py +++ b/jedi/inference/compiled/subprocess/functions.py @@ -2,12 +2,16 @@ import sys import os import re import inspect +import importlib +import warnings +from zipimport import zipimporter from importlib.machinery import all_suffixes -from jedi._compatibility import find_module, cast_path +from jedi._compatibility import cast_path from jedi.inference.compiled import access from jedi import debug from jedi import parser_utils +from jedi.file_io import KnownContentFileIO, ZipFileIO def get_sys_path(): @@ -34,7 +38,7 @@ def get_module_info(inference_state, sys_path=None, full_name=None, **kwargs): if sys_path is not None: sys.path, temp = sys_path, sys.path try: - return find_module(full_name=full_name, **kwargs) + return _find_module(full_name=full_name, **kwargs) except ImportError: return None, None finally: @@ -112,3 +116,123 @@ def _iter_module_names(inference_state, paths): if modname and '.' not in modname: if modname != '__init__': yield modname + + +def _find_module(string, path=None, full_name=None, is_global_search=True): + """ + Provides information about a module. + + This function isolates the differences in importing libraries introduced with + python 3.3 on; it gets a module name and optionally a path. It will return a + tuple containin an open file for the module (if not builtin), the filename + or the name of the module if it is a builtin one and a boolean indicating + if the module is contained in a package. + """ + spec = None + loader = None + + for finder in sys.meta_path: + if is_global_search and finder != importlib.machinery.PathFinder: + p = None + else: + p = path + try: + find_spec = finder.find_spec + except AttributeError: + # These are old-school clases that still have a different API, just + # ignore those. + continue + + spec = find_spec(string, p) + if spec is not None: + loader = spec.loader + 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) + 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): + loader = loader or importlib.machinery.PathFinder.find_module(string, path) + + if loader is None and path is None: # Fallback to find builtins + try: + with warnings.catch_warnings(record=True): + # Mute "DeprecationWarning: Use importlib.util.find_spec() + # instead." While we should replace that in the future, it's + # probably good to wait until we deprecate Python 3.3, since + # it was added in Python 3.4 and find_loader hasn't been + # removed in 3.6. + loader = importlib.find_loader(string) + except ValueError as e: + # See #491. Importlib might raise a ValueError, to avoid this, we + # just raise an ImportError to fix the issue. + raise ImportError("Originally " + repr(e)) + + if loader is None: + raise ImportError("Couldn't find a loader for {}".format(string)) + + return _from_loader(loader, string) + + +def _from_loader(loader, string): + try: + is_package_method = loader.is_package + except AttributeError: + is_package = False + else: + is_package = is_package_method(string) + try: + get_filename = loader.get_filename + except AttributeError: + return None, is_package + else: + module_path = cast_path(get_filename(string)) + + # To avoid unicode and read bytes, "overwrite" loader.get_source if + # possible. + try: + f = type(loader).get_source + except AttributeError: + raise ImportError("get_source was not defined on loader") + + if f is not importlib.machinery.SourceFileLoader.get_source: + # Unfortunately we are reading unicode here, not bytes. + # It seems hard to get bytes, because the zip importer + # logic just unpacks the zip file and returns a file descriptor + # that we cannot as easily access. Therefore we just read it as + # a string in the cases where get_source was overwritten. + code = loader.get_source(string) + else: + code = _get_source(loader, string) + + if code is None: + return None, is_package + if isinstance(loader, zipimporter): + return ZipFileIO(module_path, code, cast_path(loader.archive)), is_package + + return KnownContentFileIO(module_path, code), is_package + + +def _get_source(loader, fullname): + """ + This method is here as a replacement for SourceLoader.get_source. That + method returns unicode, but we prefer bytes. + """ + path = loader.get_filename(fullname) + try: + return loader.get_data(path) + except OSError: + raise ImportError('source not available through get_data()', + name=fullname) + + +class ImplicitNSInfo(object): + """Stores information returned from an implicit namespace spec""" + def __init__(self, name, paths): + self.name = name + self.paths = paths diff --git a/jedi/inference/imports.py b/jedi/inference/imports.py index 4b57736b..4fcb4c1d 100644 --- a/jedi/inference/imports.py +++ b/jedi/inference/imports.py @@ -13,7 +13,6 @@ import os from parso.python import tree from parso.tree import search_ancestor -from jedi._compatibility import ImplicitNSInfo from jedi import debug from jedi import settings from jedi.file_io import FolderIO @@ -28,6 +27,7 @@ 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, \ create_stub_module, parse_stub_module +from jedi.inference.compiled.subprocess.functions import ImplicitNSInfo from jedi.plugins import plugin_manager diff --git a/test/test_inference/test_imports.py b/test/test_inference/test_imports.py index f5a24e15..48140d1d 100644 --- a/test/test_inference/test_imports.py +++ b/test/test_inference/test_imports.py @@ -8,32 +8,32 @@ import os import pytest from jedi.file_io import FileIO -from jedi._compatibility import find_module_py33, find_module from jedi.inference import compiled from jedi.inference import imports from jedi.api.project import Project from jedi.inference.gradual.conversion import _stub_to_python_value_set from jedi.inference.references import get_module_contexts_containing_name from ..helpers import get_example_dir, test_dir, test_dir_project, root_dir +from jedi.inference.compiled.subprocess.functions import _find_module_py33, _find_module THIS_DIR = os.path.dirname(__file__) def test_find_module_basic(): """Needs to work like the old find_module.""" - assert find_module_py33('_io') == (None, False) + assert _find_module_py33('_io') == (None, False) with pytest.raises(ImportError): - assert find_module_py33('_DOESNTEXIST_') == (None, None) + assert _find_module_py33('_DOESNTEXIST_') == (None, None) def test_find_module_package(): - file_io, is_package = find_module('json') + file_io, is_package = _find_module('json') assert file_io.path.endswith(os.path.join('json', '__init__.py')) assert is_package is True def test_find_module_not_package(): - file_io, is_package = find_module('io') + file_io, is_package = _find_module('io') assert file_io.path.endswith('io.py') assert is_package is False