""" modulegraph.find_modules - High-level module dependency finding interface ========================================================================= History ........ Originally (loosely) based on code in py2exe's build_exe.py by Thomas Heller. """ import sys import os import imp import warnings import pkgutil from . import modulegraph from .modulegraph import Alias, Script, Extension 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 for _, module_name, is_package in pkgutil.iter_modules(xml.etree.__path__): if not is_package: result["_elementtree"].append("xml.etree.%s" % (module_name,)) 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.iter_graph(): # 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'): # XXX - this doesn't look nearly complete 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', 'win32ctypes', '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.add_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.find_node(f) if m is not None: path = m.packagepath[0] else: # Find path of package # TODO: use imp_find_module_or_importer 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. # FIXME: # 1) Needs to be adjusted for namespace packages in python 3.3 # 2) Code is fairly dodgy and needs better tests 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, '.') 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