a little nicer wrt pep8
[sfa.git] / tools / depgraph2dot.py
1 #!/usr/bin/env python3
2 # Copyright 2004 Toby Dickenson
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject
10 # to the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be included
13 # in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
23
24 import sys
25 import getopt
26 import colorsys
27 import imp
28 import hashlib
29
30
31 class pydepgraphdot:
32
33     def main(self, argv):
34         opts, args = getopt.getopt(argv, '', ['mono'])
35         self.colored = 1
36         for o, v in opts:
37             if o == '--mono':
38                 self.colored = 0
39         self.render()
40
41     def fix(self, s):
42         # Convert a module name to a syntactically correct node name
43         return s.replace('.', '_')
44
45     def render(self):
46         p, t = self.get_data()
47
48         # normalise our input data
49         for k, d in list(p.items()):
50             for v in list(d.keys()):
51                 if v not in p:
52                     p[v] = {}
53
54         f = self.get_output_file()
55
56         f.write('digraph G {\n')
57         #f.write('concentrate = true;\n')
58         #f.write('ordering = out;\n')
59         f.write('ranksep=1.0;\n')
60         f.write('node [style=filled,fontname=Helvetica,fontsize=10];\n')
61         allkd = list(p.items())
62         allkd.sort()
63         for k, d in allkd:
64             tk = t.get(k)
65             if self.use(k, tk):
66                 allv = list(d.keys())
67                 allv.sort()
68                 for v in allv:
69                     tv = t.get(v)
70                     if self.use(v, tv) and not self.toocommon(v, tv):
71                         f.write('%s -> %s' % (self.fix(k), self.fix(v)))
72                         self.write_attributes(f, self.edge_attributes(k, v))
73                         f.write(';\n')
74                 f.write(self.fix(k))
75                 self.write_attributes(f, self.node_attributes(k, tk))
76                 f.write(';\n')
77         f.write('}\n')
78
79     def write_attributes(self, f, a):
80         if a:
81             f.write(' [')
82             f.write(','.join(a))
83             f.write(']')
84
85     def node_attributes(self, k, type):
86         a = []
87         a.append('label="%s"' % self.label(k))
88         if self.colored:
89             a.append('fillcolor="%s"' % self.color(k, type))
90         else:
91             a.append('fillcolor=white')
92         if self.toocommon(k, type):
93             a.append('peripheries=2')
94         return a
95
96     def edge_attributes(self, k, v):
97         a = []
98         weight = self.weight(k, v)
99         if weight != 1:
100             a.append('weight=%d' % weight)
101         length = self.alien(k, v)
102         if length:
103             a.append('minlen=%d' % length)
104         return a
105
106     def get_data(self):
107         t = eval(sys.stdin.read())
108         return t['depgraph'], t['types']
109
110     def get_output_file(self):
111         return sys.stdout
112
113     def use(self, s, type):
114         # Return true if this module is interesting and should be drawn. Return false
115         # if it should be completely omitted. This is a default policy - please
116         # override.
117         if s in ('os', 'sys', 'qt', 'time', '__future__', 'types', 're', 'string'):
118             # nearly all modules use all of these... more or less. They add nothing to
119             # our diagram.
120             return 0
121         if s.startswith('encodings.'):
122             return 0
123         if s == '__main__':
124             return 1
125         if self.toocommon(s, type):
126             # A module where we dont want to draw references _to_. Dot doesnt handle these
127             # well, so it is probably best to not draw them at all.
128             return 0
129         return 1
130
131     def toocommon(self, s, type):
132         # Return true if references to this module are uninteresting. Such references
133         # do not get drawn. This is a default policy - please override.
134         #
135         if s == '__main__':
136             # references *to* __main__ are never interesting. omitting them means
137             # that main floats to the top of the page
138             return 1
139         if type == imp.PKG_DIRECTORY:
140             # dont draw references to packages.
141             return 1
142         return 0
143
144     def weight(self, a, b):
145         # Return the weight of the dependency from a to b. Higher weights
146         # usually have shorter straighter edges. Return 1 if it has normal weight.
147         # A value of 4 is usually good for ensuring that a related pair of modules
148         # are drawn next to each other. This is a default policy - please override.
149         #
150         if b.split('.')[-1].startswith('_'):
151             # A module that starts with an underscore. You need a special reason to
152             # import these (for example random imports _random), so draw them close
153             # together
154             return 4
155         return 1
156
157     def alien(self, a, b):
158         # Return non-zero if references to this module are strange, and should be drawn
159         # extra-long. the value defines the length, in rank. This is also good for putting some
160         # vertical space between seperate subsystems. This is a default policy - please override.
161         #
162         return 0
163
164     def label(self, s):
165         # Convert a module name to a formatted node label. This is a default policy - please override.
166         #
167         return '\\.\\n'.join(s.split('.'))
168
169     def color(self, s, type):
170         # Return the node color for this module name. This is a default policy - please override.
171         #
172         # Calculate a color systematically based on the hash of the module name. Modules in the
173         # same package have the same color. Unpackaged modules are grey
174         t = self.normalise_module_name_for_hash_coloring(s, type)
175         return self.color_from_name(t)
176
177     def normalise_module_name_for_hash_coloring(self, s, type):
178         if type == imp.PKG_DIRECTORY:
179             return s
180         else:
181             i = s.rfind('.')
182             if i < 0:
183                 return ''
184             else:
185                 return s[:i]
186
187     def color_from_name(self, name):
188         n = hashlib.md5(name).digest()
189         hf = float(ord(n[0]) + ord(n[1]) * 0xff) / 0xffff
190         sf = float(ord(n[2])) / 0xff
191         vf = float(ord(n[3])) / 0xff
192         r, g, b = colorsys.hsv_to_rgb(hf, 0.3 + 0.6 * sf, 0.8 + 0.2 * vf)
193         return '#%02x%02x%02x' % (r * 256, g * 256, b * 256)
194
195
196 def main():
197     pydepgraphdot().main(sys.argv[1:])
198
199 if __name__ == '__main__':
200     main()