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

Remove dependency on imp. #857

Merged
merged 1 commit into from
Dec 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ Release Date: TBA

* Reduce memory usage of astroid's module cache.

* Remove dependency on `imp`.

Close #594
Close #681

What's New in astroid 2.4.3?
============================
Release Date: TBA
Expand Down
101 changes: 57 additions & 44 deletions astroid/interpreter/_import/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,11 @@
import collections
import distutils
import enum
import imp
import os
import sys
import zipimport

try:
import importlib.machinery

_HAS_MACHINERY = True
except ImportError:
_HAS_MACHINERY = False
import importlib.machinery

try:
from functools import lru_cache
Expand All @@ -37,22 +31,6 @@
"PY_CODERESOURCE PY_COMPILED PY_FROZEN PY_RESOURCE "
"PY_SOURCE PY_ZIPMODULE PY_NAMESPACE",
)
_ImpTypes = {
imp.C_BUILTIN: ModuleType.C_BUILTIN,
imp.C_EXTENSION: ModuleType.C_EXTENSION,
imp.PKG_DIRECTORY: ModuleType.PKG_DIRECTORY,
imp.PY_COMPILED: ModuleType.PY_COMPILED,
imp.PY_FROZEN: ModuleType.PY_FROZEN,
imp.PY_SOURCE: ModuleType.PY_SOURCE,
}
if hasattr(imp, "PY_RESOURCE"):
_ImpTypes[imp.PY_RESOURCE] = ModuleType.PY_RESOURCE
if hasattr(imp, "PY_CODERESOURCE"):
_ImpTypes[imp.PY_CODERESOURCE] = ModuleType.PY_CODERESOURCE


def _imp_type_to_module_type(imp_type):
return _ImpTypes[imp_type]


_ModuleSpec = collections.namedtuple(
Expand Down Expand Up @@ -114,26 +92,59 @@ def contribute_to_path(self, spec, processed):
"""Get a list of extra paths where this finder can search."""


class ImpFinder(Finder):
"""A finder based on the imp module."""
class ImportlibFinder(Finder):
"""A finder based on the importlib module."""

_SUFFIXES = (
[(s, ModuleType.C_EXTENSION) for s in importlib.machinery.EXTENSION_SUFFIXES]
+ [(s, ModuleType.PY_SOURCE) for s in importlib.machinery.SOURCE_SUFFIXES]
+ [(s, ModuleType.PY_COMPILED) for s in importlib.machinery.BYTECODE_SUFFIXES]
)

def find_module(self, modname, module_parts, processed, submodule_path):
if not isinstance(modname, str):
raise TypeError("'modname' must be a str, not {}".format(type(modname)))
if submodule_path is not None:
submodule_path = list(submodule_path)
try:
stream, mp_filename, mp_desc = imp.find_module(modname, submodule_path)
except ImportError:
return None

# Close resources.
if stream:
stream.close()

return ModuleSpec(
name=modname,
location=mp_filename,
module_type=_imp_type_to_module_type(mp_desc[2]),
)
else:
try:
spec = importlib.util.find_spec(modname)
if spec:
if spec.loader is importlib.machinery.BuiltinImporter:
return ModuleSpec(
name=modname,
location=None,
module_type=ModuleType.C_BUILTIN,
)
if spec.loader is importlib.machinery.FrozenImporter:
return ModuleSpec(
name=modname,
location=None,
module_type=ModuleType.PY_FROZEN,
)
except ValueError:
pass
submodule_path = sys.path

for entry in submodule_path:
package_directory = os.path.join(entry, modname)
for suffix in [".py", importlib.machinery.BYTECODE_SUFFIXES[0]]:
package_file_name = "__init__" + suffix
file_path = os.path.join(package_directory, package_file_name)
if os.path.isfile(file_path):
return ModuleSpec(
name=modname,
location=package_directory,
module_type=ModuleType.PKG_DIRECTORY,
)
for suffix, type_ in ImportlibFinder._SUFFIXES:
file_name = modname + suffix
file_path = os.path.join(entry, file_name)
if os.path.isfile(file_path):
return ModuleSpec(
name=modname, location=file_path, module_type=type_
)
return None

def contribute_to_path(self, spec, processed):
if spec.location is None:
Expand All @@ -159,7 +170,7 @@ def contribute_to_path(self, spec, processed):
return path


class ExplicitNamespacePackageFinder(ImpFinder):
class ExplicitNamespacePackageFinder(ImportlibFinder):
"""A finder for the explicit namespace packages, generated through pkg_resources."""

def find_module(self, modname, module_parts, processed, submodule_path):
Expand Down Expand Up @@ -229,10 +240,12 @@ def contribute_to_path(self, spec, processed):
return None


_SPEC_FINDERS = (ImpFinder, ZipFinder)
if _HAS_MACHINERY:
_SPEC_FINDERS += (PathSpecFinder,)
_SPEC_FINDERS += (ExplicitNamespacePackageFinder,)
_SPEC_FINDERS = (
ImportlibFinder,
ZipFinder,
PathSpecFinder,
ExplicitNamespacePackageFinder,
)


def _is_setuptools_namespace(location):
Expand Down
108 changes: 29 additions & 79 deletions astroid/modutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
:type BUILTIN_MODULES: dict
:var BUILTIN_MODULES: dictionary with builtin module names has key
"""
import imp
import importlib.util
import os
import platform
import sys
Expand All @@ -44,7 +44,6 @@
# distutils is replaced by virtualenv with a module that does
# weird path manipulations in order to get to the
# real distutils module.
from typing import Optional, List

from .interpreter._import import spec
from .interpreter._import import util
Expand Down Expand Up @@ -178,110 +177,53 @@ def _cache_normalize_path(path):
return result


def load_module_from_name(dotted_name, path=None, use_sys=True):
def load_module_from_name(dotted_name):
"""Load a Python module from its name.

:type dotted_name: str
:param dotted_name: python name of a module or package

:type path: list or None
:param path:
optional list of path where the module or package should be
searched (use sys.path if nothing or None is given)

:type use_sys: bool
:param use_sys:
boolean indicating whether the sys.modules dictionary should be
used or not


:raise ImportError: if the module or package is not found

:rtype: module
:return: the loaded module
"""
return load_module_from_modpath(dotted_name.split("."), path, use_sys)
try:
return sys.modules[dotted_name]
except KeyError:
pass

return importlib.import_module(dotted_name)

def load_module_from_modpath(parts, path: Optional[List[str]] = None, use_sys=1):

def load_module_from_modpath(parts):
"""Load a python module from its split name.

:type parts: list(str) or tuple(str)
:param parts:
python name of a module or package split on '.'

:param path:
Optional list of path where the module or package should be
searched (use sys.path if nothing or None is given)

:type use_sys: bool
:param use_sys:
boolean indicating whether the sys.modules dictionary should be used or not

:raise ImportError: if the module or package is not found

:rtype: module
:return: the loaded module
"""
if use_sys:
try:
return sys.modules[".".join(parts)]
except KeyError:
pass
modpath = []
prevmodule = None
for part in parts:
modpath.append(part)
curname = ".".join(modpath)
module = None
if len(modpath) != len(parts):
# even with use_sys=False, should try to get outer packages from sys.modules
module = sys.modules.get(curname)
elif use_sys:
# because it may have been indirectly loaded through a parent
module = sys.modules.get(curname)
if module is None:
mp_file, mp_filename, mp_desc = imp.find_module(part, path)
module = imp.load_module(curname, mp_file, mp_filename, mp_desc)
# mp_file still needs to be closed.
if mp_file:
mp_file.close()
if prevmodule:
setattr(prevmodule, part, module)
_file = getattr(module, "__file__", "")
prevmodule = module
if not _file and util.is_namespace(curname):
continue
if not _file and len(modpath) != len(parts):
raise ImportError("no module in %s" % ".".join(parts[len(modpath) :]))
path = [os.path.dirname(_file)]
return module
return load_module_from_name(".".join(parts))


def load_module_from_file(
filepath: str, path: Optional[List[str]] = None, use_sys=True
):
def load_module_from_file(filepath: str):
"""Load a Python module from it's path.

:type filepath: str
:param filepath: path to the python module or package

:param Optional[List[str]] path:
Optional list of path where the module or package should be
searched (use sys.path if nothing or None is given)

:type use_sys: bool
:param use_sys:
boolean indicating whether the sys.modules dictionary should be
used or not

:raise ImportError: if the module or package is not found

:rtype: module
:return: the loaded module
"""
modpath = modpath_from_file(filepath)
return load_module_from_modpath(modpath, path, use_sys)
return load_module_from_modpath(modpath)


def check_modpath_has_init(path, mod_path):
Expand Down Expand Up @@ -418,7 +360,9 @@ def file_info_from_modpath(modpath, path=None, context_file=None):
elif modpath == ["os", "path"]:
# FIXME: currently ignoring search_path...
return spec.ModuleSpec(
name="os.path", location=os.path.__file__, module_type=imp.PY_SOURCE
name="os.path",
location=os.path.__file__,
module_type=spec.ModuleType.PY_SOURCE,
)
return _spec_from_modpath(modpath, path, context)

Expand Down Expand Up @@ -614,16 +558,22 @@ def is_relative(modname, from_file):
from_file = os.path.dirname(from_file)
if from_file in sys.path:
return False
try:
stream, _, _ = imp.find_module(modname.split(".")[0], [from_file])

# Close the stream to avoid ResourceWarnings.
if stream:
stream.close()
return True
except ImportError:
name = os.path.basename(from_file)
file_path = os.path.dirname(from_file)
parent_spec = importlib.util.find_spec(name, from_file)
while parent_spec is None and len(file_path) > 0:
name = os.path.basename(file_path) + "." + name
file_path = os.path.dirname(file_path)
parent_spec = importlib.util.find_spec(name, from_file)

if parent_spec is None:
return False

submodule_spec = importlib.util.find_spec(
name + "." + modname.split(".")[0], parent_spec.submodule_search_locations
)
return submodule_spec is not None


# internal only functions #####################################################

Expand Down
1 change: 1 addition & 0 deletions astroid/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1448,6 +1448,7 @@ def extra_decorators(self):
decorators.append(assign.value)
return decorators

# pylint: disable=invalid-overridden-method
@decorators_mod.cachedproperty
def type(
self
Expand Down
4 changes: 1 addition & 3 deletions tests/testdata/python3/data/nonregr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ def toto(value):
print(v.get('yo'))


import imp
fp, mpath, desc = imp.find_module('optparse',a)
s_opt = imp.load_module('std_optparse', fp, mpath, desc)
import optparse as s_opt

class OptionParser(s_opt.OptionParser):

Expand Down
6 changes: 4 additions & 2 deletions tests/unittest_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,10 @@ def test_multiprocessing_manager(self):
obj = next(module[attr].infer())
self.assertEqual(obj.qname(), "{}.{}".format(bases.BUILTINS, attr))

array = next(module["array"].infer())
self.assertEqual(array.qname(), "array.array")
# pypy's implementation of array.__spec__ return None. This causes problems for this inference.
if not hasattr(sys, "pypy_version_info"):
array = next(module["array"].infer())
self.assertEqual(array.qname(), "array.array")

manager = next(module["manager"].infer())
# Verify that we have these attributes
Expand Down
12 changes: 0 additions & 12 deletions tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,18 +541,6 @@ class Warning(Warning):
self.assertEqual(ancestor.root().name, BUILTINS)
self.assertRaises(StopIteration, partial(next, ancestors))

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree these test is not specially clear, but i do not understand why do you delete it. Can you explain a little bit more?

def test_qqch(self):
code = """
from astroid.modutils import load_module_from_name
xxx = load_module_from_name('__pkginfo__')
"""
ast = parse(code, __name__)
xxx = ast["xxx"]
self.assertSetEqual(
{n.__class__ for n in xxx.inferred()},
{nodes.Const, util.Uninferable.__class__},
)

def test_method_argument(self):
code = '''
class ErudiEntitySchema:
Expand Down
Loading