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 23 import sys 24 import re 25 import os 26 27 28 # Main program 29 # 30 def 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 # 60 m_import = re.compile('^[ \t]*from[ \t]+([^ \t]+)[ \t]+') 61 m_from = re.compile('^[ \t]*import[ \t]+([^#]+)') 62 63 64 # Collect data from one file 65 # 66 def 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 # 94 def 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 # 123 def 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 # 137 def 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 # 146 def 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 161 if __name__ == '__main__': 162 try: 163 sys.exit(main()) 164 except KeyboardInterrupt: 165 sys.exit(1) 166