1#! /usr/bin/env python3 2 3# pdeps 4# 5# Find dependencies between a bunch of Python modules. 6# 7# Usage: 8# pdeps file1.py file2.py ... 9# 10# Output: 11# Four tables separated by lines like '--- Closure ---': 12# 1) Direct dependencies, listing which module imports which other modules 13# 2) The inverse of (1) 14# 3) Indirect dependencies, or the closure of the above 15# 4) The inverse of (3) 16# 17# To do: 18# - command line options to select output type 19# - option to automatically scan the Python library for referenced modules 20# - option to limit output to particular modules 21 22 23import sys 24import re 25import os 26 27 28# Main program 29# 30def main(): 31 args = sys.argv[1:] 32 if not args: 33 print('usage: pdeps file.py file.py ...') 34 return 2 35 # 36 table = {} 37 for arg in args: 38 process(arg, table) 39 # 40 print('--- Uses ---') 41 printresults(table) 42 # 43 print('--- Used By ---') 44 inv = inverse(table) 45 printresults(inv) 46 # 47 print('--- Closure of Uses ---') 48 reach = closure(table) 49 printresults(reach) 50 # 51 print('--- Closure of Used By ---') 52 invreach = inverse(reach) 53 printresults(invreach) 54 # 55 return 0 56 57 58# Compiled regular expressions to search for import statements 59# 60m_import = re.compile('^[ \t]*from[ \t]+([^ \t]+)[ \t]+') 61m_from = re.compile('^[ \t]*import[ \t]+([^#]+)') 62 63 64# Collect data from one file 65# 66def process(filename, table): 67 fp = open(filename, 'r') 68 mod = os.path.basename(filename) 69 if mod[-3:] == '.py': 70 mod = mod[:-3] 71 table[mod] = list = [] 72 while 1: 73 line = fp.readline() 74 if not line: break 75 while line[-1:] == '\\': 76 nextline = fp.readline() 77 if not nextline: break 78 line = line[:-1] + nextline 79 m_found = m_import.match(line) or m_from.match(line) 80 if m_found: 81 (a, b), (a1, b1) = m_found.regs[:2] 82 else: continue 83 words = line[a1:b1].split(',') 84 # print '#', line, words 85 for word in words: 86 word = word.strip() 87 if word not in list: 88 list.append(word) 89 fp.close() 90 91 92# Compute closure (this is in fact totally general) 93# 94def closure(table): 95 modules = list(table.keys()) 96 # 97 # Initialize reach with a copy of table 98 # 99 reach = {} 100 for mod in modules: 101 reach[mod] = table[mod][:] 102 # 103 # Iterate until no more change 104 # 105 change = 1 106 while change: 107 change = 0 108 for mod in modules: 109 for mo in reach[mod]: 110 if mo in modules: 111 for m in reach[mo]: 112 if m not in reach[mod]: 113 reach[mod].append(m) 114 change = 1 115 # 116 return reach 117 118 119# Invert a table (this is again totally general). 120# All keys of the original table are made keys of the inverse, 121# so there may be empty lists in the inverse. 122# 123def inverse(table): 124 inv = {} 125 for key in table.keys(): 126 if key not in inv: 127 inv[key] = [] 128 for item in table[key]: 129 store(inv, item, key) 130 return inv 131 132 133# Store "item" in "dict" under "key". 134# The dictionary maps keys to lists of items. 135# If there is no list for the key yet, it is created. 136# 137def store(dict, key, item): 138 if key in dict: 139 dict[key].append(item) 140 else: 141 dict[key] = [item] 142 143 144# Tabulate results neatly 145# 146def printresults(table): 147 modules = sorted(table.keys()) 148 maxlen = 0 149 for mod in modules: maxlen = max(maxlen, len(mod)) 150 for mod in modules: 151 list = sorted(table[mod]) 152 print(mod.ljust(maxlen), ':', end=' ') 153 if mod in list: 154 print('(*)', end=' ') 155 for ref in list: 156 print(ref, end=' ') 157 print() 158 159 160# Call main and honor exit status 161if __name__ == '__main__': 162 try: 163 sys.exit(main()) 164 except KeyboardInterrupt: 165 sys.exit(1) 166