Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 103 lines
  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

3 comments

Anand 18 years, 3 months ago  # | flag

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 ?

from MROGraph import testHierarchy

File "D:\OpensourceApps\MROGraph.py", line 69

yield ''.join([\


       ^

SyntaxError: invalid syntax

-Anand

Michele Simionato (author) 18 years, 1 month ago  # | flag

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.

Stefan Wiechula 16 years, 3 months ago  # | flag

Duplicate edges. For a MRO graph such as

  A
  |
  B
 / \
C   D

MROgraph(C, D) will generate dot code with two lines that both read

edge [style=solid]; A -> B ;

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

#codeiter=itertools.chain(*[self.genMROcode(cls) for cls in classes])
codeiter = []
for line in itertools.chain(*[self.genMROcode(cls) for cls in classes]):
    if not line in codeiter:
        codeiter.append(line)

or tell dot to ignore duplicate edges by making it a "strict digraph".

self.dotcode='strict digraph %s{\n%s%s}' % ...

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).

Created by Michele Simionato on Sun, 3 Aug 2003 (PSF)
Python recipes (4591)
Michele Simionato's recipes (12)

Required Modules

  • (none specified)

Other Information and Tasks