17db96d56Sopenharmony_ci#! /usr/bin/env python3
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ci# pdeps
47db96d56Sopenharmony_ci#
57db96d56Sopenharmony_ci# Find dependencies between a bunch of Python modules.
67db96d56Sopenharmony_ci#
77db96d56Sopenharmony_ci# Usage:
87db96d56Sopenharmony_ci#       pdeps file1.py file2.py ...
97db96d56Sopenharmony_ci#
107db96d56Sopenharmony_ci# Output:
117db96d56Sopenharmony_ci# Four tables separated by lines like '--- Closure ---':
127db96d56Sopenharmony_ci# 1) Direct dependencies, listing which module imports which other modules
137db96d56Sopenharmony_ci# 2) The inverse of (1)
147db96d56Sopenharmony_ci# 3) Indirect dependencies, or the closure of the above
157db96d56Sopenharmony_ci# 4) The inverse of (3)
167db96d56Sopenharmony_ci#
177db96d56Sopenharmony_ci# To do:
187db96d56Sopenharmony_ci# - command line options to select output type
197db96d56Sopenharmony_ci# - option to automatically scan the Python library for referenced modules
207db96d56Sopenharmony_ci# - option to limit output to particular modules
217db96d56Sopenharmony_ci
227db96d56Sopenharmony_ci
237db96d56Sopenharmony_ciimport sys
247db96d56Sopenharmony_ciimport re
257db96d56Sopenharmony_ciimport os
267db96d56Sopenharmony_ci
277db96d56Sopenharmony_ci
287db96d56Sopenharmony_ci# Main program
297db96d56Sopenharmony_ci#
307db96d56Sopenharmony_cidef main():
317db96d56Sopenharmony_ci    args = sys.argv[1:]
327db96d56Sopenharmony_ci    if not args:
337db96d56Sopenharmony_ci        print('usage: pdeps file.py file.py ...')
347db96d56Sopenharmony_ci        return 2
357db96d56Sopenharmony_ci    #
367db96d56Sopenharmony_ci    table = {}
377db96d56Sopenharmony_ci    for arg in args:
387db96d56Sopenharmony_ci        process(arg, table)
397db96d56Sopenharmony_ci    #
407db96d56Sopenharmony_ci    print('--- Uses ---')
417db96d56Sopenharmony_ci    printresults(table)
427db96d56Sopenharmony_ci    #
437db96d56Sopenharmony_ci    print('--- Used By ---')
447db96d56Sopenharmony_ci    inv = inverse(table)
457db96d56Sopenharmony_ci    printresults(inv)
467db96d56Sopenharmony_ci    #
477db96d56Sopenharmony_ci    print('--- Closure of Uses ---')
487db96d56Sopenharmony_ci    reach = closure(table)
497db96d56Sopenharmony_ci    printresults(reach)
507db96d56Sopenharmony_ci    #
517db96d56Sopenharmony_ci    print('--- Closure of Used By ---')
527db96d56Sopenharmony_ci    invreach = inverse(reach)
537db96d56Sopenharmony_ci    printresults(invreach)
547db96d56Sopenharmony_ci    #
557db96d56Sopenharmony_ci    return 0
567db96d56Sopenharmony_ci
577db96d56Sopenharmony_ci
587db96d56Sopenharmony_ci# Compiled regular expressions to search for import statements
597db96d56Sopenharmony_ci#
607db96d56Sopenharmony_cim_import = re.compile('^[ \t]*from[ \t]+([^ \t]+)[ \t]+')
617db96d56Sopenharmony_cim_from = re.compile('^[ \t]*import[ \t]+([^#]+)')
627db96d56Sopenharmony_ci
637db96d56Sopenharmony_ci
647db96d56Sopenharmony_ci# Collect data from one file
657db96d56Sopenharmony_ci#
667db96d56Sopenharmony_cidef process(filename, table):
677db96d56Sopenharmony_ci    with open(filename, encoding='utf-8') as fp:
687db96d56Sopenharmony_ci        mod = os.path.basename(filename)
697db96d56Sopenharmony_ci        if mod[-3:] == '.py':
707db96d56Sopenharmony_ci            mod = mod[:-3]
717db96d56Sopenharmony_ci        table[mod] = list = []
727db96d56Sopenharmony_ci        while 1:
737db96d56Sopenharmony_ci            line = fp.readline()
747db96d56Sopenharmony_ci            if not line: break
757db96d56Sopenharmony_ci            while line[-1:] == '\\':
767db96d56Sopenharmony_ci                nextline = fp.readline()
777db96d56Sopenharmony_ci                if not nextline: break
787db96d56Sopenharmony_ci                line = line[:-1] + nextline
797db96d56Sopenharmony_ci            m_found = m_import.match(line) or m_from.match(line)
807db96d56Sopenharmony_ci            if m_found:
817db96d56Sopenharmony_ci                (a, b), (a1, b1) = m_found.regs[:2]
827db96d56Sopenharmony_ci            else: continue
837db96d56Sopenharmony_ci            words = line[a1:b1].split(',')
847db96d56Sopenharmony_ci            # print '#', line, words
857db96d56Sopenharmony_ci            for word in words:
867db96d56Sopenharmony_ci                word = word.strip()
877db96d56Sopenharmony_ci                if word not in list:
887db96d56Sopenharmony_ci                    list.append(word)
897db96d56Sopenharmony_ci
907db96d56Sopenharmony_ci
917db96d56Sopenharmony_ci# Compute closure (this is in fact totally general)
927db96d56Sopenharmony_ci#
937db96d56Sopenharmony_cidef closure(table):
947db96d56Sopenharmony_ci    modules = list(table.keys())
957db96d56Sopenharmony_ci    #
967db96d56Sopenharmony_ci    # Initialize reach with a copy of table
977db96d56Sopenharmony_ci    #
987db96d56Sopenharmony_ci    reach = {}
997db96d56Sopenharmony_ci    for mod in modules:
1007db96d56Sopenharmony_ci        reach[mod] = table[mod][:]
1017db96d56Sopenharmony_ci    #
1027db96d56Sopenharmony_ci    # Iterate until no more change
1037db96d56Sopenharmony_ci    #
1047db96d56Sopenharmony_ci    change = 1
1057db96d56Sopenharmony_ci    while change:
1067db96d56Sopenharmony_ci        change = 0
1077db96d56Sopenharmony_ci        for mod in modules:
1087db96d56Sopenharmony_ci            for mo in reach[mod]:
1097db96d56Sopenharmony_ci                if mo in modules:
1107db96d56Sopenharmony_ci                    for m in reach[mo]:
1117db96d56Sopenharmony_ci                        if m not in reach[mod]:
1127db96d56Sopenharmony_ci                            reach[mod].append(m)
1137db96d56Sopenharmony_ci                            change = 1
1147db96d56Sopenharmony_ci    #
1157db96d56Sopenharmony_ci    return reach
1167db96d56Sopenharmony_ci
1177db96d56Sopenharmony_ci
1187db96d56Sopenharmony_ci# Invert a table (this is again totally general).
1197db96d56Sopenharmony_ci# All keys of the original table are made keys of the inverse,
1207db96d56Sopenharmony_ci# so there may be empty lists in the inverse.
1217db96d56Sopenharmony_ci#
1227db96d56Sopenharmony_cidef inverse(table):
1237db96d56Sopenharmony_ci    inv = {}
1247db96d56Sopenharmony_ci    for key in table.keys():
1257db96d56Sopenharmony_ci        if key not in inv:
1267db96d56Sopenharmony_ci            inv[key] = []
1277db96d56Sopenharmony_ci        for item in table[key]:
1287db96d56Sopenharmony_ci            store(inv, item, key)
1297db96d56Sopenharmony_ci    return inv
1307db96d56Sopenharmony_ci
1317db96d56Sopenharmony_ci
1327db96d56Sopenharmony_ci# Store "item" in "dict" under "key".
1337db96d56Sopenharmony_ci# The dictionary maps keys to lists of items.
1347db96d56Sopenharmony_ci# If there is no list for the key yet, it is created.
1357db96d56Sopenharmony_ci#
1367db96d56Sopenharmony_cidef store(dict, key, item):
1377db96d56Sopenharmony_ci    if key in dict:
1387db96d56Sopenharmony_ci        dict[key].append(item)
1397db96d56Sopenharmony_ci    else:
1407db96d56Sopenharmony_ci        dict[key] = [item]
1417db96d56Sopenharmony_ci
1427db96d56Sopenharmony_ci
1437db96d56Sopenharmony_ci# Tabulate results neatly
1447db96d56Sopenharmony_ci#
1457db96d56Sopenharmony_cidef printresults(table):
1467db96d56Sopenharmony_ci    modules = sorted(table.keys())
1477db96d56Sopenharmony_ci    maxlen = 0
1487db96d56Sopenharmony_ci    for mod in modules: maxlen = max(maxlen, len(mod))
1497db96d56Sopenharmony_ci    for mod in modules:
1507db96d56Sopenharmony_ci        list = sorted(table[mod])
1517db96d56Sopenharmony_ci        print(mod.ljust(maxlen), ':', end=' ')
1527db96d56Sopenharmony_ci        if mod in list:
1537db96d56Sopenharmony_ci            print('(*)', end=' ')
1547db96d56Sopenharmony_ci        for ref in list:
1557db96d56Sopenharmony_ci            print(ref, end=' ')
1567db96d56Sopenharmony_ci        print()
1577db96d56Sopenharmony_ci
1587db96d56Sopenharmony_ci
1597db96d56Sopenharmony_ci# Call main and honor exit status
1607db96d56Sopenharmony_ciif __name__ == '__main__':
1617db96d56Sopenharmony_ci    try:
1627db96d56Sopenharmony_ci        sys.exit(main())
1637db96d56Sopenharmony_ci    except KeyboardInterrupt:
1647db96d56Sopenharmony_ci        sys.exit(1)
165