comparison scripts/deps.py @ 8772:52816813cbec

docs: describe, visualize, and verify internal code structure and layering Try to describe something that isn't entirely there yet. deps.py will help track and minimize violations through checks and visualization in deps.svg .
author Mads Kiilerich <mads@kiilerich.com>
date Fri, 13 Nov 2020 01:04:30 +0100
parents
children 341e4bb9e227
comparison
equal deleted inserted replaced
8771:f8971422795e 8772:52816813cbec
1 #!/usr/bin/env python3
2
3
4 import re
5 import sys
6
7
8 ignored_modules = set('''
9 argparse
10 base64
11 bcrypt
12 binascii
13 bleach
14 calendar
15 celery
16 celery
17 chardet
18 click
19 collections
20 configparser
21 copy
22 csv
23 ctypes
24 datetime
25 dateutil
26 decimal
27 decorator
28 difflib
29 distutils
30 docutils
31 email
32 errno
33 fileinput
34 functools
35 getpass
36 grp
37 hashlib
38 hmac
39 html
40 http
41 imp
42 importlib
43 inspect
44 io
45 ipaddr
46 IPython
47 isapi_wsgi
48 itertools
49 json
50 kajiki
51 ldap
52 logging
53 mako
54 markdown
55 mimetypes
56 mock
57 msvcrt
58 multiprocessing
59 operator
60 os
61 paginate
62 paginate_sqlalchemy
63 pam
64 paste
65 pkg_resources
66 platform
67 posixpath
68 pprint
69 pwd
70 pyflakes
71 pytest
72 pytest_localserver
73 random
74 re
75 routes
76 setuptools
77 shlex
78 shutil
79 smtplib
80 socket
81 ssl
82 stat
83 string
84 struct
85 subprocess
86 sys
87 tarfile
88 tempfile
89 textwrap
90 tgext
91 threading
92 time
93 traceback
94 traitlets
95 types
96 urllib
97 urlobject
98 uuid
99 warnings
100 webhelpers2
101 webob
102 webtest
103 whoosh
104 win32traceutil
105 zipfile
106 '''.split())
107
108 top_modules = set('''
109 kallithea.alembic
110 kallithea.bin
111 kallithea.config
112 kallithea.controllers
113 kallithea.templates.py
114 scripts
115 '''.split())
116
117 bottom_external_modules = set('''
118 tg
119 mercurial
120 sqlalchemy
121 alembic
122 formencode
123 pygments
124 dulwich
125 beaker
126 psycopg2
127 docs
128 setup
129 conftest
130 '''.split())
131
132 normal_modules = set('''
133 kallithea
134 kallithea.lib.celerylib.tasks
135 kallithea.lib
136 kallithea.lib.auth
137 kallithea.lib.auth_modules
138 kallithea.lib.base
139 kallithea.lib.celerylib
140 kallithea.lib.db_manage
141 kallithea.lib.helpers
142 kallithea.lib.hooks
143 kallithea.lib.indexers
144 kallithea.lib.utils
145 kallithea.lib.utils2
146 kallithea.lib.vcs
147 kallithea.lib.webutils
148 kallithea.model
149 kallithea.model.scm
150 kallithea.templates.py
151 '''.split())
152
153 shown_modules = normal_modules | top_modules
154
155 # break the chains somehow - this is a cleanup TODO list
156 known_violations = [
157 ('kallithea.lib.auth_modules', 'kallithea.lib.auth'), # needs base&facade
158 ('kallithea.lib.utils', 'kallithea.model'), # clean up utils
159 ('kallithea.lib.utils', 'kallithea.model.db'),
160 ('kallithea.lib.utils', 'kallithea.model.scm'),
161 ('kallithea.lib.celerylib.tasks', 'kallithea.lib.helpers'),
162 ('kallithea.lib.celerylib.tasks', 'kallithea.lib.hooks'),
163 ('kallithea.lib.celerylib.tasks', 'kallithea.lib.indexers'),
164 ('kallithea.lib.celerylib.tasks', 'kallithea.model'),
165 ('kallithea.model', 'kallithea.lib.auth'), # auth.HasXXX
166 ('kallithea.model', 'kallithea.lib.auth_modules'), # validators
167 ('kallithea.model', 'kallithea.lib.helpers'),
168 ('kallithea.model', 'kallithea.lib.hooks'), # clean up hooks
169 ('kallithea.model', 'kallithea.model.scm'),
170 ('kallithea.model.scm', 'kallithea.lib.hooks'),
171 ]
172
173 extra_edges = [
174 ('kallithea.config', 'kallithea.controllers'), # through TG
175 ('kallithea.lib.auth', 'kallithea.lib.auth_modules'), # custom loader
176 ]
177
178
179 def normalize(s):
180 """Given a string with dot path, return the string it should be shown as."""
181 parts = s.replace('.__init__', '').split('.')
182 short_2 = '.'.join(parts[:2])
183 short_3 = '.'.join(parts[:3])
184 short_4 = '.'.join(parts[:4])
185 if parts[0] in ['scripts', 'contributor_data', 'i18n_utils']:
186 return 'scripts'
187 if short_3 == 'kallithea.model.meta':
188 return 'kallithea.model.db'
189 if parts[:4] == ['kallithea', 'lib', 'vcs', 'ssh']:
190 return 'kallithea.lib.vcs.ssh'
191 if short_4 in shown_modules:
192 return short_4
193 if short_3 in shown_modules:
194 return short_3
195 if short_2 in shown_modules:
196 return short_2
197 if short_2 == 'kallithea.tests':
198 return None
199 if parts[0] in ignored_modules:
200 return None
201 assert parts[0] in bottom_external_modules, parts
202 return parts[0]
203
204
205 def main(filenames):
206 if not filenames or filenames[0].startswith('-'):
207 print('''\
208 Usage:
209 hg files 'set:!binary()&grep("^#!.*python")' 'set:**.py' | xargs scripts/deps.py
210 dot -Tsvg deps.dot > deps.svg
211 ''')
212 raise SystemExit(1)
213
214 files_imports = dict() # map filenames to its imports
215 import_deps = set() # set of tuples with module name and its imports
216 for fn in filenames:
217 with open(fn) as f:
218 s = f.read()
219
220 dot_name = (fn[:-3] if fn.endswith('.py') else fn).replace('/', '.')
221 file_imports = set()
222 for m in re.finditer(r'^ *(?:from ([^ ]*) import (?:([a-zA-Z].*)|\(([^)]*)\))|import (.*))$', s, re.MULTILINE):
223 m_from, m_from_import, m_from_import2, m_import = m.groups()
224 if m_from:
225 pre = m_from + '.'
226 if pre.startswith('.'):
227 pre = dot_name.rsplit('.', 1)[0] + pre
228 importlist = m_from_import or m_from_import2
229 else:
230 pre = ''
231 importlist = m_import
232 for imp in importlist.split('#', 1)[0].split(','):
233 full_imp = pre + imp.strip().split(' as ', 1)[0]
234 file_imports.add(full_imp)
235 import_deps.add((dot_name, full_imp))
236 files_imports[fn] = file_imports
237
238 # dump out all deps for debugging and analysis
239 with open('deps.txt', 'w') as f:
240 for fn, file_imports in sorted(files_imports.items()):
241 for file_import in sorted(file_imports):
242 if file_import.split('.', 1)[0] in ignored_modules:
243 continue
244 f.write('%s: %s\n' % (fn, file_import))
245
246 # find leafs that haven't been ignored - they are the important external dependencies and shown in the bottom row
247 only_imported = set(
248 set(normalize(b) for a, b in import_deps) -
249 set(normalize(a) for a, b in import_deps) -
250 set([None, 'kallithea'])
251 )
252
253 normalized_dep_edges = set()
254 for dot_name, full_imp in import_deps:
255 a = normalize(dot_name)
256 b = normalize(full_imp)
257 if a is None or b is None or a == b:
258 continue
259 normalized_dep_edges.add((a, b))
260 #print((dot_name, full_imp, a, b))
261 normalized_dep_edges.update(extra_edges)
262
263 unseen_shown_modules = shown_modules.difference(a for a, b in normalized_dep_edges).difference(b for a, b in normalized_dep_edges)
264 assert not unseen_shown_modules, unseen_shown_modules
265
266 with open('deps.dot', 'w') as f:
267 f.write('digraph {\n')
268 f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(top_modules)))
269 f.write('subgraph { rank = same; %s}\n' % ''.join('"%s"; ' % s for s in sorted(only_imported)))
270 for a, b in sorted(normalized_dep_edges):
271 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 ''))
272 f.write('}\n')
273
274 # verify dependencies by untangling dependency chain bottom-up:
275 todo = set(normalized_dep_edges)
276 for x in known_violations:
277 todo.remove(x)
278
279 while todo:
280 depending = set(a for a, b in todo)
281 depended = set(b for a, b in todo)
282 drop = depended - depending
283 if not drop:
284 print('ERROR: cycles:', len(todo))
285 for x in sorted(todo):
286 print('%s,' % (x,))
287 raise SystemExit(1)
288 #for do_b in sorted(drop):
289 # print('Picking', do_b, '- unblocks:', ' '.join(a for a, b in sorted((todo)) if b == do_b))
290 todo = set((a, b) for a, b in todo if b in depending)
291 #print()
292
293
294 if __name__ == '__main__':
295 main(sys.argv[1:])