|  | #!/usr/bin/env python | 
|  | # | 
|  | # Determine dependencies of python scripts or available python modules in a search path. | 
|  | # | 
|  | # Given the -d argument and a filename/filenames, returns the modules imported by those files. | 
|  | # Given the -d argument and a directory/directories, recurses to find all | 
|  | # python packages and modules, returns the modules imported by these. | 
|  | # Given the -p argument and a path or paths, scans that path for available python modules/packages. | 
|  |  | 
|  | import argparse | 
|  | import ast | 
|  | import imp | 
|  | import logging | 
|  | import os.path | 
|  | import sys | 
|  |  | 
|  |  | 
|  | logger = logging.getLogger('pythondeps') | 
|  |  | 
|  | suffixes = [] | 
|  | for triple in imp.get_suffixes(): | 
|  | suffixes.append(triple[0]) | 
|  |  | 
|  |  | 
|  | class PythonDepError(Exception): | 
|  | pass | 
|  |  | 
|  |  | 
|  | class DependError(PythonDepError): | 
|  | def __init__(self, path, error): | 
|  | self.path = path | 
|  | self.error = error | 
|  | PythonDepError.__init__(self, error) | 
|  |  | 
|  | def __str__(self): | 
|  | return "Failure determining dependencies of {}: {}".format(self.path, self.error) | 
|  |  | 
|  |  | 
|  | class ImportVisitor(ast.NodeVisitor): | 
|  | def __init__(self): | 
|  | self.imports = set() | 
|  | self.importsfrom = [] | 
|  |  | 
|  | def visit_Import(self, node): | 
|  | for alias in node.names: | 
|  | self.imports.add(alias.name) | 
|  |  | 
|  | def visit_ImportFrom(self, node): | 
|  | self.importsfrom.append((node.module, [a.name for a in node.names], node.level)) | 
|  |  | 
|  |  | 
|  | def walk_up(path): | 
|  | while path: | 
|  | yield path | 
|  | path, _, _ = path.rpartition(os.sep) | 
|  |  | 
|  |  | 
|  | def get_provides(path): | 
|  | path = os.path.realpath(path) | 
|  |  | 
|  | def get_fn_name(fn): | 
|  | for suffix in suffixes: | 
|  | if fn.endswith(suffix): | 
|  | return fn[:-len(suffix)] | 
|  |  | 
|  | isdir = os.path.isdir(path) | 
|  | if isdir: | 
|  | pkg_path = path | 
|  | walk_path = path | 
|  | else: | 
|  | pkg_path = get_fn_name(path) | 
|  | if pkg_path is None: | 
|  | return | 
|  | walk_path = os.path.dirname(path) | 
|  |  | 
|  | for curpath in walk_up(walk_path): | 
|  | if not os.path.exists(os.path.join(curpath, '__init__.py')): | 
|  | libdir = curpath | 
|  | break | 
|  | else: | 
|  | libdir = '' | 
|  |  | 
|  | package_relpath = pkg_path[len(libdir)+1:] | 
|  | package = '.'.join(package_relpath.split(os.sep)) | 
|  | if not isdir: | 
|  | yield package, path | 
|  | else: | 
|  | if os.path.exists(os.path.join(path, '__init__.py')): | 
|  | yield package, path | 
|  |  | 
|  | for dirpath, dirnames, filenames in os.walk(path): | 
|  | relpath = dirpath[len(path)+1:] | 
|  | if relpath: | 
|  | if '__init__.py' not in filenames: | 
|  | dirnames[:] = [] | 
|  | continue | 
|  | else: | 
|  | context = '.'.join(relpath.split(os.sep)) | 
|  | if package: | 
|  | context = package + '.' + context | 
|  | yield context, dirpath | 
|  | else: | 
|  | context = package | 
|  |  | 
|  | for fn in filenames: | 
|  | adjusted_fn = get_fn_name(fn) | 
|  | if not adjusted_fn or adjusted_fn == '__init__': | 
|  | continue | 
|  |  | 
|  | fullfn = os.path.join(dirpath, fn) | 
|  | if context: | 
|  | yield context + '.' + adjusted_fn, fullfn | 
|  | else: | 
|  | yield adjusted_fn, fullfn | 
|  |  | 
|  |  | 
|  | def get_code_depends(code_string, path=None, provide=None, ispkg=False): | 
|  | try: | 
|  | code = ast.parse(code_string, path) | 
|  | except TypeError as exc: | 
|  | raise DependError(path, exc) | 
|  | except SyntaxError as exc: | 
|  | raise DependError(path, exc) | 
|  |  | 
|  | visitor = ImportVisitor() | 
|  | visitor.visit(code) | 
|  | for builtin_module in sys.builtin_module_names: | 
|  | if builtin_module in visitor.imports: | 
|  | visitor.imports.remove(builtin_module) | 
|  |  | 
|  | if provide: | 
|  | provide_elements = provide.split('.') | 
|  | if ispkg: | 
|  | provide_elements.append("__self__") | 
|  | context = '.'.join(provide_elements[:-1]) | 
|  | package_path = os.path.dirname(path) | 
|  | else: | 
|  | context = None | 
|  | package_path = None | 
|  |  | 
|  | levelzero_importsfrom = (module for module, names, level in visitor.importsfrom | 
|  | if level == 0) | 
|  | for module in visitor.imports | set(levelzero_importsfrom): | 
|  | if context and path: | 
|  | module_basepath = os.path.join(package_path, module.replace('.', '/')) | 
|  | if os.path.exists(module_basepath): | 
|  | # Implicit relative import | 
|  | yield context + '.' + module, path | 
|  | continue | 
|  |  | 
|  | for suffix in suffixes: | 
|  | if os.path.exists(module_basepath + suffix): | 
|  | # Implicit relative import | 
|  | yield context + '.' + module, path | 
|  | break | 
|  | else: | 
|  | yield module, path | 
|  | else: | 
|  | yield module, path | 
|  |  | 
|  | for module, names, level in visitor.importsfrom: | 
|  | if level == 0: | 
|  | continue | 
|  | elif not provide: | 
|  | raise DependError("Error: ImportFrom non-zero level outside of a package: {0}".format((module, names, level)), path) | 
|  | elif level > len(provide_elements): | 
|  | raise DependError("Error: ImportFrom level exceeds package depth: {0}".format((module, names, level)), path) | 
|  | else: | 
|  | context = '.'.join(provide_elements[:-level]) | 
|  | if module: | 
|  | if context: | 
|  | yield context + '.' + module, path | 
|  | else: | 
|  | yield module, path | 
|  |  | 
|  |  | 
|  | def get_file_depends(path): | 
|  | try: | 
|  | code_string = open(path, 'r').read() | 
|  | except (OSError, IOError) as exc: | 
|  | raise DependError(path, exc) | 
|  |  | 
|  | return get_code_depends(code_string, path) | 
|  |  | 
|  |  | 
|  | def get_depends_recursive(directory): | 
|  | directory = os.path.realpath(directory) | 
|  |  | 
|  | provides = dict((v, k) for k, v in get_provides(directory)) | 
|  | for filename, provide in provides.iteritems(): | 
|  | if os.path.isdir(filename): | 
|  | filename = os.path.join(filename, '__init__.py') | 
|  | ispkg = True | 
|  | elif not filename.endswith('.py'): | 
|  | continue | 
|  | else: | 
|  | ispkg = False | 
|  |  | 
|  | with open(filename, 'r') as f: | 
|  | source = f.read() | 
|  |  | 
|  | depends = get_code_depends(source, filename, provide, ispkg) | 
|  | for depend, by in depends: | 
|  | yield depend, by | 
|  |  | 
|  |  | 
|  | def get_depends(path): | 
|  | if os.path.isdir(path): | 
|  | return get_depends_recursive(path) | 
|  | else: | 
|  | return get_file_depends(path) | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | logging.basicConfig() | 
|  |  | 
|  | parser = argparse.ArgumentParser(description='Determine dependencies and provided packages for python scripts/modules') | 
|  | parser.add_argument('path', nargs='+', help='full path to content to be processed') | 
|  | group = parser.add_mutually_exclusive_group() | 
|  | group.add_argument('-p', '--provides', action='store_true', | 
|  | help='given a path, display the provided python modules') | 
|  | group.add_argument('-d', '--depends', action='store_true', | 
|  | help='given a filename, display the imported python modules') | 
|  |  | 
|  | args = parser.parse_args() | 
|  | if args.provides: | 
|  | modules = set() | 
|  | for path in args.path: | 
|  | for provide, fn in get_provides(path): | 
|  | modules.add(provide) | 
|  |  | 
|  | for module in sorted(modules): | 
|  | print(module) | 
|  | elif args.depends: | 
|  | for path in args.path: | 
|  | try: | 
|  | modules = get_depends(path) | 
|  | except PythonDepError as exc: | 
|  | logger.error(str(exc)) | 
|  | sys.exit(1) | 
|  |  | 
|  | for module, imp_by in modules: | 
|  | print("{}\t{}".format(module, imp_by)) | 
|  | else: | 
|  | parser.print_help() | 
|  | sys.exit(2) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main() |