summaryrefslogtreecommitdiff
path: root/astroid/modutils.py
diff options
context:
space:
mode:
Diffstat (limited to 'astroid/modutils.py')
-rw-r--r--astroid/modutils.py315
1 files changed, 209 insertions, 106 deletions
diff --git a/astroid/modutils.py b/astroid/modutils.py
index 465efcc9..57c625dc 100644
--- a/astroid/modutils.py
+++ b/astroid/modutils.py
@@ -27,6 +27,9 @@
:var BUILTIN_MODULES: dictionary with builtin module names has key
"""
+import abc
+import collections
+import enum
import imp
import os
import platform
@@ -39,13 +42,34 @@ from distutils.errors import DistutilsPlatformError
# weird path manipulations in order to get to the
# real distutils module.
import zipimport
+try:
+ import importlib.machinery
+ _HAS_MACHINERY = True
+except ImportError:
+ _HAS_MACHINERY = False
try:
import pkg_resources
except ImportError:
pkg_resources = None
-PY_ZIPMODULE = object()
+ModuleType = enum.Enum('ModuleType', 'C_BUILTIN C_EXTENSION PKG_DIRECTORY '
+ '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]
if sys.platform.startswith('win'):
PY_SOURCE_EXTS = ('py', 'pyw')
@@ -325,7 +349,7 @@ def modpath_from_file(filename, extrapath=None):
def file_from_modpath(modpath, path=None, context_file=None):
- return file_info_from_modpath(modpath, path, context_file)[0]
+ return file_info_from_modpath(modpath, path, context_file).location
def file_info_from_modpath(modpath, path=None, context_file=None):
"""given a mod path (i.e. splitted module / package name), return the
@@ -364,13 +388,13 @@ def file_info_from_modpath(modpath, path=None, context_file=None):
if modpath[0] == 'xml':
# handle _xmlplus
try:
- return _file_from_modpath(['_xmlplus'] + modpath[1:], path, context)
+ return _spec_from_modpath(['_xmlplus'] + modpath[1:], path, context)
except ImportError:
- return _file_from_modpath(modpath, path, context)
+ return _spec_from_modpath(modpath, path, context)
elif modpath == ['os', 'path']:
# FIXME: currently ignoring search_path...
- return os.path.__file__, imp.PY_SOURCE
- return _file_from_modpath(modpath, path, context)
+ return ModuleSpec(name='os.path', location=os.path.__file__, type=imp.PY_SOURCE)
+ return _spec_from_modpath(modpath, path, context)
def get_module_part(dotted_name, context_file=None):
@@ -573,54 +597,195 @@ def is_relative(modname, from_file):
# internal only functions #####################################################
-def _file_from_modpath(modpath, path=None, context=None):
+def _spec_from_modpath(modpath, path=None, context=None):
"""given a mod path (i.e. splitted module / package name), return the
- corresponding file
+ corresponding spec
this function is used internally, see `file_from_modpath`'s
documentation for more information
"""
assert len(modpath) > 0
+ location = None
if context is not None:
try:
- mtype, mp_filename = _module_file(modpath, [context])
+ spec = _find_spec(modpath, [context])
+ location = spec.location
except ImportError:
- mtype, mp_filename = _module_file(modpath, path)
+ spec = _find_spec(modpath, path)
+ location = spec.location
else:
- mtype, mp_filename = _module_file(modpath, path)
- if mtype == imp.PY_COMPILED:
+ spec = _find_spec(modpath, path)
+ if spec.type == ModuleType.PY_COMPILED:
try:
- return get_source_file(mp_filename), imp.PY_SOURCE
+ location = get_source_file(spec.location)
+ return spec._replace(location=location, type=ModuleSpec.PY_SOURCE)
except NoSourceFile:
- return mp_filename, imp.PY_COMPILED
- elif mtype == imp.C_BUILTIN:
+ return spec.replace(location=location)
+ elif spec.type == ModuleType.C_BUILTIN:
# integrated builtin module
- return None, imp.C_BUILTIN
- elif mtype == imp.PKG_DIRECTORY:
- mp_filename = _has_init(mp_filename)
- mtype = imp.PY_SOURCE
- return mp_filename, mtype
+ return spec._replace(location=None)
+ elif spec.type == ModuleType.PKG_DIRECTORY:
+ location = _has_init(spec.location)
+ return spec._replace(location=location, type=ModuleType.PY_SOURCE)
+ return spec
+
def _search_zip(modpath, pic):
for filepath, importer in list(pic.items()):
if importer is not None:
- if importer.find_module(modpath[0]):
+ found = importer.find_module(modpath[0])
+ if found:
if not importer.find_module(os.path.sep.join(modpath)):
raise ImportError('No module named %s in %s/%s' % (
'.'.join(modpath[1:]), filepath, modpath))
- return (PY_ZIPMODULE,
+ #import code; code.interact(local=locals())
+ return (ModuleType.PY_ZIPMODULE,
os.path.abspath(filepath) + os.path.sep + os.path.sep.join(modpath),
filepath)
raise ImportError('No module named %s' % '.'.join(modpath))
+def _precache_zipimporters(path=None):
+ pic = sys.path_importer_cache
+ path = path or sys.path
+ for entry_path in path:
+ if entry_path not in pic:
+ try:
+ pic[entry_path] = zipimport.zipimporter(entry_path)
+ except zipimport.ZipImportError:
+ pic[entry_path] = None
+ return pic
+
+
def _is_namespace(modname):
# pylint: disable=no-member; astroid issue #290, modifying globals at runtime.
return (pkg_resources is not None
and modname in pkg_resources._namespace_packages)
-def _module_file(modpath, path=None):
+def _is_setuptools_namespace(location):
+ try:
+ with open(os.path.join(location, '__init__.py'), 'rb') as stream:
+ data = stream.read(4096)
+ except IOError:
+ pass
+ else:
+ extend_path = b'pkgutil' in data and b'extend_path' in data
+ declare_namespace = (
+ b"pkg_resources" in data
+ and b"declare_namespace(__name__)" in data)
+ return extend_path or declare_namespace
+
+
+# Spec finders.
+
+_ModuleSpec = collections.namedtuple('_ModuleSpec', 'name type location '
+ 'origin submodule_search_locations')
+
+class ModuleSpec(_ModuleSpec):
+
+ def __new__(cls, name, type, location=None, origin=None,
+ submodule_search_locations=None):
+ return _ModuleSpec.__new__(cls, name=name, type=type,
+ location=location, origin=origin,
+ submodule_search_locations=submodule_search_locations)
+
+
+class Finder(object):
+
+ def __init__(self, path=None):
+ self._path = path or sys.path
+
+ @abc.abstractmethod
+ def find_module(self, modname, module_parts, processed, submodule_path):
+ pass
+
+ def contribute_to_path(self, filename, processed):
+ return None
+
+
+class ImpFinder(Finder):
+
+ def find_module(self, modname, _, processed, 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,
+ type=_imp_type_to_module_type(mp_desc[2]))
+
+ def contribute_to_path(self, spec, processed):
+ if spec.location is None:
+ # Builtin.
+ return None
+
+ if _is_setuptools_namespace(spec.location):
+ # extend_path is called, search sys.path for module/packages
+ # of this name see pkgutil.extend_path documentation
+ path = [os.path.join(p, *processed) for p in sys.path
+ if os.path.isdir(os.path.join(p, *processed))]
+ else:
+ path = [spec.location]
+ return path
+
+
+class ZipFinder(Finder):
+
+ def __init__(self, path):
+ super(ZipFinder, self).__init__(path)
+ self._zipimporters = _precache_zipimporters(path)
+
+ def find_module(self, modname, module_parts, processed, submodule_path):
+ try:
+ file_type, filename, path = _search_zip(module_parts, self._zipimporters)
+ except ImportError:
+ return None
+
+ return ModuleSpec(name=modname, location=filename,
+ origin='egg', type=file_type,
+ submodule_search_locations=path)
+
+
+class PEP420SpecFinder(Finder):
+
+ def find_module(self, modname, module_parts, processed, submodule_path):
+ spec = importlib.machinery.PathFinder.find_spec(modname, path=submodule_path)
+ if spec:
+ location = spec.origin if spec.origin != 'namespace' else None
+ type = ModuleType.PY_NAMESPACE if spec.origin == 'namespace' else None
+ spec = ModuleSpec(name=spec.name, location=location,
+ origin=spec.origin, type=type,
+ submodule_search_locations=list(spec.submodule_search_locations or []))
+ return spec
+
+ def contribute_to_path(self, spec, processed):
+ if spec.type == ModuleType.PY_NAMESPACE:
+ return spec.submodule_search_locations
+ return None
+
+
+def _find_spec_with_path(search_path, modname, module_parts, processed, submodule_path):
+ finders = [finder(search_path) for finder in (ImpFinder, ZipFinder)]
+ if _HAS_MACHINERY:
+ finders.append(PEP420SpecFinder(search_path))
+
+ for finder in finders:
+ spec = finder.find_module(modname, module_parts, processed, submodule_path)
+ if spec is None:
+ continue
+ return finder, spec
+
+ raise ImportError('No module %s in %s' % ('.'.join(module_parts),
+ '.'.join(processed)))
+
+
+
+def _find_spec(modpath, path=None):
"""get a module type / file path
:type modpath: list or tuple
@@ -637,91 +802,29 @@ def _module_file(modpath, path=None):
:rtype: tuple(int, str)
:return: the module type flag and the file path for a module
"""
- # egg support compat
- try:
- pic = sys.path_importer_cache
- _path = (path is None and sys.path or path)
- for __path in _path:
- if __path not in pic:
- try:
- pic[__path] = zipimport.zipimporter(__path)
- except zipimport.ZipImportError:
- pic[__path] = None
- checkeggs = True
- except AttributeError:
- checkeggs = False
- # pkg_resources support (aka setuptools namespace packages)
- if _is_namespace(modpath[0]) and modpath[0] in sys.modules:
- # setuptools has added into sys.modules a module object with proper
- # __path__, get back information from there
- module = sys.modules[modpath.pop(0)]
- path = module.__path__
- if not modpath:
- return imp.C_BUILTIN, None
- imported = []
+ _path = path or sys.path
+
+ # Need a copy for not mutating the argument.
+ modpath = modpath[:]
+
+ submodule_path = []
+ module_parts = modpath[:]
+ processed = []
+
while modpath:
- modname = modpath[0]
- # take care to changes in find_module implementation wrt builtin modules
- #
- # Python 2.6.6 (r266:84292, Sep 11 2012, 08:34:23)
- # >>> imp.find_module('posix')
- # (None, 'posix', ('', '', 6))
- #
- # Python 3.3.1 (default, Apr 26 2013, 12:08:46)
- # >>> imp.find_module('posix')
- # (None, None, ('', '', 6))
- try:
- stream, mp_filename, mp_desc = imp.find_module(modname, path)
- except ImportError:
- if checkeggs:
- return _search_zip(modpath, pic)[:2]
- raise
- else:
- # Don't forget to close the stream to avoid
- # spurious ResourceWarnings.
- if stream:
- stream.close()
-
- if checkeggs and mp_filename:
- fullabspath = [_cache_normalize_path(x) for x in _path]
- try:
- pathindex = fullabspath.index(os.path.dirname(_normalize_path(mp_filename)))
- emtype, emp_filename, zippath = _search_zip(modpath, pic)
- if pathindex > _path.index(zippath):
- # an egg takes priority
- return emtype, emp_filename
- except ValueError:
- # XXX not in _path
- pass
- except ImportError:
- pass
- checkeggs = False
- imported.append(modpath.pop(0))
- mtype = mp_desc[2]
+ modname = modpath.pop(0)
+ finder, spec = _find_spec_with_path(_path, modname,
+ module_parts, processed,
+ submodule_path or path)
+ processed.append(modname)
if modpath:
- if mtype != imp.PKG_DIRECTORY:
- raise ImportError('No module %s in %s' % ('.'.join(modpath),
- '.'.join(imported)))
- # XXX guess if package is using pkgutil.extend_path by looking for
- # those keywords in the first four Kbytes
- try:
- with open(os.path.join(mp_filename, '__init__.py'), 'rb') as stream:
- data = stream.read(4096)
- except IOError:
- path = [mp_filename]
- else:
- extend_path = b'pkgutil' in data and b'extend_path' in data
- declare_namespace = (
- b"pkg_resources" in data
- and b"declare_namespace(__name__)" in data)
- if extend_path or declare_namespace:
- # extend_path is called, search sys.path for module/packages
- # of this name see pkgutil.extend_path documentation
- path = [os.path.join(p, *imported) for p in sys.path
- if os.path.isdir(os.path.join(p, *imported))]
- else:
- path = [mp_filename]
- return mtype, mp_filename
+ submodule_path = finder.contribute_to_path(spec, processed)
+
+ if spec.type == ModuleType.PKG_DIRECTORY:
+ spec = spec._replace(submodule_search_locations=submodule_path)
+
+ return spec
+
def _is_python_file(filename):
"""return true if the given filename should be considered as a python file