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