b1e27ccd308972017baa4bcfefae7ed9eb138bf0
[depgraph.git] / depgraph2dot.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2004      Toby Dickenson
4 # Copyright 2008-2011 W. Trevor King
5 #
6 # Permission is hereby granted, free of charge, to any person obtaining
7 # a copy of this software and associated documentation files (the
8 # "Software"), to deal in the Software without restriction, including
9 # without limitation the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the Software, and to
11 # permit persons to whom the Software is furnished to do so, subject
12 # to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included
15 # in all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25 """Convert `py2depgraph` dependency data to Graphviz dot syntax.
26 """
27
28 import colorsys
29 from hashlib import md5
30 import imp
31 from os import popen, getuid  # for finding C extension dependencies with system calls
32 from pwd import getpwuid
33 import re
34 import sys
35
36
37 USER=getpwuid(getuid())[0] # get effective user name
38
39 INVISIBLE_MODS=('__future__','copy','doctest','glob','optparse','os','qt','re',
40                 'StringIO','string','sys','textwrap','time','types','unittest')
41 INVISIBLE_PATHS=[] #(r'.*',)
42 VISIBLE_PATHS=(r'.*%s.*' % USER,r'.*comedi.*')
43
44 def _pathmatch(regexp_tuple, path) :
45     "Check if a regexp in regexp tuple matches the string path"
46     for regexp in regexp_tuple :
47         if re.match(regexp, path) != None :
48             return True
49     return False
50
51 class hooks (object) :
52     """
53     Modules show up if visible_mod_test(...) == True.
54
55     """
56     def __init__(self,
57                  invisible_mods=INVISIBLE_MODS,
58                  invisible_paths=INVISIBLE_PATHS,
59                  visible_paths=VISIBLE_PATHS,
60                  link_outside_visited_nodes=True,
61                  ignore_builtins=True) :
62         self._invisible_mods = invisible_mods
63         self._invisible_paths = invisible_paths
64         self._visible_paths = visible_paths
65         self._link_outside_visited_nodes = link_outside_visited_nodes
66         self._ignore_builtins = ignore_builtins
67         self._entered_bonus_nodes = {} # a dict of bonus nodes already printed
68     def continue_test(self) :
69         return True
70     def visible_mod_test(self, mod_name, dep_dict, type, path,
71                          check_external_link=True) :
72         """
73         Return true if this module is interesting and should be drawn.
74         Return false if it should be completely omitted.
75         """
76         if self._invisible_name(mod_name) == True :
77             if self._debug :  print "\t\tinvisible module", mod_name
78             return False
79         if self._link_outside_visited_nodes == False \
80                 and check_external_link == True \
81                 and mod_name != '__main__' :
82             if self.follow_edge_test('__main__', imp.PY_SOURCE, './dummy.py',
83                                      mod_name, type, path) == False:
84                 return False # don't draw nodes we wouldn't visit
85         return True
86     def follow_edge_test(self, module_name, type, path, 
87                          dname, dtype, dpath):
88         if self._debug :
89             print "\ttesting edge from %s %s %s to %s %s %s" \
90                 % (module_name, type, path, dname, dtype, dpath)
91         if self.visible_mod_test(dname, None, dtype, dpath,
92                                  check_external_link=False) == False :
93             if self._debug :  print "\t\tinvisible target module"
94             return False # don't draw edges to invisible modules
95         elif dname == '__main__':
96             # references *to* __main__ are never interesting. omitting them means
97             # that main floats to the top of the page
98             if self._debug :  print "\t\ttarget is __main__"
99             return False
100         elif self._invisible_path(path) == True and module_name != '__main__' :
101             # the path for __main__ seems to be it's filename
102             if self._debug :  print "\t\tinvisible module parent path", path
103             return False # don't draw edges from invisible path modules
104         elif self._link_outside_visited_nodes == False \
105                 and self._invisible_path(dpath) == True :
106             if self._debug :  print "\t\tinvisible module path", dpath
107             return False # don't draw edges to invisible path modules            
108         elif dtype == imp.PKG_DIRECTORY:
109             # don't draw edges to packages.
110             if self._debug :  print "\t\tpackage"
111             return False
112         return True
113     def _invisible_name(self, mod_name) :
114         if mod_name in self._invisible_mods :
115             # nearly all modules use all of these... more or less.
116             # They add nothing to our diagram.
117             return True
118         return False
119     def _invisible_path(self, path) :
120         """
121         Paths are visible by default.  Adding a regexp to invisible_paths hides
122         matching paths, unless the path matches a regexp in visible_paths, in
123         which case it is again visible.
124         """
125         if path == None and self._ignore_builtins :
126             return True # ignore modules without paths (builtins, etc)
127         if (_pathmatch(self._invisible_paths, path)
128                 and not _pathmatch(self._visible_paths, path)):
129             return True
130         return False
131
132 class dotformat (object) :
133     def __init__(self, colored=True, hooks_instance=None) :
134         if hooks_instance != None :
135             self._hooks = hooks_instance
136         else :
137             self._hooks = hooks()
138         self._colored = colored
139     def header(self):
140         return ('digraph G {\n'
141                 #'  concentrate = true;\n'
142                 #'  ordering = out;\n'
143                 '  ranksep=1.0;\n'
144                 '  node [style=filled,fontname=Helvetica,fontsize=10];\n')
145     def footer(self):
146         return '}\n'
147     def module(self, mod_name, dep_dict, type, path) :
148         name = self._fix_name(mod_name)
149         a = []
150         if mod_name == '__main__' :
151             # the path for __main__ seems to be it's filename
152             a.append('label="%s"' % self._label(path))
153         else :
154             a.append('label="%s"' % self._label(mod_name))
155         if self._colored:
156             a.append('fillcolor="%s"' % self._color(mod_name,type))
157         else:
158             a.append('fillcolor=white')
159         if self._hooks._invisible_path(path):
160             # for printing `invisible' modules
161             a.append('peripheries=2')
162         return self._dot_node(name, a)
163     def edge(self, mod_name, dep_dict, type, path,
164              dep_name, dep_type, dep_path) :
165         name = self._fix_name(mod_name)
166         target = self._fix_name(dep_name)
167         a = []
168         weight = self._weight(mod_name,dep_name)
169         if weight!=1:
170             a.append('weight=%d' % weight)
171         length = self._alien(mod_name)
172         if length:
173             a.append('minlen=%d' % length)
174         return self._dot_edge(name, target, a)
175
176     def _fix_name(self, mod_name):
177         # Convert a module name to a syntactically correct node name
178         return mod_name.replace('.','_')    
179     def _label(self,s):
180         # Convert a module name to a formatted node label.
181         return '\\.\\n'.join(s.split('.'))
182     def _weight(self, mod_name, target_name):
183         # Return the weight of the dependency from a to b. Higher weights
184         # usually have shorter straighter edges. Return 1 if it has normal weight.
185         # A value of 4 is usually good for ensuring that a related pair of modules 
186         # are drawn next to each other.
187         #
188         if target_name.split('.')[-1].startswith('_'):
189             # A module that starts with an underscore. You need a special reason to
190             # import these (for example random imports _random), so draw them close
191             # together
192             return 4
193         return 1
194     def _alien(self, mod_name):
195         # Return non-zero if references to this module are strange, and should be drawn
196         # extra-long. the value defines the length, in rank. This is also good for putting some
197         # vertical space between seperate subsystems.
198         return 0
199     def _color(self, mod_name, type):
200         # Return the node color for this module name.
201         if type == imp.C_EXTENSION:
202             # make C extensions bluegreen
203             # bluegreen is at 180 deg, see http://en.wikipedia.org/wiki/Image:HueScale.svg
204             r,g,b = colorsys.hsv_to_rgb(180.0/360.0, .2, 1)
205             return '#%02x%02x%02x' % (r*255,g*255,b*255)
206         # Calculate a color systematically based on the hash of the module name. Modules in the
207         # same package have the same color. Unpackaged modules are grey
208         t = self._normalise_module_name_for_hash_coloring(mod_name,type)
209         return self._color_from_name(t)
210     def _normalise_module_name_for_hash_coloring(self,mod_name,type):
211         if type==imp.PKG_DIRECTORY:
212             return mod_name
213         else:
214             i = mod_name.rfind('.')
215             if i<0:
216                 return ''
217             else:
218                 return mod_name[:i]
219     def _color_from_name(self,name):
220         n = md5(name).digest()
221         hf = float(ord(n[0])+ord(n[1])*0xff)/0xffff
222         sf = float(ord(n[2]))/0xff
223         vf = float(ord(n[3]))/0xff
224         r,g,b = colorsys.hsv_to_rgb(hf, 0.3+0.6*sf, 0.8+0.2*vf)
225         return '#%02x%02x%02x' % (r*256,g*256,b*256)
226     
227     # abstract out most of the dot language for head and edge declarations
228     def _dot_node(self, name, attrs) :
229         string = '  %s' % self._fix_name(name)
230         string += self._attribute_string(attrs)
231         string += ';\n'
232         return string
233     def _dot_edge(self, source, target, attrs) :
234         string = '  %s -> %s' % (source, target)
235         string += self._attribute_string(attrs)
236         string += ';\n'
237         return string
238     def _attribute_string(self, attributes):
239         string = ''
240         if attributes:
241             string += ' [%s]' % (','.join(attributes))
242         return string
243
244 class dotformat_Cext (dotformat) :
245     # support for listing C-language extension code.
246     _visible_paths = VISIBLE_PATHS
247     def module(self, mod_name, dep_dict, type, path) :
248         name = self._fix_name(mod_name)
249         a = []
250         if mod_name == '__main__' :
251             # the path for __main__ seems to be it's filename
252             a.append('label="%s"' % self._label(path))
253         else :
254             a.append('label="%s"' % self._label(mod_name))
255         if self._colored:
256             a.append('fillcolor="%s"' % self._color(mod_name,type))
257         else:
258             a.append('fillcolor=white')
259         if self._hooks._invisible_path(path):
260             # for printing `invisible' modules
261             a.append('peripheries=2')
262         string = self._dot_node(name, a)
263         #print "type %s:\t%s\t(%s)" % (mod_name, type, imp.C_EXTENSION)
264         if type == imp.C_EXTENSION:
265             string += self._Cext_depend_dotstring(mod_name, path)
266         return string
267     def _Cext_depend_dotstring(self, mod_name, path) :
268         deps = self._Cext_depends(mod_name, path)
269         string = ""
270         for dep in deps :
271             edge_attrs = self._Cext_edge_attributes(mod_name, dep)
272             string += self._dot_node(dep, self._Cext_node_attributes(dep))
273             string += self._dot_edge(mod_name, dep, edge_attrs)
274         return string
275     def _Cext_depends(self, s, path):
276         "Return a list of dependencies for a shared object file"
277         # make sure the extension is a shared object file (sanity check)
278         ret = []
279         if path.find('.so') != len(path)-len('.so'):
280             return ret
281         for line in popen('ldd %s' % path, 'r') :
282             try: # ldd line:  soname [=> path] (address)
283                 soname = line.split('=>')[0].strip()
284                 sopath = line.split('=>')[1].split('(')[0].strip()
285             except IndexError:
286                 continue # irregular dependency (kernel?)
287             if _pathmatch(self._visible_paths, path) :
288                 ret.append(soname)
289         return ret
290
291     def _Cext_edge_attributes(self, mod_name, dep_name):
292         return [] # nothing for now...
293
294     def _Cext_node_attributes(self, dep_name):
295         a = []
296         a.append('label="%s"' % self._label(dep_name))
297         if self._colored:
298             a.append('fillcolor="%s"' % self._Cext_depcolor(dep_name))
299         else:
300             a.append('fillcolor=white')
301         return a
302
303     def _Cext_depcolor(self, dep_name):
304         # make extension dependencies green
305         r,g,b = colorsys.hsv_to_rgb(120.0/360.0, .2, 1) # green is at 120 deg, see http://en.wikipedia.org/wiki/Image:HueScale.svg
306         return '#%02x%02x%02x' % (r*255,g*255,b*255)
307
308
309
310
311 class pydepgraphdot (object) :
312     def __init__(self, hooks_instance=None, dotformat_instance=None) :
313         if dotformat_instance != None :
314             self._dotformat = dotformat_instance
315         else :
316             self._dotformat = dotformat()
317         if hooks_instance != None :
318             self._hooks = hooks_instance
319         else :
320             self._hooks = hooks()
321         self.reset()
322         self._debug=False
323
324     def render(self, ifile, root_module='__main__'):
325         depgraph,types,paths = self.get_data(ifile)
326
327         if root_module != None :
328             self.add_module_target(root_module)
329             
330         depgraph,type,paths = self.fill_missing_deps(depgraph, types, paths)
331
332         f = self.get_output_file()
333
334         f.write(self._dotformat.header())
335
336         while True :
337             if self._hooks.continue_test() == False :
338                 if self.debug :  print '\t\tcontinue_test() False'
339                 break
340             mod = self.next_module_target()
341             if mod == None :
342                 if self._debug :  print '\t\tout of modules'
343                 break # out of modules
344             # I don't know anything about the underlying implementation,
345             # but I assume `key in dict` is more efficient than `key in list`
346             # because dicts are inherently hashed.
347             # That's my excuse for passing around deps with dummy values.
348             deps = depgraph[mod]
349             type = types[mod]
350             path = paths[mod]
351             if self._hooks.visible_mod_test(mod, deps, type, path) == False :
352                 if self._debug :  print '\t\tinvisible module'
353                 continue
354             f.write(self._dotformat.module(mod, deps, type, path))
355             ds = deps.keys() # now we want a consistent ordering,
356             ds.sort()        # so pull out the keys and sort them
357             for d in ds :
358                 if self._hooks.follow_edge_test(mod, type, path,
359                                                 d, types[d], paths[d]) :
360                     if self._debug :  print '\t\tfollow to %s' % d
361                     #print "%s, %s, %s, %s, %s, %s, %s" % (mod, deps, type, path, d, types[d], paths[d]) 
362                     f.write(self._dotformat.edge(mod, deps, type, path,
363                                                  d, types[d], paths[d]))
364                     self.add_module_target(d)
365                 else :
366                     if self._debug :  print "\t\tdon't follow to %s" % d
367
368         f.write(self._dotformat.footer())
369
370     # data processing methods (input, output, checking)
371     def get_data(self, ifile):
372         t = eval(ifile.read())
373         return t['depgraph'],t['types'],t['paths']
374     def get_output_file(self):
375         return sys.stdout
376     def fill_missing_deps(self, depgraph, types, paths) :
377         # normalize our input data
378         for mod,deps in depgraph.items(): # module and it's dependencies
379             for dep in deps.keys():
380                 if not depgraph.has_key(dep):
381                     # if dep not listed in depgraph somehow...
382                     # add it in, with no further dependencies
383                     depgraph[dep] = {}        
384                     # add dummies to types and paths too, if neccessary
385                     if not dep in types :
386                         types[dep] = None
387                     if not dep in paths :
388                         paths[dep] = None
389                     if self._debug :
390                         print "Adding dummy entry for missing module '%s'" \
391                               % dep
392         return (depgraph, types, paths)
393
394     # keep a list of modules for a breadth-first search.
395     def reset(self) :
396         # create stacks of nodes for traversing the mesh
397         self._modules_todo = []
398         self._modules_entered = []
399     def add_module_target(self, target_module) :
400         if not target_module in self._modules_entered :
401             # add to the back of the stack
402             if self._debug :  print '\tpush', target_module
403             self._modules_todo.append(target_module)
404             self._modules_entered.append(target_module)
405         # otherwise, it's already on the list, so don't worry about it.
406     def next_module_target(self) :
407         if len(self._modules_todo) > 0 :
408             if self._debug :  print '\tpop', self._modules_todo[0]
409             return self._modules_todo.pop(0) # remove from front of the list
410         else :
411             return None # no more modules! we're done.
412
413             
414 if __name__=='__main__':
415     from optparse import OptionParser
416
417     usage = '%prog [options]'
418     p = OptionParser(usage=usage, description=__doc__)
419     p.add_option('-m', '--mono', dest='color', default=True,
420                  action='store_false', help="Don't use color.")
421     p.add_option('-i', '--input', dest='input', default='-',
422                  help="Path to input file ('-' for stdin, %default).")
423     options,args = p.parse_args()
424
425     # Fancyness with shared hooks instance so we can do slick thinks like
426     # printing all modules just inside an invisible zone, since we'll need 
427     # the dotformatter to know which nodes are visible.
428     hk = hooks(link_outside_visited_nodes=False)
429     #hk._debug = True
430     dt = dotformat_Cext(colored=options.color, hooks_instance=hk)
431     py = pydepgraphdot(hooks_instance=hk, dotformat_instance=dt)
432     #py._debug = True
433
434     if options.input == '-':
435         ifile = sys.stdin
436     else:
437         ifile = open(options.input, 'r')
438
439     try:
440         py.render(ifile)
441     finally:
442         if options.input != '-':
443             ifile.close()