blob: 870d9f40a005f7e4a34190df84c3f7140db0007a [file] [log] [blame]
"""
modulegraph.find_modules - High-level module dependency finding interface
=========================================================================
History
........
Originally (loosely) based on code in py2exe's build_exe.py by Thomas Heller.
"""
from __future__ import absolute_import
import imp
import os
import sys
import warnings
from . import modulegraph
from .modulegraph import Alias, Extension, Script
from .util import imp_find_module
__all__ = ["find_modules", "parse_mf_results"]
_PLATFORM_MODULES = {"posix", "nt", "os2", "mac", "ce", "riscos"}
def get_implies():
result = {
# imports done from builtin modules in C code
# (untrackable by modulegraph)
"_curses": ["curses"],
"posix": ["resource"],
"gc": ["time"],
"time": ["_strptime"],
"datetime": ["time"],
"MacOS": ["macresource"],
"cPickle": ["copy_reg", "cStringIO"],
"parser": ["copy_reg"],
"codecs": ["encodings"],
"cStringIO": ["copy_reg"],
"_sre": ["copy", "string", "sre"],
"zipimport": ["zlib"],
# Python 3.2:
"_datetime": ["time", "_strptime"],
"_json": ["json.decoder"],
"_pickle": ["codecs", "copyreg", "_compat_pickle"],
"_posixsubprocess": ["gc"],
"_ssl": ["socket"],
# Python 3.3:
"_elementtree": ["copy", "xml.etree.ElementPath"],
# mactoolboxglue can do a bunch more of these
# that are far harder to predict, these should be tracked
# manually for now.
# this isn't C, but it uses __import__
"anydbm": ["dbhash", "gdbm", "dbm", "dumbdbm", "whichdb"],
# package aliases
"wxPython.wx": Alias("wx"),
}
if sys.version_info[0] == 3:
result["_sre"] = ["copy", "re"]
result["parser"] = ["copyreg"]
# _frozen_importlib is part of the interpreter itself
result["_frozen_importlib"] = None
if sys.version_info[0] == 2 and sys.version_info[1] >= 5:
result.update(
{
"email.base64MIME": Alias("email.base64mime"),
"email.Charset": Alias("email.charset"),
"email.Encoders": Alias("email.encoders"),
"email.Errors": Alias("email.errors"),
"email.Feedparser": Alias("email.feedParser"),
"email.Generator": Alias("email.generator"),
"email.Header": Alias("email.header"),
"email.Iterators": Alias("email.iterators"),
"email.Message": Alias("email.message"),
"email.Parser": Alias("email.parser"),
"email.quopriMIME": Alias("email.quoprimime"),
"email.Utils": Alias("email.utils"),
"email.MIMEAudio": Alias("email.mime.audio"),
"email.MIMEBase": Alias("email.mime.base"),
"email.MIMEImage": Alias("email.mime.image"),
"email.MIMEMessage": Alias("email.mime.message"),
"email.MIMEMultipart": Alias("email.mime.multipart"),
"email.MIMENonMultipart": Alias("email.mime.nonmultipart"),
"email.MIMEText": Alias("email.mime.text"),
}
)
if sys.version_info[:2] >= (2, 5):
result["_elementtree"] = ["pyexpat"]
import xml.etree
files = os.listdir(xml.etree.__path__[0])
for fn in files:
if fn.endswith(".py") and fn != "__init__.py":
result["_elementtree"].append("xml.etree.%s" % (fn[:-3],))
if sys.version_info[:2] >= (2, 6):
result["future_builtins"] = ["itertools"]
# os.path is an alias for a platform specific submodule,
# ensure that the graph shows this.
result["os.path"] = Alias(os.path.__name__)
return result
def parse_mf_results(mf):
"""
Return two lists: the first one contains the python files in the graph,
the second the C extensions.
:param mf: a :class:`modulegraph.modulegraph.ModuleGraph` instance
"""
# Retrieve modules from modulegraph
py_files = []
extensions = []
for item in mf.flatten():
# There may be __main__ modules (from mf.run_script), but
# we don't need it in the zipfile we build.
if item.identifier == "__main__":
continue
src = item.filename
if src and src != "-":
if isinstance(item, Script):
# Scripts are python files
py_files.append(item)
elif isinstance(item, Extension):
extensions.append(item)
else:
py_files.append(item)
# sort on the file names, the output is nicer to read
py_files.sort(key=lambda v: v.filename)
extensions.sort(key=lambda v: v.filename)
return py_files, extensions
def plat_prepare(includes, packages, excludes):
# used by Python itself
includes.update(["warnings", "unicodedata", "weakref"])
if not sys.platform.startswith("irix"):
excludes.update(["AL", "sgi", "vms_lib"])
if sys.platform not in ("mac", "darwin"):
excludes.update(
[
"Audio_mac",
"Carbon.File",
"Carbon.Folder",
"Carbon.Folders",
"EasyDialogs",
"MacOS",
"macfs",
"macostools",
"_scproxy",
]
)
if not sys.platform == "win32":
# only win32
excludes.update(
[
"nturl2path",
"win32api",
"win32con",
"win32event",
"win32evtlogutil",
"win32evtlog",
"win32file",
"win32gui",
"win32pipe",
"win32process",
"win32security",
"pywintypes",
"winsound",
"win32",
"_winreg",
"_winapi",
"msvcrt",
"winreg",
"_subprocess",
]
)
if not sys.platform == "riscos":
excludes.update(["riscosenviron", "rourl2path"])
if not sys.platform == "dos" or sys.platform.startswith("ms-dos"):
excludes.update(["dos"])
if not sys.platform == "os2emx":
excludes.update(["_emx_link"])
excludes.update(_PLATFORM_MODULES - set(sys.builtin_module_names))
# Carbon.Res depends on this, but the module hasn't been present
# for a while...
excludes.add("OverrideFrom23")
excludes.add("OverrideFrom23._Res")
# import trickery in the dummy_threading module (stdlib)
excludes.add("_dummy_threading")
try:
imp_find_module("poll")
except ImportError:
excludes.update(["poll"])
def find_needed_modules(
mf=None, scripts=(), includes=(), packages=(), warn=warnings.warn
):
if mf is None:
mf = modulegraph.ModuleGraph()
# feed Modulefinder with everything, and return it.
for path in scripts:
mf.run_script(path)
for mod in includes:
try:
if mod[-2:] == ".*":
mf.import_hook(mod[:-2], None, ["*"])
else:
mf.import_hook(mod)
except ImportError:
warn("No module named %s" % (mod,))
for f in packages:
# If modulegraph has seen a reference to the package, then
# we prefer to believe that (imp_find_module doesn't seem to locate
# sub-packages)
m = mf.findNode(f)
if m is not None and m.packagepath is not None:
path = m.packagepath[0]
else:
# Find path of package
try:
path = imp_find_module(f, mf.path)[1]
except ImportError:
warn("No package named %s" % f)
continue
# walk the path to find subdirs containing __init__.py files
# scan the results (directory of __init__.py files)
# first trim the path (of the head package),
# then convert directory name in package name,
# finally push into modulegraph.
for (dirpath, dirnames, filenames) in os.walk(path):
if "__init__.py" in filenames and dirpath.startswith(path):
package = (
f
+ "."
+ dirpath[len(path) + 1 :].replace(os.sep, ".") # noqa: E203
)
if package.endswith("."):
package = package[:-1]
m = mf.import_hook(package, None, ["*"])
else:
# Exclude subtrees that aren't packages
dirnames[:] = []
return mf
#
# resource constants
#
PY_SUFFIXES = [".py", ".pyw", ".pyo", ".pyc"]
C_SUFFIXES = [
_triple[0] for _triple in imp.get_suffixes() if _triple[2] == imp.C_EXTENSION
]
#
# side-effects
#
def _replacePackages():
REPLACEPACKAGES = {
"_xmlplus": "xml",
}
for k, v in REPLACEPACKAGES.items():
modulegraph.replacePackage(k, v)
_replacePackages()
def find_modules(scripts=(), includes=(), packages=(), excludes=(), path=None, debug=0):
"""
High-level interface, takes iterables for:
scripts, includes, packages, excludes
And returns a :class:`modulegraph.modulegraph.ModuleGraph` instance,
python_files, and extensions
python_files is a list of pure python dependencies as modulegraph.Module
objects, extensions is a list of platform-specific C extension dependencies
as modulegraph.Module objects
"""
scripts = set(scripts)
includes = set(includes)
packages = set(packages)
excludes = set(excludes)
plat_prepare(includes, packages, excludes)
mf = modulegraph.ModuleGraph(
path=path,
excludes=(excludes - includes),
implies=get_implies(),
debug=debug,
)
find_needed_modules(mf, scripts, includes, packages)
return mf