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 with open(filename, encoding='utf-8') as fp: 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 90 91# Compute closure (this is in fact totally general) 92# 93def closure(table): 94 modules = list(table.keys()) 95 # 96 # Initialize reach with a copy of table 97 # 98 reach = {} 99 for mod in modules: 100 reach[mod] = table[mod][:] 101 # 102 # Iterate until no more change 103 # 104 change = 1 105 while change: 106 change = 0 107 for mod in modules: 108 for mo in reach[mod]: 109 if mo in modules: 110 for m in reach[mo]: 111 if m not in reach[mod]: 112 reach[mod].append(m) 113 change = 1 114 # 115 return reach 116 117 118# Invert a table (this is again totally general). 119# All keys of the original table are made keys of the inverse, 120# so there may be empty lists in the inverse. 121# 122def inverse(table): 123 inv = {} 124 for key in table.keys(): 125 if key not in inv: 126 inv[key] = [] 127 for item in table[key]: 128 store(inv, item, key) 129 return inv 130 131 132# Store "item" in "dict" under "key". 133# The dictionary maps keys to lists of items. 134# If there is no list for the key yet, it is created. 135# 136def store(dict, key, item): 137 if key in dict: 138 dict[key].append(item) 139 else: 140 dict[key] = [item] 141 142 143# Tabulate results neatly 144# 145def printresults(table): 146 modules = sorted(table.keys()) 147 maxlen = 0 148 for mod in modules: maxlen = max(maxlen, len(mod)) 149 for mod in modules: 150 list = sorted(table[mod]) 151 print(mod.ljust(maxlen), ':', end=' ') 152 if mod in list: 153 print('(*)', end=' ') 154 for ref in list: 155 print(ref, end=' ') 156 print() 157 158 159# Call main and honor exit status 160if __name__ == '__main__': 161 try: 162 sys.exit(main()) 163 except KeyboardInterrupt: 164 sys.exit(1) 165