Dot is a very nice graph description language developed at MIT and available for free at http://www.graphviz.org/ . Combined with Python, it makes an ideal tool to automatically generate diagrams. I will describe here a short recipe which produces beautiful inheritance diagrams for Python classes (and metaclasses too). In particular the recipe allows to display the MRO (Method Resolution Order) for complicate inheritance hierarchies.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | #<MROgraph.py>
"""
Draw inheritance hierarchies via Dot (http://www.graphviz.org/)
Author: Michele Simionato
E-mail: mis6@pitt.edu
Date: August 2003
License: Python-like
Requires: Python 2.3, dot, standard Unix tools
"""
import os,itertools
PSVIEWER='gv' # you may change these with
PNGVIEWER='kview' # your preferred viewers
PSFONT='Times' # you may change these too
PNGFONT='Courier' # on my system PNGFONT=Times does not work
def if_(cond,e1,e2=''):
"Ternary operator would be"
if cond: return e1
else: return e2
def MRO(cls):
"Returns the MRO of cls as a text"
out=["MRO of %s:" % cls.__name__]
for counter,c in enumerate(cls.__mro__):
name=c.__name__
bases=','.join([b.__name__ for b in c.__bases__])
s=" %s - %s(%s)" % (counter,name,bases)
if type(c) is not type: s+="[%s]" % type(c).__name__
out.append(s)
return '\n'.join(out)
class MROgraph(object):
def __init__(self,*classes,**options):
"Generates the MRO graph of a set of given classes."
if not classes: raise "Missing class argument!"
filename=options.get('filename',"MRO_of_%s.ps" % classes[0].__name__)
self.labels=options.get('labels',2)
caption=options.get('caption',False)
setup=options.get('setup','')
name,dotformat=os.path.splitext(filename)
format=dotformat[1:]
fontopt="fontname="+if_(format=='ps',PSFONT,PNGFONT)
nodeopt=' node [%s];\n' % fontopt
edgeopt=' edge [%s];\n' % fontopt
viewer=if_(format=='ps',PSVIEWER,PNGVIEWER)
self.textrepr='\n'.join([MRO(cls) for cls in classes])
caption=if_(caption,
'caption [shape=box,label="%s\n",fontsize=9];'
% self.textrepr).replace('\n','\\l')
setupcode=nodeopt+edgeopt+caption+'\n'+setup+'\n'
codeiter=itertools.chain(*[self.genMROcode(cls) for cls in classes])
self.dotcode='digraph %s{\n%s%s}' % (
name,setupcode,'\n'.join(codeiter))
os.system("echo '%s' | dot -T%s > %s; %s %s&" %
(self.dotcode,format,filename,viewer,filename))
def genMROcode(self,cls):
"Generates the dot code for the MRO of a given class"
for mroindex,c in enumerate(cls.__mro__):
name=c.__name__
manyparents=len(c.__bases__) > 1
if c.__bases__:
yield ''.join([
' edge [style=solid]; %s -> %s %s;\n' % (
b.__name__,name,if_(manyparents and self.labels==2,
'[label="%s"]' % (i+1)))
for i,b in enumerate(c.__bases__)])
if manyparents:
yield " {rank=same; %s}\n" % ''.join([
'"%s"; ' % b.__name__ for b in c.__bases__])
number=if_(self.labels,"%s-" % mroindex)
label='label="%s"' % (number+name)
option=if_(issubclass(cls,type), # if cls is a metaclass
'[%s]' % label,
'[shape=box,%s]' % label)
yield(' %s %s;\n' % (name,option))
if type(c) is not type: # c has a custom metaclass
metaname=type(c).__name__
yield ' edge [style=dashed]; %s -> %s;' % (metaname,name)
def __repr__(self):
"Returns the Dot representation of the graph"
return self.dotcode
def __str__(self):
"Returns a text representation of the MRO"
return self.textrepr
def testHierarchy(**options):
class M(type): pass # metaclass
class F(object): pass
class E(object): pass
class D(object): pass
class G(object): __metaclass__=M
class C(F,D,G): pass
class B(E,D): pass
class A(B,C): pass
return MROgraph(A,M,**options)
if __name__=="__main__":
testHierarchy() # generates a postscript diagram of A and M hierarchies
#</MROgraph.py>
|
The recipe should work as it is on Linux systems (it may require
to customize the postscript and PNG viewers); Windows users
must work a bit and change the os.system
line.
The recipe may be customized and extended at your will; but since
I wanted the script to fit in one hundred lines I have restricted
the currently available customization to the following options:
<pre> - filename= sets the filename containing the picture;
labels= turns on/off the labeling of edges;
caption= turns on/off the insertion of a caption;
setup= allows the user to enter raw Dot code. </pre> By default, filename is equal to
MRO_of_.ps
and the picture is stored in a postscript format (you may want to change this). Dot recognizes many other formats; I only need the PNG format for graph to be inserted in Web pages, so the recipe currently only works for .ps and .png filename extensions, but it is trivial to add new formats. The option labels=0 makes no label appearing in the graph; labels=1 makes labels specifying the MRO order appearing in the graph; labels=2 makes additional labels specifying the ordering of parents to appear. This latter option (which is the default) is useful since Dot changes the order of the parents in order to draw a nicer picture. caption=True adds an explanatory caption to the diagram; the default is False, i.e. no caption is displayed. The setup option can be used to initialize the graph; for instance to change the colors, to fix a size (in inches) and an aspect ratio, to set the orientation, etc. Here is an example:
<pre>
>>> from MROgraph import testHierarchy
>>> colors='edge [color=blue]; node [color=red];'
>>> g=testHierarchy(filename='A.png', labels=1, caption=True,
... setup='size="8,6"; ratio=0.7; '+colors)
</pre>
If an unrecognized option is passed, it is simply ignored and
nothing happens: you may want to raise an error instead, but this
is up to you. Also, you may want to add more customizable options;
it is easy to change the code accordingly. The aim is not to
wrap all the Dot features, here.
Many additional examples (with pictures) can be found at http://www.phyast.pitt.edu/~micheles/python/drawMRO.html
Error with script. I tried to run this script by using the
test code given at http://www.phyast.pitt.edu/~micheles/python/drawMRO.html
D:\OpensourceApps>testMROGraph.py
D:\OpensourceApps\MROGraph.py:69: Warning: 'yield' will become a reserved keyword in the future
Traceback (most recent call last):
File "D:\OpensourceApps\testMROGraph.py", line 1, in ?
File "D:\OpensourceApps\MROGraph.py", line 69
SyntaxError: invalid syntax
-Anand
You need Python 2.3. Since you get an error with "yield", I guess you are using Python 2.2. It requires Python 2.3, as stated in the docstring.
Duplicate edges. For a MRO graph such as
MROgraph(C, D) will generate dot code with two lines that both read
and dot will draw two edges from A to B. If you prefer to not see duplicate edges in parts of the graph common to the MRO of more than one cls in classes, you can either filter duplicate lines out of the dot code
or tell dot to ignore duplicate edges by making it a "strict digraph".
I prefer the first since I've been saving the dot code to a file and this makes it slightly nicer to look at (eliminates duplicate nodes as well).