251 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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()
 | 
