Drawing inheritance diagrams with "Dot"
=================================================
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
draw automatically generated diagrams. As an
example, I will describe here a short recipe which
produce 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. Here is the code::
#
"""
Draw inheritance hierarchies via Dot (http://www.graphviz.org/)
Author: M. 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
#
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:
- *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.
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:
>>> 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)
.. image:: A.png
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.
Examples with both old style and new style classes
----------------------------------------------------------------
This recipe can be convenient for documenting programs,
but it can also be used as a learning tool,
when you are studying a large framework with
a complicated class hierarchy. In this case
there could be a problem, since by design the
recipe works with new style only, whereas
most of legacy Python code works with old-style
classes; however it is trivial to convert
an old-style class to new-style, simply
by composing it with the ``object`` class.
For instance, let me show what can be learned about the
ScrolledText widget of Tkinter:
>>> from MROgraph import MROgraph
>>> import ScrolledText
>>> class ScrolledText_(ScrolledText.ScrolledText,object):
... "Creates a new style class from ScrolledText.ScrolledText"
>>> g=MROgraph(ScrolledText_,
... filename="ScrolledText.png",setup='size="5,5"; '+colors)
.. image:: ScrolledText.png
We see here that Tkinter makes use of the mixins Pack, Place and Grid
(in this order) which combined with BaseWidget generate a Widget. We
also see that Misc has the precedence over Pack, Place and Grid.
Finally, all Tkinter classes are old-style classes, instances
of the metaclass ``classobj``.
I should warn the reader that the MRO for new style and old style
classes is different, so there are old style hierarchies which cannot
be converted to new style one. Here is an example:
>>> class A: pass # notice, old style!
...
>>> class B(A): pass
...
>>> class C(A,B): pass
...
>>> class D(C,object): pass
...
>>> g=MROgraph(D,filename="D.png",setup=colors,caption=True)
...
.. image:: D.png
This only works since A,B and C are old style, since the triangle
configuration is forbidden for new style classes by the C3 MRO.
If you try to derive A from object, you will get a MRO error.
On the other hand, there are old style hierarchies which can be
converted to new style one, but behave differently after the
conversion. Consider for instance the following old style hierarchy:
>>> class A: a='A' # makes A,B,C and D old style
...
>>> class B(A): pass
...
>>> class C(A): a='C'
...
>>> class D(B,C): pass
...
>>> class E(D,object): pass # makes E new style
...
>>> g=MROgraph(E,filename='oldstyle.png',setup=colors,caption=True)
.. image:: oldstyle.png
In this example A comes before C,
>>> E.a
...
'A'
whereas for new style classes it would be the opposite:
>>> class A(object): a='A' # makes the whole hierarchy new style
...
>>> class B(A): pass
...
>>> class C(A): a='C'
...
>>> class D(B,C): pass
...
>>> class E(D): pass
...
>>> g=MROgraph(E,filename='newstyle.png',setup=colors,caption=True)
.. image:: newstyle.png
Here C has the precedence over A:
>>> E.a
...
'C'