#!/usr/bin/env python3 import re import sys ignored_modules = set(''' argparse base64 bcrypt binascii bleach calendar celery celery chardet click collections configparser copy csv ctypes datetime dateutil decimal decorator difflib distutils docutils email errno fileinput functools getpass grp hashlib hmac html http imp importlib inspect io ipaddr IPython isapi_wsgi itertools json kajiki ldap logging mako markdown mimetypes mock msvcrt multiprocessing operator os paginate paginate_sqlalchemy pam paste pkg_resources platform posixpath pprint pwd pyflakes pytest pytest_localserver random re routes setuptools shlex shutil smtplib socket ssl stat string struct subprocess sys tarfile tempfile textwrap tgext threading time traceback traitlets types urllib urlobject uuid warnings webhelpers2 webob webtest whoosh win32traceutil zipfile '''.split()) top_modules = set(''' kallithea.alembic kallithea.bin kallithea.config kallithea.controllers kallithea.templates.py scripts '''.split()) bottom_external_modules = set(''' tg mercurial sqlalchemy alembic formencode pygments dulwich beaker psycopg2 docs setup conftest '''.split()) normal_modules = set(''' kallithea kallithea.lib.celerylib.tasks kallithea.lib kallithea.lib.auth kallithea.lib.auth_modules kallithea.lib.base kallithea.lib.celerylib kallithea.lib.db_manage kallithea.lib.helpers kallithea.lib.hooks kallithea.lib.indexers kallithea.lib.utils kallithea.lib.utils2 kallithea.lib.vcs kallithea.lib.webutils kallithea.model kallithea.model.scm kallithea.templates.py '''.split()) shown_modules = normal_modules | top_modules # break the chains somehow - this is a cleanup TODO list known_violations = [ ('kallithea.lib.auth_modules', 'kallithea.lib.auth'), # needs base&facade ('kallithea.lib.utils', 'kallithea.model'), # clean up utils ('kallithea.lib.utils', 'kallithea.model.db'), ('kallithea.lib.utils', 'kallithea.model.scm'), ('kallithea.lib.celerylib.tasks', 'kallithea.lib.helpers'), ('kallithea.lib.celerylib.tasks', 'kallithea.lib.hooks'), ('kallithea.lib.celerylib.tasks', 'kallithea.lib.indexers'), ('kallithea.lib.celerylib.tasks', 'kallithea.model'), ('kallithea.model', 'kallithea.lib.auth'), # auth.HasXXX ('kallithea.model', 'kallithea.lib.auth_modules'), # validators ('kallithea.model', 'kallithea.lib.helpers'), ('kallithea.model', 'kallithea.lib.hooks'), # clean up hooks ('kallithea.model', 'kallithea.model.scm'), ('kallithea.model.scm', 'kallithea.lib.hooks'), ] extra_edges = [ ('kallithea.config', 'kallithea.controllers'), # through TG ('kallithea.lib.auth', 'kallithea.lib.auth_modules'), # custom loader ] def normalize(s): """Given a string with dot path, return the string it should be shown as.""" parts = s.replace('.__init__', '').split('.') short_2 = '.'.join(parts[:2]) short_3 = '.'.join(parts[:3]) short_4 = '.'.join(parts[:4]) if parts[0] in ['scripts', 'contributor_data', 'i18n_utils']: return 'scripts' if short_3 == 'kallithea.model.meta': return 'kallithea.model.db' if parts[:4] == ['kallithea', 'lib', 'vcs', 'ssh']: return 'kallithea.lib.vcs.ssh' if short_4 in shown_modules: return short_4 if short_3 in shown_modules: return short_3 if short_2 in shown_modules: return short_2 if short_2 == 'kallithea.tests': return None if parts[0] in ignored_modules: return None assert parts[0] in bottom_external_modules, parts return parts[0] def main(filenames): if not filenames or filenames[0].startswith('-'): print('''\ Usage: hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/deps.py dot -Tsvg deps.dot > deps.svg ''') raise SystemExit(1) files_imports = dict() # map filenames to its imports import_deps = set() # set of tuples with module name and its imports for fn in filenames: with open(fn) as f: s = f.read() dot_name = (fn[:-3] if fn.endswith('.py') else fn).replace('/', '.') file_imports = set() for m in re.finditer(r'^ *(?:from ([^ ]*) import (?:([a-zA-Z].*)|\(([^)]*)\))|import (.*))$', s, re.MULTILINE): m_from, m_from_import, m_from_import2, m_import = m.groups() if m_from: pre = m_from + '.' if pre.startswith('.'): pre = dot_name.rsplit('.', 1)[0] + pre importlist = m_from_import or m_from_import2 else: pre = '' importlist = m_import for imp in importlist.split('#', 1)[0].split(','): full_imp = pre + imp.strip().split(' as ', 1)[0] file_imports.add(full_imp) import_deps.add((dot_name, full_imp)) files_imports[fn] = file_imports # dump out all deps for debugging and analysis with open('deps.txt', 'w') as f: for fn, file_imports in sorted(files_imports.items()): for file_import in sorted(file_imports): if file_import.split('.', 1)[0] in ignored_modules: continue f.write('%s: %s\n' % (fn, file_import)) # find leafs that haven't been ignored - they are the important external dependencies and shown in the bottom row only_imported = set( set(normalize(b) for a, b in import_deps) - set(normalize(a) for a, b in import_deps) - set([None, 'kallithea']) ) normalized_dep_edges = set() for dot_name, full_imp in import_deps: a = normalize(dot_name) b = normalize(full_imp) if a is None or b is None or a == b: continue normalized_dep_edges.add((a, b)) #print((dot_name, full_imp, a, b)) normalized_dep_edges.update(extra_edges) unseen_shown_modules = shown_modules.difference(a for a, b in normalized_dep_edges).difference(b for a, b in normalized_dep_edges) assert not unseen_shown_modules, unseen_shown_modules with open('deps.dot', 'w') as f: f.write('digraph {\n') f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(top_modules))) f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(only_imported))) for a, b in sorted(normalized_dep_edges): f.write(' "%s" -> "%s"%s\n' % (a, b, ' [color=red]' if (a, b) in known_violations else ' [color=green]' if (a, b) in extra_edges else '')) f.write('}\n') # verify dependencies by untangling dependency chain bottom-up: todo = set(normalized_dep_edges) for x in known_violations: todo.remove(x) while todo: depending = set(a for a, b in todo) depended = set(b for a, b in todo) drop = depended - depending if not drop: print('ERROR: cycles:', len(todo)) for x in sorted(todo): print('%s,' % (x,)) raise SystemExit(1) #for do_b in sorted(drop): # print('Picking', do_b, '- unblocks:', ' '.join(a for a, b in sorted((todo)) if b == do_b)) todo = set((a, b) for a, b in todo if b in depending) #print() if __name__ == '__main__': main(sys.argv[1:])