3 # Copyright 2004 Toby Dickenson
4 # Copyright 2008-2010 W. Trevor King
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:
14 # The above copyright notice and this permission notice shall be included
15 # in all copies or substantial portions of the Software.
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.
25 import sys, getopt, colorsys, imp, md5, re, pprint
26 from os import popen, getuid # for finding C extension dependencies with system calls
27 from pwd import getpwuid
29 USER=getpwuid(getuid())[0] # get effective user name
31 INVISIBLE_MODS=('__future__','copy','doctest','glob','optparse','os','qt','re',
32 'StringIO','string','sys','textwrap','time','types','unittest')
33 INVISIBLE_PATHS=(r'.*',)
34 VISIBLE_PATHS=(r'.*%s.*' % USER,r'.*comedi.*')
36 def _pathmatch(regexp_tuple, path) :
37 "Check if a regexp in regexp tuple matches the string path"
38 for regexp in regexp_tuple :
39 if re.match(regexp, path) != None :
43 class hooks (object) :
45 Modules show up if visible_mod_test(...) == True.
49 invisible_mods=INVISIBLE_MODS,
50 invisible_paths=INVISIBLE_PATHS,
51 visible_paths=VISIBLE_PATHS,
52 link_outside_visited_nodes=True,
53 ignore_builtins=True) :
54 self._invisible_mods = invisible_mods
55 self._invisible_paths = invisible_paths
56 self._visible_paths = visible_paths
57 self._link_outside_visited_nodes = link_outside_visited_nodes
58 self._ignore_builtins = ignore_builtins
59 self._entered_bonus_nodes = {} # a dict of bonus nodes already printed
61 def continue_test(self) :
63 def visible_mod_test(self, mod_name, dep_dict, type, path,
64 check_external_link=True) :
66 Return true if this module is interesting and should be drawn.
67 Return false if it should be completely omitted.
69 if self._invisible_name(mod_name) == True :
70 if self._debug : print "\t\tinvisible module", mod_name
72 if self._link_outside_visited_nodes == False \
73 and check_external_link == True \
74 and mod_name != '__main__' :
75 if self.follow_edge_test('__main__', imp.PY_SOURCE, './dummy.py',
76 mod_name, type, path) == False:
77 return False # don't draw nodes we wouldn't visit
79 def follow_edge_test(self, module_name, type, path,
82 print "\ttesting edge from %s %s %s to %s %s %s" \
83 % (module_name, type, path, dname, dtype, dpath)
84 if self.visible_mod_test(dname, None, dtype, dpath,
85 check_external_link=False) == False :
86 if self._debug : print "\t\tinvisible target module"
87 return False # don't draw edges to invisible modules
88 elif dname == '__main__':
89 # references *to* __main__ are never interesting. omitting them means
90 # that main floats to the top of the page
91 if self._debug : print "\t\ttarget is __main__"
93 elif self._invisible_path(path) == True and module_name != '__main__' :
94 # the path for __main__ seems to be it's filename
95 if self._debug : print "\t\tinvisible module parent path", path
96 return False # don't draw edges from invisible path modules
97 elif self._link_outside_visited_nodes == False \
98 and self._invisible_path(dpath) == True :
99 if self._debug : print "\t\tinvisible module path", dpath
100 return False # don't draw edges to invisible path modules
101 elif dtype == imp.PKG_DIRECTORY:
102 # don't draw edges to packages.
103 if self._debug : print "\t\tpackage"
106 def _invisible_name(self, mod_name) :
107 if mod_name in self._invisible_mods :
108 # nearly all modules use all of these... more or less.
109 # They add nothing to our diagram.
112 def _invisible_path(self, path) :
114 Paths are visible by default. Adding a regexp to invisible_paths hides
115 matching paths, unless the path matches a regexp in visible_paths, in
116 which case it is again visible.
118 if path == None and self._ignore_builtins :
119 return True # ignore modules without paths (builtins, etc)
120 if (_pathmatch(self._invisible_paths, path)
121 and not _pathmatch(self._visible_paths, path)):
125 class dotformat (object) :
126 def __init__(self, colored=True, hooks_instance=None) :
127 if hooks_instance != None :
128 self._hooks = hooks_instance
130 self._hooks = hooks()
131 self._colored = colored
133 return ('digraph G {\n'
134 #' concentrate = true;\n'
135 #' ordering = out;\n'
137 ' node [style=filled,fontname=Helvetica,fontsize=10];\n')
140 def module(self, mod_name, dep_dict, type, path) :
141 name = self._fix_name(mod_name)
143 if mod_name == '__main__' :
144 # the path for __main__ seems to be it's filename
145 a.append('label="%s"' % self._label(path))
147 a.append('label="%s"' % self._label(mod_name))
149 a.append('fillcolor="%s"' % self._color(mod_name,type))
151 a.append('fillcolor=white')
152 if self._hooks._invisible_path(path):
153 # for printing `invisible' modules
154 a.append('peripheries=2')
155 return self._dot_node(name, a)
156 def edge(self, mod_name, dep_dict, type, path,
157 dep_name, dep_type, dep_path) :
158 name = self._fix_name(mod_name)
159 target = self._fix_name(dep_name)
161 weight = self._weight(mod_name,dep_name)
163 a.append('weight=%d' % weight)
164 length = self._alien(mod_name)
166 a.append('minlen=%d' % length)
167 return self._dot_edge(name, target, a)
169 def _fix_name(self, mod_name):
170 # Convert a module name to a syntactically correct node name
171 return mod_name.replace('.','_')
173 # Convert a module name to a formatted node label.
174 return '\\.\\n'.join(s.split('.'))
175 def _weight(self, mod_name, target_name):
176 # Return the weight of the dependency from a to b. Higher weights
177 # usually have shorter straighter edges. Return 1 if it has normal weight.
178 # A value of 4 is usually good for ensuring that a related pair of modules
179 # are drawn next to each other.
181 if target_name.split('.')[-1].startswith('_'):
182 # A module that starts with an underscore. You need a special reason to
183 # import these (for example random imports _random), so draw them close
187 def _alien(self, mod_name):
188 # Return non-zero if references to this module are strange, and should be drawn
189 # extra-long. the value defines the length, in rank. This is also good for putting some
190 # vertical space between seperate subsystems.
192 def _color(self, mod_name, type):
193 # Return the node color for this module name.
194 if type == imp.C_EXTENSION:
195 # make C extensions bluegreen
196 # bluegreen is at 180 deg, see http://en.wikipedia.org/wiki/Image:HueScale.svg
197 r,g,b = colorsys.hsv_to_rgb(180.0/360.0, .2, 1)
198 return '#%02x%02x%02x' % (r*255,g*255,b*255)
199 # Calculate a color systematically based on the hash of the module name. Modules in the
200 # same package have the same color. Unpackaged modules are grey
201 t = self._normalise_module_name_for_hash_coloring(mod_name,type)
202 return self._color_from_name(t)
203 def _normalise_module_name_for_hash_coloring(self,mod_name,type):
204 if type==imp.PKG_DIRECTORY:
207 i = mod_name.rfind('.')
212 def _color_from_name(self,name):
213 n = md5.md5(name).digest()
214 hf = float(ord(n[0])+ord(n[1])*0xff)/0xffff
215 sf = float(ord(n[2]))/0xff
216 vf = float(ord(n[3]))/0xff
217 r,g,b = colorsys.hsv_to_rgb(hf, 0.3+0.6*sf, 0.8+0.2*vf)
218 return '#%02x%02x%02x' % (r*256,g*256,b*256)
220 # abstract out most of the dot language for head and edge declarations
221 def _dot_node(self, name, attrs) :
222 string = ' %s' % self._fix_name(name)
223 string += self._attribute_string(attrs)
226 def _dot_edge(self, source, target, attrs) :
227 string = ' %s -> %s' % (source, target)
228 string += self._attribute_string(attrs)
231 def _attribute_string(self, attributes):
234 string += ' [%s]' % (','.join(attributes))
237 class dotformat_Cext (dotformat) :
238 # support for listing C-language extension code.
239 _visible_paths = VISIBLE_PATHS
240 def module(self, mod_name, dep_dict, type, path) :
241 name = self._fix_name(mod_name)
243 if mod_name == '__main__' :
244 # the path for __main__ seems to be it's filename
245 a.append('label="%s"' % self._label(path))
247 a.append('label="%s"' % self._label(mod_name))
249 a.append('fillcolor="%s"' % self._color(mod_name,type))
251 a.append('fillcolor=white')
252 if self._hooks._invisible_path(path):
253 # for printing `invisible' modules
254 a.append('peripheries=2')
255 string = self._dot_node(name, a)
256 #print "type %s:\t%s\t(%s)" % (mod_name, type, imp.C_EXTENSION)
257 if type == imp.C_EXTENSION:
258 string += self._Cext_depend_dotstring(mod_name, path)
260 def _Cext_depend_dotstring(self, mod_name, path) :
261 deps = self._Cext_depends(mod_name, path)
264 edge_attrs = self._Cext_edge_attributes(mod_name, dep)
265 string += self._dot_node(dep, self._Cext_node_attributes(dep))
266 string += self._dot_edge(mod_name, dep, edge_attrs)
268 def _Cext_depends(self, s, path):
269 "Return a list of dependencies for a shared object file"
270 # make sure the extension is a shared object file (sanity check)
272 if path.find('.so') != len(path)-len('.so'):
274 for line in popen('ldd %s' % path, 'r') :
275 try: # ldd line: soname [=> path] (address)
276 soname = line.split('=>')[0].strip()
277 sopath = line.split('=>')[1].split('(')[0].strip()
279 continue # irregular dependency (kernel?)
280 if _pathmatch(self._visible_paths, path) :
284 def _Cext_edge_attributes(self, mod_name, dep_name):
285 return [] # nothing for now...
287 def _Cext_node_attributes(self, dep_name):
289 a.append('label="%s"' % self._label(dep_name))
291 a.append('fillcolor="%s"' % self._Cext_depcolor(dep_name))
293 a.append('fillcolor=white')
296 def _Cext_depcolor(self, dep_name):
297 # make extension dependencies green
298 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
299 return '#%02x%02x%02x' % (r*255,g*255,b*255)
304 class pydepgraphdot (object) :
305 def __init__(self, hooks_instance=None, dotformat_instance=None) :
306 if dotformat_instance != None :
307 self._dotformat = dotformat_instance
309 self._dotformat = dotformat()
310 if hooks_instance != None :
311 self._hooks = hooks_instance
313 self._hooks = hooks()
317 def render(self, root_module='__main__'):
318 depgraph,types,paths = self.get_data()
320 if root_module != None :
321 self.add_module_target(root_module)
323 depgraph,type,paths = self.fill_missing_deps(depgraph, types, paths)
325 f = self.get_output_file()
327 f.write(self._dotformat.header())
330 if self._hooks.continue_test() == False :
331 if self.debug : print '\t\tcontinue_test() False'
333 mod = self.next_module_target()
335 if self._debug : print '\t\tout of modules'
336 break # out of modules
337 # I don't know anything about the underlying implementation,
338 # but I assume `key in dict` is more efficient than `key in list`
339 # because dicts are inherently hashed.
340 # That's my excuse for passing around deps with dummy values.
344 if self._hooks.visible_mod_test(mod, deps, type, path) == False :
345 if self._debug : print '\t\tinvisible module'
347 f.write(self._dotformat.module(mod, deps, type, path))
348 ds = deps.keys() # now we want a consistent ordering,
349 ds.sort() # so pull out the keys and sort them
351 if self._hooks.follow_edge_test(mod, type, path,
352 d, types[d], paths[d]) :
353 if self._debug : print '\t\tfollow to %s' % d
354 #print "%s, %s, %s, %s, %s, %s, %s" % (mod, deps, type, path, d, types[d], paths[d])
355 f.write(self._dotformat.edge(mod, deps, type, path,
356 d, types[d], paths[d]))
357 self.add_module_target(d)
359 if self._debug : print "\t\tdon't follow to %s" % d
361 f.write(self._dotformat.footer())
363 # data processing methods (input, output, checking)
365 t = eval(sys.stdin.read())
366 return t['depgraph'],t['types'],t['paths']
367 def get_output_file(self):
369 def fill_missing_deps(self, depgraph, types, paths) :
370 # normalize our input data
371 for mod,deps in depgraph.items(): # module and it's dependencies
372 for dep in deps.keys():
373 if not depgraph.has_key(dep):
374 # if dep not listed in depgraph somehow...
375 # add it in, with no further dependencies
377 # add dummies to types and paths too, if neccessary
378 if not dep in types :
380 if not dep in paths :
383 print "Adding dummy entry for missing module '%s'" \
385 return (depgraph, types, paths)
387 # keep a list of modules for a breadth-first search.
389 # create stacks of nodes for traversing the mesh
390 self._modules_todo = []
391 self._modules_entered = []
392 def add_module_target(self, target_module) :
393 if not target_module in self._modules_entered :
394 # add to the back of the stack
395 if self._debug : print '\tpush', target_module
396 self._modules_todo.append(target_module)
397 self._modules_entered.append(target_module)
398 # otherwise, it's already on the list, so don't worry about it.
399 def next_module_target(self) :
400 if len(self._modules_todo) > 0 :
401 if self._debug : print '\tpop', self._modules_todo[0]
402 return self._modules_todo.pop(0) # remove from front of the list
404 return None # no more modules! we're done.
408 opts,args = getopt.getopt(sys.argv,'',['mono'])
414 # Fancyness with shared hooks instance so we can do slick thinks like
415 # printing all modules just inside an invisible zone, since we'll need
416 # the dotformatter to know which nodes are visible.
417 hk = hooks(link_outside_visited_nodes=False)
419 dt = dotformat_Cext(colored=colored, hooks_instance=hk)
420 py = pydepgraphdot(hooks_instance=hk, dotformat_instance=dt)
424 if __name__=='__main__':