1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun# Determine dependencies of python scripts or available python modules in a search path. 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun# Given the -d argument and a filename/filenames, returns the modules imported by those files. 8*4882a593Smuzhiyun# Given the -d argument and a directory/directories, recurses to find all 9*4882a593Smuzhiyun# python packages and modules, returns the modules imported by these. 10*4882a593Smuzhiyun# Given the -p argument and a path or paths, scans that path for available python modules/packages. 11*4882a593Smuzhiyun 12*4882a593Smuzhiyunimport argparse 13*4882a593Smuzhiyunimport ast 14*4882a593Smuzhiyunimport importlib 15*4882a593Smuzhiyunfrom importlib import machinery 16*4882a593Smuzhiyunimport logging 17*4882a593Smuzhiyunimport os.path 18*4882a593Smuzhiyunimport sys 19*4882a593Smuzhiyun 20*4882a593Smuzhiyun 21*4882a593Smuzhiyunlogger = logging.getLogger('pythondeps') 22*4882a593Smuzhiyun 23*4882a593Smuzhiyunsuffixes = importlib.machinery.all_suffixes() 24*4882a593Smuzhiyun 25*4882a593Smuzhiyunclass PythonDepError(Exception): 26*4882a593Smuzhiyun pass 27*4882a593Smuzhiyun 28*4882a593Smuzhiyun 29*4882a593Smuzhiyunclass DependError(PythonDepError): 30*4882a593Smuzhiyun def __init__(self, path, error): 31*4882a593Smuzhiyun self.path = path 32*4882a593Smuzhiyun self.error = error 33*4882a593Smuzhiyun PythonDepError.__init__(self, error) 34*4882a593Smuzhiyun 35*4882a593Smuzhiyun def __str__(self): 36*4882a593Smuzhiyun return "Failure determining dependencies of {}: {}".format(self.path, self.error) 37*4882a593Smuzhiyun 38*4882a593Smuzhiyun 39*4882a593Smuzhiyunclass ImportVisitor(ast.NodeVisitor): 40*4882a593Smuzhiyun def __init__(self): 41*4882a593Smuzhiyun self.imports = set() 42*4882a593Smuzhiyun self.importsfrom = [] 43*4882a593Smuzhiyun 44*4882a593Smuzhiyun def visit_Import(self, node): 45*4882a593Smuzhiyun for alias in node.names: 46*4882a593Smuzhiyun self.imports.add(alias.name) 47*4882a593Smuzhiyun 48*4882a593Smuzhiyun def visit_ImportFrom(self, node): 49*4882a593Smuzhiyun self.importsfrom.append((node.module, [a.name for a in node.names], node.level)) 50*4882a593Smuzhiyun 51*4882a593Smuzhiyun 52*4882a593Smuzhiyundef walk_up(path): 53*4882a593Smuzhiyun while path: 54*4882a593Smuzhiyun yield path 55*4882a593Smuzhiyun path, _, _ = path.rpartition(os.sep) 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun 58*4882a593Smuzhiyundef get_provides(path): 59*4882a593Smuzhiyun path = os.path.realpath(path) 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun def get_fn_name(fn): 62*4882a593Smuzhiyun for suffix in suffixes: 63*4882a593Smuzhiyun if fn.endswith(suffix): 64*4882a593Smuzhiyun return fn[:-len(suffix)] 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun isdir = os.path.isdir(path) 67*4882a593Smuzhiyun if isdir: 68*4882a593Smuzhiyun pkg_path = path 69*4882a593Smuzhiyun walk_path = path 70*4882a593Smuzhiyun else: 71*4882a593Smuzhiyun pkg_path = get_fn_name(path) 72*4882a593Smuzhiyun if pkg_path is None: 73*4882a593Smuzhiyun return 74*4882a593Smuzhiyun walk_path = os.path.dirname(path) 75*4882a593Smuzhiyun 76*4882a593Smuzhiyun for curpath in walk_up(walk_path): 77*4882a593Smuzhiyun if not os.path.exists(os.path.join(curpath, '__init__.py')): 78*4882a593Smuzhiyun libdir = curpath 79*4882a593Smuzhiyun break 80*4882a593Smuzhiyun else: 81*4882a593Smuzhiyun libdir = '' 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun package_relpath = pkg_path[len(libdir)+1:] 84*4882a593Smuzhiyun package = '.'.join(package_relpath.split(os.sep)) 85*4882a593Smuzhiyun if not isdir: 86*4882a593Smuzhiyun yield package, path 87*4882a593Smuzhiyun else: 88*4882a593Smuzhiyun if os.path.exists(os.path.join(path, '__init__.py')): 89*4882a593Smuzhiyun yield package, path 90*4882a593Smuzhiyun 91*4882a593Smuzhiyun for dirpath, dirnames, filenames in os.walk(path): 92*4882a593Smuzhiyun relpath = dirpath[len(path)+1:] 93*4882a593Smuzhiyun if relpath: 94*4882a593Smuzhiyun if '__init__.py' not in filenames: 95*4882a593Smuzhiyun dirnames[:] = [] 96*4882a593Smuzhiyun continue 97*4882a593Smuzhiyun else: 98*4882a593Smuzhiyun context = '.'.join(relpath.split(os.sep)) 99*4882a593Smuzhiyun if package: 100*4882a593Smuzhiyun context = package + '.' + context 101*4882a593Smuzhiyun yield context, dirpath 102*4882a593Smuzhiyun else: 103*4882a593Smuzhiyun context = package 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun for fn in filenames: 106*4882a593Smuzhiyun adjusted_fn = get_fn_name(fn) 107*4882a593Smuzhiyun if not adjusted_fn or adjusted_fn == '__init__': 108*4882a593Smuzhiyun continue 109*4882a593Smuzhiyun 110*4882a593Smuzhiyun fullfn = os.path.join(dirpath, fn) 111*4882a593Smuzhiyun if context: 112*4882a593Smuzhiyun yield context + '.' + adjusted_fn, fullfn 113*4882a593Smuzhiyun else: 114*4882a593Smuzhiyun yield adjusted_fn, fullfn 115*4882a593Smuzhiyun 116*4882a593Smuzhiyun 117*4882a593Smuzhiyundef get_code_depends(code_string, path=None, provide=None, ispkg=False): 118*4882a593Smuzhiyun try: 119*4882a593Smuzhiyun code = ast.parse(code_string, path) 120*4882a593Smuzhiyun except TypeError as exc: 121*4882a593Smuzhiyun raise DependError(path, exc) 122*4882a593Smuzhiyun except SyntaxError as exc: 123*4882a593Smuzhiyun raise DependError(path, exc) 124*4882a593Smuzhiyun 125*4882a593Smuzhiyun visitor = ImportVisitor() 126*4882a593Smuzhiyun visitor.visit(code) 127*4882a593Smuzhiyun for builtin_module in sys.builtin_module_names: 128*4882a593Smuzhiyun if builtin_module in visitor.imports: 129*4882a593Smuzhiyun visitor.imports.remove(builtin_module) 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun if provide: 132*4882a593Smuzhiyun provide_elements = provide.split('.') 133*4882a593Smuzhiyun if ispkg: 134*4882a593Smuzhiyun provide_elements.append("__self__") 135*4882a593Smuzhiyun context = '.'.join(provide_elements[:-1]) 136*4882a593Smuzhiyun package_path = os.path.dirname(path) 137*4882a593Smuzhiyun else: 138*4882a593Smuzhiyun context = None 139*4882a593Smuzhiyun package_path = None 140*4882a593Smuzhiyun 141*4882a593Smuzhiyun levelzero_importsfrom = (module for module, names, level in visitor.importsfrom 142*4882a593Smuzhiyun if level == 0) 143*4882a593Smuzhiyun for module in visitor.imports | set(levelzero_importsfrom): 144*4882a593Smuzhiyun if context and path: 145*4882a593Smuzhiyun module_basepath = os.path.join(package_path, module.replace('.', '/')) 146*4882a593Smuzhiyun if os.path.exists(module_basepath): 147*4882a593Smuzhiyun # Implicit relative import 148*4882a593Smuzhiyun yield context + '.' + module, path 149*4882a593Smuzhiyun continue 150*4882a593Smuzhiyun 151*4882a593Smuzhiyun for suffix in suffixes: 152*4882a593Smuzhiyun if os.path.exists(module_basepath + suffix): 153*4882a593Smuzhiyun # Implicit relative import 154*4882a593Smuzhiyun yield context + '.' + module, path 155*4882a593Smuzhiyun break 156*4882a593Smuzhiyun else: 157*4882a593Smuzhiyun yield module, path 158*4882a593Smuzhiyun else: 159*4882a593Smuzhiyun yield module, path 160*4882a593Smuzhiyun 161*4882a593Smuzhiyun for module, names, level in visitor.importsfrom: 162*4882a593Smuzhiyun if level == 0: 163*4882a593Smuzhiyun continue 164*4882a593Smuzhiyun elif not provide: 165*4882a593Smuzhiyun raise DependError("Error: ImportFrom non-zero level outside of a package: {0}".format((module, names, level)), path) 166*4882a593Smuzhiyun elif level > len(provide_elements): 167*4882a593Smuzhiyun raise DependError("Error: ImportFrom level exceeds package depth: {0}".format((module, names, level)), path) 168*4882a593Smuzhiyun else: 169*4882a593Smuzhiyun context = '.'.join(provide_elements[:-level]) 170*4882a593Smuzhiyun if module: 171*4882a593Smuzhiyun if context: 172*4882a593Smuzhiyun yield context + '.' + module, path 173*4882a593Smuzhiyun else: 174*4882a593Smuzhiyun yield module, path 175*4882a593Smuzhiyun 176*4882a593Smuzhiyun 177*4882a593Smuzhiyundef get_file_depends(path): 178*4882a593Smuzhiyun try: 179*4882a593Smuzhiyun code_string = open(path, 'r').read() 180*4882a593Smuzhiyun except (OSError, IOError) as exc: 181*4882a593Smuzhiyun raise DependError(path, exc) 182*4882a593Smuzhiyun 183*4882a593Smuzhiyun return get_code_depends(code_string, path) 184*4882a593Smuzhiyun 185*4882a593Smuzhiyun 186*4882a593Smuzhiyundef get_depends_recursive(directory): 187*4882a593Smuzhiyun directory = os.path.realpath(directory) 188*4882a593Smuzhiyun 189*4882a593Smuzhiyun provides = dict((v, k) for k, v in get_provides(directory)) 190*4882a593Smuzhiyun for filename, provide in provides.items(): 191*4882a593Smuzhiyun if os.path.isdir(filename): 192*4882a593Smuzhiyun filename = os.path.join(filename, '__init__.py') 193*4882a593Smuzhiyun ispkg = True 194*4882a593Smuzhiyun elif not filename.endswith('.py'): 195*4882a593Smuzhiyun continue 196*4882a593Smuzhiyun else: 197*4882a593Smuzhiyun ispkg = False 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun with open(filename, 'r') as f: 200*4882a593Smuzhiyun source = f.read() 201*4882a593Smuzhiyun 202*4882a593Smuzhiyun depends = get_code_depends(source, filename, provide, ispkg) 203*4882a593Smuzhiyun for depend, by in depends: 204*4882a593Smuzhiyun yield depend, by 205*4882a593Smuzhiyun 206*4882a593Smuzhiyun 207*4882a593Smuzhiyundef get_depends(path): 208*4882a593Smuzhiyun if os.path.isdir(path): 209*4882a593Smuzhiyun return get_depends_recursive(path) 210*4882a593Smuzhiyun else: 211*4882a593Smuzhiyun return get_file_depends(path) 212*4882a593Smuzhiyun 213*4882a593Smuzhiyun 214*4882a593Smuzhiyundef main(): 215*4882a593Smuzhiyun logging.basicConfig() 216*4882a593Smuzhiyun 217*4882a593Smuzhiyun parser = argparse.ArgumentParser(description='Determine dependencies and provided packages for python scripts/modules') 218*4882a593Smuzhiyun parser.add_argument('path', nargs='+', help='full path to content to be processed') 219*4882a593Smuzhiyun group = parser.add_mutually_exclusive_group() 220*4882a593Smuzhiyun group.add_argument('-p', '--provides', action='store_true', 221*4882a593Smuzhiyun help='given a path, display the provided python modules') 222*4882a593Smuzhiyun group.add_argument('-d', '--depends', action='store_true', 223*4882a593Smuzhiyun help='given a filename, display the imported python modules') 224*4882a593Smuzhiyun 225*4882a593Smuzhiyun args = parser.parse_args() 226*4882a593Smuzhiyun if args.provides: 227*4882a593Smuzhiyun modules = set() 228*4882a593Smuzhiyun for path in args.path: 229*4882a593Smuzhiyun for provide, fn in get_provides(path): 230*4882a593Smuzhiyun modules.add(provide) 231*4882a593Smuzhiyun 232*4882a593Smuzhiyun for module in sorted(modules): 233*4882a593Smuzhiyun print(module) 234*4882a593Smuzhiyun elif args.depends: 235*4882a593Smuzhiyun for path in args.path: 236*4882a593Smuzhiyun try: 237*4882a593Smuzhiyun modules = get_depends(path) 238*4882a593Smuzhiyun except PythonDepError as exc: 239*4882a593Smuzhiyun logger.error(str(exc)) 240*4882a593Smuzhiyun sys.exit(1) 241*4882a593Smuzhiyun 242*4882a593Smuzhiyun for module, imp_by in modules: 243*4882a593Smuzhiyun print("{}\t{}".format(module, imp_by)) 244*4882a593Smuzhiyun else: 245*4882a593Smuzhiyun parser.print_help() 246*4882a593Smuzhiyun sys.exit(2) 247*4882a593Smuzhiyun 248*4882a593Smuzhiyun 249*4882a593Smuzhiyunif __name__ == '__main__': 250*4882a593Smuzhiyun main() 251