1# -*- coding: Latin-1 -*-
2"""Graphviz's dot language Python interface.
3
4This module provides with a full interface to create handle modify
5and process graphs in Graphviz's dot language.
6
7References:
8
9pydot Homepage: http://code.google.com/p/pydot/
10Graphviz:       http://www.graphviz.org/
11DOT Language:   http://www.graphviz.org/doc/info/lang.html
12
13Programmed and tested with Graphviz 2.26.3 and Python 2.6 on OSX 10.6.4
14
15Copyright (c) 2005-2011 Ero Carrera <ero.carrera@gmail.com>
16
17Distributed under MIT license [http://opensource.org/licenses/mit-license.html].
18"""
19
20__revision__ = "$LastChangedRevision: 28 $"
21__author__ = 'Ero Carrera'
22__version__ = '1.0.%d' % int( __revision__[21:-2] )
23__license__ = 'MIT'
24
25import os
26import re
27import subprocess
28import tempfile
29import copy
30'''
31try:
32    import dot_parser
33except Exception, e:
34    print "Couldn't import dot_parser, loading of dot files will not be possible."
35'''
36
37
38GRAPH_ATTRIBUTES = set( ['Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor',
39    'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound',
40    'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints',
41    'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames',
42    'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc',
43    'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap',
44    'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist',
45    'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit',
46    'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap',
47    'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir',
48    'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
49    'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep',
50    'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start',
51    'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin',
52    # for subgraphs
53    'rank' ] )
54
55
56EDGE_ATTRIBUTES = set( ['URL', 'arrowhead', 'arrowsize', 'arrowtail',
57    'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
58    'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor',
59    'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel',
60    'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label',
61    'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor',
62    'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget',
63    'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen',
64    'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes',
65    'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport',
66    'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight',
67    'rank' ] )
68
69
70NODE_ATTRIBUTES = set( ['URL', 'color', 'colorscheme', 'comment',
71    'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname',
72    'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label',
73    'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth',
74    'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
75    'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style',
76    'target', 'tooltip', 'vertices', 'width', 'z',
77    # The following are attributes dot2tex
78    'texlbl',  'texmode' ] )
79
80
81CLUSTER_ATTRIBUTES = set( ['K', 'URL', 'bgcolor', 'color', 'colorscheme',
82    'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
83    'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor',
84    'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip'] )
85
86
87#
88# Extented version of ASPN's Python Cookbook Recipe:
89# Frozen dictionaries.
90# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/414283
91#
92# This version freezes dictionaries used as values within dictionaries.
93#
94class frozendict(dict):
95    def _blocked_attribute(obj):
96        raise AttributeError, "A frozendict cannot be modified."
97    _blocked_attribute = property(_blocked_attribute)
98
99    __delitem__ = __setitem__ = clear = _blocked_attribute
100    pop = popitem = setdefault = update = _blocked_attribute
101
102    def __new__(cls, *args, **kw):
103        new = dict.__new__(cls)
104
105        args_ = []
106        for arg in args:
107            if isinstance(arg, dict):
108                arg = copy.copy(arg)
109                for k, v in arg.iteritems():
110                    if isinstance(v, frozendict):
111                        arg[k] = v
112                    elif isinstance(v, dict):
113                        arg[k] = frozendict(v)
114                    elif isinstance(v, list):
115                        v_ = list()
116                        for elm in v:
117                            if isinstance(elm, dict):
118                                v_.append( frozendict(elm) )
119                            else:
120                                v_.append( elm )
121                        arg[k] = tuple(v_)
122                args_.append( arg )
123            else:
124                args_.append( arg )
125
126        dict.__init__(new, *args_, **kw)
127        return new
128
129    def __init__(self, *args, **kw):
130        pass
131
132    def __hash__(self):
133        try:
134            return self._cached_hash
135        except AttributeError:
136            h = self._cached_hash = hash(tuple(sorted(self.iteritems())))
137            return h
138
139    def __repr__(self):
140        return "frozendict(%s)" % dict.__repr__(self)
141
142
143dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
144
145id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE)
146id_re_alpha_nums_with_ports = re.compile('^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE)
147id_re_num = re.compile('^[0-9,]+$', re.UNICODE)
148id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE)
149id_re_dbl_quoted = re.compile('^\".*\"$', re.S|re.UNICODE)
150id_re_html = re.compile('^<.*>$', re.S|re.UNICODE)
151
152
153def needs_quotes( s ):
154    """Checks whether a string is a dot language ID.
155
156    It will check whether the string is solely composed
157    by the characters allowed in an ID or not.
158    If the string is one of the reserved keywords it will
159    need quotes too but the user will need to add them
160    manually.
161    """
162
163    # If the name is a reserved keyword it will need quotes but pydot
164    # can't tell when it's being used as a keyword or when it's simply
165    # a name. Hence the user needs to supply the quotes when an element
166    # would use a reserved keyword as name. This function will return
167    # false indicating that a keyword string, if provided as-is, won't
168    # need quotes.
169    if s in dot_keywords:
170        return False
171
172    chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0]
173    if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s):
174        return True
175
176    for test_re in [id_re_alpha_nums, id_re_num, id_re_dbl_quoted, id_re_html, id_re_alpha_nums_with_ports]:
177        if test_re.match(s):
178            return False
179
180    m = id_re_with_port.match(s)
181    if m:
182        return needs_quotes(m.group(1)) or needs_quotes(m.group(2))
183
184    return True
185
186
187def quote_if_necessary(s):
188
189    if isinstance(s, bool):
190        if s is True:
191            return 'True'
192        return 'False'
193
194    if not isinstance( s, basestring ):
195        return s
196
197    if not s:
198        return s
199
200    if needs_quotes(s):
201        replace = {'"'  : r'\"',
202                   "\n" : r'\n',
203                   "\r" : r'\r'}
204        for (a,b) in replace.items():
205            s = s.replace(a, b)
206
207        return '"' + s + '"'
208
209    return s
210
211
212
213def graph_from_dot_data(data):
214    """Load graph as defined by data in DOT format.
215
216    The data is assumed to be in DOT format. It will
217    be parsed and a Dot class will be returned,
218    representing the graph.
219    """
220
221    return dot_parser.parse_dot_data(data)
222
223
224def graph_from_dot_file(path):
225    """Load graph as defined by a DOT file.
226
227    The file is assumed to be in DOT format. It will
228    be loaded, parsed and a Dot class will be returned,
229    representing the graph.
230    """
231
232    fd = file(path, 'rb')
233    data = fd.read()
234    fd.close()
235
236    return graph_from_dot_data(data)
237
238
239
240def graph_from_edges(edge_list, node_prefix='', directed=False):
241    """Creates a basic graph out of an edge list.
242
243    The edge list has to be a list of tuples representing
244    the nodes connected by the edge.
245    The values can be anything: bool, int, float, str.
246
247    If the graph is undirected by default, it is only
248    calculated from one of the symmetric halves of the matrix.
249    """
250
251    if directed:
252        graph = Dot(graph_type='digraph')
253
254    else:
255        graph = Dot(graph_type='graph')
256
257    for edge in edge_list:
258
259        if isinstance(edge[0], str):
260            src = node_prefix + edge[0]
261        else:
262            src = node_prefix + str(edge[0])
263
264        if isinstance(edge[1], str):
265            dst = node_prefix + edge[1]
266        else:
267            dst = node_prefix + str(edge[1])
268
269        e = Edge( src, dst )
270        graph.add_edge(e)
271
272    return graph
273
274
275def graph_from_adjacency_matrix(matrix, node_prefix= u'', directed=False):
276    """Creates a basic graph out of an adjacency matrix.
277
278    The matrix has to be a list of rows of values
279    representing an adjacency matrix.
280    The values can be anything: bool, int, float, as long
281    as they can evaluate to True or False.
282    """
283
284    node_orig = 1
285
286    if directed:
287        graph = Dot(graph_type='digraph')
288    else:
289        graph = Dot(graph_type='graph')
290
291    for row in matrix:
292        if not directed:
293            skip = matrix.index(row)
294            r = row[skip:]
295        else:
296            skip = 0
297            r = row
298        node_dest = skip+1
299
300        for e in r:
301            if e:
302                graph.add_edge(
303                    Edge( node_prefix + node_orig,
304                        node_prefix + node_dest) )
305            node_dest += 1
306        node_orig += 1
307
308    return graph
309
310
311
312def graph_from_incidence_matrix(matrix, node_prefix='', directed=False):
313    """Creates a basic graph out of an incidence matrix.
314
315    The matrix has to be a list of rows of values
316    representing an incidence matrix.
317    The values can be anything: bool, int, float, as long
318    as they can evaluate to True or False.
319    """
320
321    node_orig = 1
322
323    if directed:
324        graph = Dot(graph_type='digraph')
325    else:
326        graph = Dot(graph_type='graph')
327
328    for row in matrix:
329        nodes = []
330        c = 1
331
332        for node in row:
333            if node:
334                nodes.append(c*node)
335            c += 1
336            nodes.sort()
337
338        if len(nodes) == 2:
339            graph.add_edge(
340                Edge( node_prefix + abs(nodes[0]),
341                    node_prefix + nodes[1] ))
342
343    if not directed:
344        graph.set_simplify(True)
345
346    return graph
347
348
349
350
351def __find_executables(path):
352    """Used by find_graphviz
353
354    path - single directory as a string
355
356    If any of the executables are found, it will return a dictionary
357    containing the program names as keys and their paths as values.
358
359    Otherwise returns None
360    """
361
362    success = False
363    progs = {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': '', 'sfdp': ''}
364
365    was_quoted = False
366    path = path.strip()
367    if path.startswith('"') and path.endswith('"'):
368        path = path[1:-1]
369        was_quoted =  True
370
371    if os.path.isdir(path) :
372
373        for prg in progs.iterkeys():
374
375            if progs[prg]:
376                continue
377
378            if os.path.exists( os.path.join(path, prg) ):
379
380                if was_quoted:
381                    progs[prg] = '"' + os.path.join(path, prg) + '"'
382                else:
383                    progs[prg] = os.path.join(path, prg)
384
385                success = True
386
387            elif os.path.exists( os.path.join(path, prg + '.exe') ):
388
389                if was_quoted:
390                    progs[prg] = '"' + os.path.join(path, prg + '.exe') + '"'
391                else:
392                    progs[prg] = os.path.join(path, prg + '.exe')
393
394                success = True
395
396    if success:
397
398        return progs
399
400    else:
401
402        return None
403
404
405
406# The multi-platform version of this 'find_graphviz' function was
407# contributed by Peter Cock
408#
409def find_graphviz():
410    """Locate Graphviz's executables in the system.
411
412    Tries three methods:
413
414    First: Windows Registry (Windows only)
415    This requires Mark Hammond's pywin32 is installed.
416
417    Secondly: Search the path
418    It will look for 'dot', 'twopi' and 'neato' in all the directories
419    specified in the PATH environment variable.
420
421    Thirdly: Default install location (Windows only)
422    It will look for 'dot', 'twopi' and 'neato' in the default install
423    location under the "Program Files" directory.
424
425    It will return a dictionary containing the program names as keys
426    and their paths as values.
427
428    If this fails, it returns None.
429    """
430
431    # Method 1 (Windows only)
432    #
433    if os.sys.platform == 'win32':
434
435        HKEY_LOCAL_MACHINE =    0x80000002
436        KEY_QUERY_VALUE =       0x0001
437
438        RegOpenKeyEx = None
439        RegQueryValueEx = None
440        RegCloseKey = None
441
442        try:
443            import win32api, win32con
444            RegOpenKeyEx = win32api.RegOpenKeyEx
445            RegQueryValueEx = win32api.RegQueryValueEx
446            RegCloseKey = win32api.RegCloseKey
447
448        except ImportError:
449            # Print a messaged suggesting they install these?
450            #
451            pass
452
453        try:
454            import ctypes
455
456            def RegOpenKeyEx(key, subkey, opt, sam):
457                result = ctypes.c_uint(0)
458                ctypes.windll.advapi32.RegOpenKeyExA(key, subkey, opt, sam, ctypes.byref(result))
459                return result.value
460
461            def RegQueryValueEx( hkey, valuename ):
462                data_type = ctypes.c_uint(0)
463                data_len = ctypes.c_uint(1024)
464                data = ctypes.create_string_buffer( 1024 )
465
466                res = ctypes.windll.advapi32.RegQueryValueExA(hkey, valuename, 0,
467                    ctypes.byref(data_type), data, ctypes.byref(data_len))
468
469                return data.value
470
471            RegCloseKey = ctypes.windll.advapi32.RegCloseKey
472
473        except ImportError:
474            # Print a messaged suggesting they install these?
475            #
476            pass
477
478        if RegOpenKeyEx is not None:
479
480            # Get the GraphViz install path from the registry
481            #
482            hkey = None
483            potentialKeys = [
484                "SOFTWARE\\ATT\\Graphviz",
485                "SOFTWARE\\AT&T Research Labs\\Graphviz",
486            ]
487            for potentialKey in potentialKeys:
488
489                try:
490                    hkey = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
491                        potentialKey, 0, KEY_QUERY_VALUE )
492
493                    if hkey is not None:
494                        path = RegQueryValueEx( hkey, "InstallPath" )
495                        RegCloseKey( hkey )
496
497                        # The regitry variable might exist, left by old installations
498                        # but with no value, in those cases we keep searching...
499                        if not path:
500                            continue
501
502                        # Now append the "bin" subdirectory:
503                        #
504                        path = os.path.join(path, "bin")
505                        progs = __find_executables(path)
506                        if progs is not None :
507                            #print "Used Windows registry"
508                            return progs
509
510                except Exception, excp:
511                    #raise excp
512                    pass
513                else:
514                    break
515
516
517
518    # Method 2 (Linux, Windows etc)
519    #
520    if os.environ.has_key('PATH'):
521
522        for path in os.environ['PATH'].split(os.pathsep):
523            progs = __find_executables(path)
524            if progs is not None :
525                #print "Used path"
526                return progs
527
528    # Method 3 (Windows only)
529    #
530    if os.sys.platform == 'win32':
531
532        # Try and work out the equivalent of "C:\Program Files" on this
533        # machine (might be on drive D:, or in a different language)
534        #
535
536        if os.environ.has_key('PROGRAMFILES'):
537
538            # Note, we could also use the win32api to get this
539            # information, but win32api may not be installed.
540
541            path = os.path.join(os.environ['PROGRAMFILES'], 'ATT', 'GraphViz', 'bin')
542
543        else:
544
545            #Just in case, try the default...
546            path = r"C:\Program Files\att\Graphviz\bin"
547
548        progs = __find_executables(path)
549
550        if progs is not None :
551
552            #print "Used default install location"
553            return progs
554
555
556    for path in (
557        '/usr/bin', '/usr/local/bin',
558        '/opt/local/bin',
559        '/opt/bin', '/sw/bin', '/usr/share',
560        '/Applications/Graphviz.app/Contents/MacOS/' ):
561
562        progs = __find_executables(path)
563        if progs is not None :
564            #print "Used path"
565            return progs
566
567    # Failed to find GraphViz
568    #
569    return None
570
571
572class Common:
573    """Common information to several classes.
574
575    Should not be directly used, several classes are derived from
576    this one.
577    """
578
579
580    def __getstate__(self):
581
582        dict = copy.copy(self.obj_dict)
583
584        return dict
585
586
587    def __setstate__(self, state):
588
589        self.obj_dict = state
590
591
592    def __get_attribute__(self, attr):
593        """Look for default attributes for this node"""
594
595        attr_val = self.obj_dict['attributes'].get(attr, None)
596
597        if attr_val is None:
598            # get the defaults for nodes/edges
599
600            default_node_name = self.obj_dict['type']
601
602            # The defaults for graphs are set on a node named 'graph'
603            if default_node_name in ('subgraph', 'digraph', 'cluster'):
604                default_node_name = 'graph'
605
606            g = self.get_parent_graph()
607            if g is not None:
608                defaults = g.get_node( default_node_name )
609            else:
610                return None
611
612            # Multiple defaults could be set by having repeated 'graph [...]'
613            # 'node [...]', 'edge [...]' statements. In such case, if the
614            # same attribute is set in different statements, only the first
615            # will be returned. In order to get all, one would call the
616            # get_*_defaults() methods and handle those. Or go node by node
617            # (of the ones specifying defaults) and modify the attributes
618            # individually.
619            #
620            if not isinstance(defaults, (list, tuple)):
621                defaults = [defaults]
622
623            for default in defaults:
624                attr_val = default.obj_dict['attributes'].get(attr, None)
625                if attr_val:
626                    return attr_val
627        else:
628            return attr_val
629
630        return None
631
632
633    def set_parent_graph(self, parent_graph):
634
635        self.obj_dict['parent_graph'] = parent_graph
636
637
638    def get_parent_graph(self):
639
640        return self.obj_dict.get('parent_graph', None)
641
642
643    def set(self, name, value):
644        """Set an attribute value by name.
645
646        Given an attribute 'name' it will set its value to 'value'.
647        There's always the possibility of using the methods:
648
649            set_'name'(value)
650
651        which are defined for all the existing attributes.
652        """
653
654        self.obj_dict['attributes'][name] = value
655
656
657    def get(self, name):
658        """Get an attribute value by name.
659
660        Given an attribute 'name' it will get its value.
661        There's always the possibility of using the methods:
662
663            get_'name'()
664
665        which are defined for all the existing attributes.
666        """
667
668        return self.obj_dict['attributes'].get(name, None)
669
670
671    def get_attributes(self):
672        """"""
673
674        return self.obj_dict['attributes']
675
676
677    def set_sequence(self, seq):
678
679        self.obj_dict['sequence'] = seq
680
681
682    def get_sequence(self):
683
684        return self.obj_dict['sequence']
685
686
687    def create_attribute_methods(self, obj_attributes):
688
689        #for attr in self.obj_dict['attributes']:
690        for attr in obj_attributes:
691
692            # Generate all the Setter methods.
693            #
694            self.__setattr__( 'set_'+attr, lambda x, a=attr : self.obj_dict['attributes'].__setitem__(a, x) )
695
696            # Generate all the Getter methods.
697            #
698            self.__setattr__('get_'+attr, lambda a=attr : self.__get_attribute__(a))
699
700
701
702class Error(Exception):
703    """General error handling class.
704    """
705    def __init__(self, value):
706        self.value = value
707    def __str__(self):
708        return self.value
709
710
711class InvocationException(Exception):
712    """To indicate that a ploblem occurred while running any of the GraphViz executables.
713    """
714    def __init__(self, value):
715        self.value = value
716    def __str__(self):
717        return self.value
718
719
720
721class Node(object, Common):
722    """A graph node.
723
724    This class represents a graph's node with all its attributes.
725
726    node(name, attribute=value, ...)
727
728    name: node's name
729
730    All the attributes defined in the Graphviz dot language should
731    be supported.
732    """
733
734    def __init__(self, name = '', obj_dict = None, **attrs):
735
736        #
737        # Nodes will take attributes of all other types because the defaults
738        # for any GraphViz object are dealt with as if they were Node definitions
739        #
740
741        if obj_dict is not None:
742
743            self.obj_dict = obj_dict
744
745        else:
746
747            self.obj_dict = dict()
748
749            # Copy the attributes
750            #
751            self.obj_dict[ 'attributes' ] = dict( attrs )
752            self.obj_dict[ 'type' ] = 'node'
753            self.obj_dict[ 'parent_graph' ] = None
754            self.obj_dict[ 'parent_node_list' ] = None
755            self.obj_dict[ 'sequence' ] = None
756            # Remove the compass point
757            #
758            port = None
759            if isinstance(name, basestring) and not name.startswith('"'):
760                idx = name.find(':')
761                if idx > 0 and idx+1 < len(name):
762                    name, port = name[:idx], name[idx:]
763
764            if isinstance(name, (long, int)):
765                name = str(name)
766
767            self.obj_dict['name'] = quote_if_necessary( name )
768            self.obj_dict['port'] = port
769
770        self.create_attribute_methods(NODE_ATTRIBUTES)
771
772
773
774    def set_name(self, node_name):
775        """Set the node's name."""
776
777        self.obj_dict['name'] = node_name
778
779
780    def get_name(self):
781        """Get the node's name."""
782
783        return self.obj_dict['name']
784
785
786    def get_port(self):
787        """Get the node's port."""
788
789        return self.obj_dict['port']
790
791
792    def add_style(self, style):
793
794        styles = self.obj_dict['attributes'].get('style', None)
795        if not styles and style:
796            styles = [ style ]
797        else:
798            styles = styles.split(',')
799            styles.append( style )
800
801        self.obj_dict['attributes']['style'] = ','.join( styles )
802
803
804    def to_string(self):
805        """Returns a string representation of the node in dot language.
806        """
807
808
809        # RMF: special case defaults for node, edge and graph properties.
810        #
811        node = quote_if_necessary(self.obj_dict['name'])
812
813        node_attr = list()
814
815        for attr, value in self.obj_dict['attributes'].iteritems():
816            if value is not None:
817                node_attr.append( '%s=%s' % (attr, quote_if_necessary(value) ) )
818            else:
819                node_attr.append( attr )
820
821        # No point in having nodes setting any defaults if the don't set
822        # any attributes...
823        #
824        if node in ('graph', 'node', 'edge') and len(node_attr) == 0:
825            return ''
826        node_attr = ', '.join(node_attr)
827
828        if node_attr:
829            node += ' [' + node_attr + ']'
830
831        return node + ';'
832
833
834
835class Edge(object,  Common ):
836    """A graph edge.
837
838    This class represents a graph's edge with all its attributes.
839
840    edge(src, dst, attribute=value, ...)
841
842    src: source node's name
843    dst: destination node's name
844
845    All the attributes defined in the Graphviz dot language should
846    be supported.
847
848 	Attributes can be set through the dynamically generated methods:
849
850     set_[attribute name], i.e. set_label, set_fontname
851
852    or directly by using the instance's special dictionary:
853
854     Edge.obj_dict['attributes'][attribute name], i.e.
855
856        edge_instance.obj_dict['attributes']['label']
857        edge_instance.obj_dict['attributes']['fontname']
858
859    """
860
861
862
863    def __init__(self, src='', dst='', obj_dict=None, **attrs):
864
865        if isinstance(src, (list, tuple)) and dst == '':
866            src, dst = src
867
868        if obj_dict is not None:
869
870            self.obj_dict = obj_dict
871
872        else:
873
874            self.obj_dict = dict()
875
876            # Copy the attributes
877            #
878            self.obj_dict[ 'attributes' ] = dict( attrs )
879            self.obj_dict[ 'type' ] = 'edge'
880            self.obj_dict[ 'parent_graph' ] = None
881            self.obj_dict[ 'parent_edge_list' ] = None
882            self.obj_dict[ 'sequence' ] = None
883
884            if isinstance(src, Node):
885                src = src.get_name()
886
887            if isinstance(dst, Node):
888                dst = dst.get_name()
889
890            points = ( quote_if_necessary( src) , quote_if_necessary( dst) )
891
892            self.obj_dict['points'] = points
893
894        self.create_attribute_methods(EDGE_ATTRIBUTES)
895
896
897    def get_source(self):
898        """Get the edges source node name."""
899
900        return self.obj_dict['points'][0]
901
902
903    def get_destination(self):
904        """Get the edge's destination node name."""
905
906        return self.obj_dict['points'][1]
907
908
909    def __hash__(self):
910
911         return hash( hash(self.get_source()) + hash(self.get_destination()) )
912
913
914    def __eq__(self, edge):
915        """Compare two edges.
916
917        If the parent graph is directed, arcs linking
918        node A to B are considered equal and A->B != B->A
919
920        If the parent graph is undirected, any edge
921        connecting two nodes is equal to any other
922        edge connecting the same nodes, A->B == B->A
923        """
924
925        if not isinstance(edge, Edge):
926            raise Error, "Can't compare and edge to a non-edge object."
927
928        if self.get_parent_graph().get_top_graph_type() == 'graph':
929
930            # If the graph is undirected, the edge has neither
931            # source nor destination.
932            #
933            if	( ( self.get_source() == edge.get_source() and self.get_destination() == edge.get_destination() ) or
934                ( edge.get_source() == self.get_destination() and edge.get_destination() == self.get_source() ) ):
935                return True
936
937        else:
938
939            if self.get_source()==edge.get_source() and self.get_destination()==edge.get_destination() :
940                return True
941
942        return False
943
944
945
946    def parse_node_ref(self, node_str):
947
948        if not isinstance(node_str, str):
949            return node_str
950
951        if node_str.startswith('"') and node_str.endswith('"'):
952
953            return node_str
954
955        node_port_idx = node_str.rfind(':')
956
957        if node_port_idx>0 and node_str[0]=='"' and node_str[node_port_idx-1]=='"':
958
959            return node_str
960
961        if node_port_idx>0:
962
963            a = node_str[:node_port_idx]
964            b = node_str[node_port_idx+1:]
965
966            node = quote_if_necessary(a)
967
968            node += ':'+quote_if_necessary(b)
969
970            return node
971
972        return node_str
973
974
975    def to_string(self):
976        """Returns a string representation of the edge in dot language.
977        """
978
979        src = self.parse_node_ref( self.get_source() )
980        dst = self.parse_node_ref( self.get_destination() )
981
982        if isinstance(src, frozendict):
983            edge = [ Subgraph(obj_dict=src).to_string() ]
984        elif isinstance(src, (int, long)):
985            edge = [ str(src) ]
986        else:
987            edge = [ src ]
988
989        if	(self.get_parent_graph() and
990            self.get_parent_graph().get_top_graph_type() and
991            self.get_parent_graph().get_top_graph_type() == 'digraph' ):
992
993            edge.append( '->' )
994
995        else:
996            edge.append( '--' )
997
998        if isinstance(dst, frozendict):
999            edge.append( Subgraph(obj_dict=dst).to_string() )
1000        elif isinstance(dst, (int, long)):
1001            edge.append( str(dst) )
1002        else:
1003            edge.append( dst )
1004
1005
1006        edge_attr = list()
1007
1008        for attr, value in self.obj_dict['attributes'].iteritems():
1009
1010            if value is not None:
1011                edge_attr.append( '%s=%s' % (attr, quote_if_necessary(value) ) )
1012            else:
1013                edge_attr.append( attr )
1014
1015        edge_attr = ', '.join(edge_attr)
1016
1017        if edge_attr:
1018            edge.append( ' [' + edge_attr + ']' )
1019
1020        return ' '.join(edge) + ';'
1021
1022
1023
1024
1025
1026class Graph(object, Common):
1027    """Class representing a graph in Graphviz's dot language.
1028
1029    This class implements the methods to work on a representation
1030    of a graph in Graphviz's dot language.
1031
1032    graph(  graph_name='G', graph_type='digraph',
1033        strict=False, suppress_disconnected=False, attribute=value, ...)
1034
1035    graph_name:
1036        the graph's name
1037    graph_type:
1038        can be 'graph' or 'digraph'
1039    suppress_disconnected:
1040        defaults to False, which will remove from the
1041        graph any disconnected nodes.
1042    simplify:
1043        if True it will avoid displaying equal edges, i.e.
1044        only one edge between two nodes. removing the
1045        duplicated ones.
1046
1047    All the attributes defined in the Graphviz dot language should
1048    be supported.
1049
1050    Attributes can be set through the dynamically generated methods:
1051
1052     set_[attribute name], i.e. set_size, set_fontname
1053
1054    or using the instance's attributes:
1055
1056     Graph.obj_dict['attributes'][attribute name], i.e.
1057
1058        graph_instance.obj_dict['attributes']['label']
1059        graph_instance.obj_dict['attributes']['fontname']
1060    """
1061
1062
1063    def __init__(self, graph_name='G', obj_dict=None, graph_type='digraph', strict=False,
1064        suppress_disconnected=False, simplify=False, **attrs):
1065
1066        if obj_dict is not None:
1067            self.obj_dict = obj_dict
1068
1069        else:
1070
1071            self.obj_dict = dict()
1072
1073            self.obj_dict['attributes'] = dict(attrs)
1074
1075            if graph_type not in ['graph', 'digraph']:
1076                raise Error, 'Invalid type "%s". Accepted graph types are: graph, digraph, subgraph' % graph_type
1077
1078
1079            self.obj_dict['name'] = quote_if_necessary(graph_name)
1080            self.obj_dict['type'] = graph_type
1081
1082            self.obj_dict['strict'] = strict
1083            self.obj_dict['suppress_disconnected'] = suppress_disconnected
1084            self.obj_dict['simplify'] = simplify
1085
1086            self.obj_dict['current_child_sequence'] = 1
1087            self.obj_dict['nodes'] = dict()
1088            self.obj_dict['edges'] = dict()
1089            self.obj_dict['subgraphs'] = dict()
1090
1091            self.set_parent_graph(self)
1092
1093
1094        self.create_attribute_methods(GRAPH_ATTRIBUTES)
1095
1096
1097    def get_graph_type(self):
1098
1099        return self.obj_dict['type']
1100
1101
1102    def get_top_graph_type(self):
1103
1104        parent = self
1105        while True:
1106            parent_ = parent.get_parent_graph()
1107            if parent_ == parent:
1108                break
1109            parent = parent_
1110
1111        return parent.obj_dict['type']
1112
1113
1114    def set_graph_defaults(self, **attrs):
1115
1116        self.add_node( Node('graph', **attrs) )
1117
1118
1119    def get_graph_defaults(self, **attrs):
1120
1121        graph_nodes = self.get_node('graph')
1122
1123        if isinstance( graph_nodes, (list, tuple)):
1124            return [ node.get_attributes() for node in graph_nodes ]
1125
1126        return graph_nodes.get_attributes()
1127
1128
1129
1130    def set_node_defaults(self, **attrs):
1131
1132        self.add_node( Node('node', **attrs) )
1133
1134
1135    def get_node_defaults(self, **attrs):
1136
1137
1138        graph_nodes = self.get_node('node')
1139
1140        if isinstance( graph_nodes, (list, tuple)):
1141            return [ node.get_attributes() for node in graph_nodes ]
1142
1143        return graph_nodes.get_attributes()
1144
1145
1146    def set_edge_defaults(self, **attrs):
1147
1148        self.add_node( Node('edge', **attrs) )
1149
1150
1151
1152    def get_edge_defaults(self, **attrs):
1153
1154        graph_nodes = self.get_node('edge')
1155
1156        if isinstance( graph_nodes, (list, tuple)):
1157            return [ node.get_attributes() for node in graph_nodes ]
1158
1159        return graph_nodes.get_attributes()
1160
1161
1162
1163    def set_simplify(self, simplify):
1164        """Set whether to simplify or not.
1165
1166        If True it will avoid displaying equal edges, i.e.
1167        only one edge between two nodes. removing the
1168        duplicated ones.
1169        """
1170
1171        self.obj_dict['simplify'] = simplify
1172
1173
1174
1175    def get_simplify(self):
1176        """Get whether to simplify or not.
1177
1178        Refer to set_simplify for more information.
1179        """
1180
1181        return self.obj_dict['simplify']
1182
1183
1184    def set_type(self, graph_type):
1185        """Set the graph's type, 'graph' or 'digraph'."""
1186
1187        self.obj_dict['type'] = graph_type
1188
1189
1190
1191    def get_type(self):
1192        """Get the graph's type, 'graph' or 'digraph'."""
1193
1194        return self.obj_dict['type']
1195
1196
1197
1198    def set_name(self, graph_name):
1199        """Set the graph's name."""
1200
1201        self.obj_dict['name'] = graph_name
1202
1203
1204
1205    def get_name(self):
1206        """Get the graph's name."""
1207
1208        return self.obj_dict['name']
1209
1210
1211
1212    def set_strict(self, val):
1213        """Set graph to 'strict' mode.
1214
1215        This option is only valid for top level graphs.
1216        """
1217
1218        self.obj_dict['strict'] = val
1219
1220
1221
1222    def get_strict(self, val):
1223        """Get graph's 'strict' mode (True, False).
1224
1225        This option is only valid for top level graphs.
1226        """
1227
1228        return self.obj_dict['strict']
1229
1230
1231
1232    def set_suppress_disconnected(self, val):
1233        """Suppress disconnected nodes in the output graph.
1234
1235        This option will skip nodes in the graph with no incoming or outgoing
1236        edges. This option works also for subgraphs and has effect only in the
1237        current graph/subgraph.
1238        """
1239
1240        self.obj_dict['suppress_disconnected'] = val
1241
1242
1243
1244    def get_suppress_disconnected(self, val):
1245        """Get if suppress disconnected is set.
1246
1247        Refer to set_suppress_disconnected for more information.
1248        """
1249
1250        return self.obj_dict['suppress_disconnected']
1251
1252
1253    def get_next_sequence_number(self):
1254
1255        seq = self.obj_dict['current_child_sequence']
1256
1257        self.obj_dict['current_child_sequence'] += 1
1258
1259        return seq
1260
1261
1262
1263    def add_node(self, graph_node):
1264        """Adds a node object to the graph.
1265
1266        It takes a node object as its only argument and returns
1267        None.
1268        """
1269
1270        if not isinstance(graph_node, Node):
1271            raise TypeError('add_node() received a non node class object: ' + str(graph_node))
1272
1273
1274        node = self.get_node(graph_node.get_name())
1275
1276        if not node:
1277
1278            self.obj_dict['nodes'][graph_node.get_name()] = [ graph_node.obj_dict ]
1279
1280            #self.node_dict[graph_node.get_name()] = graph_node.attributes
1281            graph_node.set_parent_graph(self.get_parent_graph())
1282
1283        else:
1284
1285            self.obj_dict['nodes'][graph_node.get_name()].append( graph_node.obj_dict )
1286
1287        graph_node.set_sequence(self.get_next_sequence_number())
1288
1289
1290
1291    def del_node(self, name, index=None):
1292        """Delete a node from the graph.
1293
1294        Given a node's name all node(s) with that same name
1295        will be deleted if 'index' is not specified or set
1296        to None.
1297        If there are several nodes with that same name and
1298        'index' is given, only the node in that position
1299        will be deleted.
1300
1301        'index' should be an integer specifying the position
1302        of the node to delete. If index is larger than the
1303        number of nodes with that name, no action is taken.
1304
1305        If nodes are deleted it returns True. If no action
1306        is taken it returns False.
1307        """
1308
1309        if isinstance(name, Node):
1310            name = name.get_name()
1311
1312        if self.obj_dict['nodes'].has_key(name):
1313
1314            if index is not None and index < len(self.obj_dict['nodes'][name]):
1315                del self.obj_dict['nodes'][name][index]
1316                return True
1317            else:
1318                del self.obj_dict['nodes'][name]
1319                return True
1320
1321        return False
1322
1323
1324    def get_node(self, name):
1325        """Retrieve a node from the graph.
1326
1327        Given a node's name the corresponding Node
1328        instance will be returned.
1329
1330        If one or more nodes exist with that name a list of
1331        Node instances is returned.
1332        An empty list is returned otherwise.
1333        """
1334
1335        match = list()
1336
1337        if self.obj_dict['nodes'].has_key(name):
1338
1339            match.extend( [ Node( obj_dict = obj_dict ) for obj_dict in self.obj_dict['nodes'][name] ])
1340
1341        return match
1342
1343
1344    def get_nodes(self):
1345        """Get the list of Node instances."""
1346
1347        return self.get_node_list()
1348
1349
1350    def get_node_list(self):
1351        """Get the list of Node instances.
1352
1353        This method returns the list of Node instances
1354        composing the graph.
1355        """
1356
1357        node_objs = list()
1358
1359        for node, obj_dict_list in self.obj_dict['nodes'].iteritems():
1360                node_objs.extend( [ Node( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
1361
1362        return node_objs
1363
1364
1365
1366    def add_edge(self, graph_edge):
1367        """Adds an edge object to the graph.
1368
1369        It takes a edge object as its only argument and returns
1370        None.
1371        """
1372
1373        if not isinstance(graph_edge, Edge):
1374            raise TypeError('add_edge() received a non edge class object: ' + str(graph_edge))
1375
1376        edge_points = ( graph_edge.get_source(), graph_edge.get_destination() )
1377
1378        if self.obj_dict['edges'].has_key(edge_points):
1379
1380            edge_list = self.obj_dict['edges'][edge_points]
1381            edge_list.append(graph_edge.obj_dict)
1382
1383        else:
1384
1385            self.obj_dict['edges'][edge_points] = [ graph_edge.obj_dict ]
1386
1387
1388        graph_edge.set_sequence( self.get_next_sequence_number() )
1389
1390        graph_edge.set_parent_graph( self.get_parent_graph() )
1391
1392
1393
1394    def del_edge(self, src_or_list, dst=None, index=None):
1395        """Delete an edge from the graph.
1396
1397        Given an edge's (source, destination) node names all
1398        matching edges(s) will be deleted if 'index' is not
1399        specified or set to None.
1400        If there are several matching edges and 'index' is
1401        given, only the edge in that position will be deleted.
1402
1403        'index' should be an integer specifying the position
1404        of the edge to delete. If index is larger than the
1405        number of matching edges, no action is taken.
1406
1407        If edges are deleted it returns True. If no action
1408        is taken it returns False.
1409        """
1410
1411        if isinstance( src_or_list, (list, tuple)):
1412            if dst is not None and isinstance(dst, (int, long)):
1413                index = dst
1414            src, dst = src_or_list
1415        else:
1416            src, dst = src_or_list, dst
1417
1418        if isinstance(src, Node):
1419            src = src.get_name()
1420
1421        if isinstance(dst, Node):
1422            dst = dst.get_name()
1423
1424        if self.obj_dict['edges'].has_key( (src, dst) ):
1425
1426            if index is not None and index < len(self.obj_dict['edges'][(src, dst)]):
1427                del self.obj_dict['edges'][(src, dst)][index]
1428                return True
1429            else:
1430                del self.obj_dict['edges'][(src, dst)]
1431                return True
1432
1433        return False
1434
1435
1436    def get_edge(self, src_or_list, dst=None):
1437        """Retrieved an edge from the graph.
1438
1439        Given an edge's source and destination the corresponding
1440        Edge instance(s) will be returned.
1441
1442        If one or more edges exist with that source and destination
1443        a list of Edge instances is returned.
1444        An empty list is returned otherwise.
1445        """
1446
1447        if isinstance( src_or_list, (list, tuple)) and dst is None:
1448            edge_points = tuple(src_or_list)
1449            edge_points_reverse = (edge_points[1], edge_points[0])
1450        else:
1451            edge_points = (src_or_list, dst)
1452            edge_points_reverse = (dst, src_or_list)
1453
1454        match = list()
1455
1456        if self.obj_dict['edges'].has_key( edge_points ) or (
1457            self.get_top_graph_type() == 'graph' and self.obj_dict['edges'].has_key( edge_points_reverse )):
1458
1459            edges_obj_dict = self.obj_dict['edges'].get(
1460                edge_points,
1461                self.obj_dict['edges'].get( edge_points_reverse, None ))
1462
1463            for edge_obj_dict in edges_obj_dict:
1464                match.append( Edge( edge_points[0], edge_points[1], obj_dict = edge_obj_dict ) )
1465
1466        return match
1467
1468
1469    def get_edges(self):
1470        return self.get_edge_list()
1471
1472
1473    def get_edge_list(self):
1474        """Get the list of Edge instances.
1475
1476        This method returns the list of Edge instances
1477        composing the graph.
1478        """
1479
1480        edge_objs = list()
1481
1482        for edge, obj_dict_list in self.obj_dict['edges'].iteritems():
1483                edge_objs.extend( [ Edge( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
1484
1485        return edge_objs
1486
1487
1488
1489    def add_subgraph(self, sgraph):
1490        """Adds an subgraph object to the graph.
1491
1492        It takes a subgraph object as its only argument and returns
1493        None.
1494        """
1495
1496        if not isinstance(sgraph, Subgraph) and not isinstance(sgraph, Cluster):
1497            raise TypeError('add_subgraph() received a non subgraph class object:' + str(sgraph))
1498
1499        if self.obj_dict['subgraphs'].has_key(sgraph.get_name()):
1500
1501            sgraph_list = self.obj_dict['subgraphs'][ sgraph.get_name() ]
1502            sgraph_list.append( sgraph.obj_dict )
1503
1504        else:
1505            self.obj_dict['subgraphs'][ sgraph.get_name() ] = [ sgraph.obj_dict ]
1506
1507        sgraph.set_sequence( self.get_next_sequence_number() )
1508
1509        sgraph.set_parent_graph( self.get_parent_graph() )
1510
1511
1512
1513
1514    def get_subgraph(self, name):
1515        """Retrieved a subgraph from the graph.
1516
1517        Given a subgraph's name the corresponding
1518        Subgraph instance will be returned.
1519
1520        If one or more subgraphs exist with the same name, a list of
1521        Subgraph instances is returned.
1522        An empty list is returned otherwise.
1523        """
1524
1525        match = list()
1526
1527        if self.obj_dict['subgraphs'].has_key( name ):
1528
1529            sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name )
1530
1531            for obj_dict_list in sgraphs_obj_dict:
1532                #match.extend( Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list )
1533                match.append( Subgraph( obj_dict = obj_dict_list ) )
1534
1535        return match
1536
1537
1538    def get_subgraphs(self):
1539
1540        return self.get_subgraph_list()
1541
1542
1543    def get_subgraph_list(self):
1544        """Get the list of Subgraph instances.
1545
1546        This method returns the list of Subgraph instances
1547        in the graph.
1548        """
1549
1550        sgraph_objs = list()
1551
1552        for sgraph, obj_dict_list in self.obj_dict['subgraphs'].iteritems():
1553                sgraph_objs.extend( [ Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
1554
1555        return sgraph_objs
1556
1557
1558
1559    def set_parent_graph(self, parent_graph):
1560
1561        self.obj_dict['parent_graph'] = parent_graph
1562
1563        for obj_list in self.obj_dict['nodes'].itervalues():
1564            for obj in obj_list:
1565                obj['parent_graph'] = parent_graph
1566
1567        for obj_list in self.obj_dict['edges'].itervalues():
1568            for obj in obj_list:
1569                obj['parent_graph'] = parent_graph
1570
1571        for obj_list in self.obj_dict['subgraphs'].itervalues():
1572            for obj in obj_list:
1573                Graph(obj_dict=obj).set_parent_graph(parent_graph)
1574
1575
1576
1577    def to_string(self):
1578        """Returns a string representation of the graph in dot language.
1579
1580        It will return the graph and all its subelements in string from.
1581        """
1582
1583
1584        graph = list()
1585
1586        if self.obj_dict.get('strict', None) is not None:
1587
1588            if self==self.get_parent_graph() and self.obj_dict['strict']:
1589
1590                graph.append('strict ')
1591
1592        if self.obj_dict['name'] == '':
1593            if 'show_keyword' in self.obj_dict and self.obj_dict['show_keyword']:
1594                graph.append( 'subgraph {\n' )
1595            else:
1596                graph.append( '{\n' )
1597        else:
1598            graph.append( '%s %s {\n' % (self.obj_dict['type'], self.obj_dict['name']) )
1599
1600
1601        for attr in self.obj_dict['attributes'].iterkeys():
1602
1603            if self.obj_dict['attributes'].get(attr, None) is not None:
1604
1605                val = self.obj_dict['attributes'].get(attr)
1606                if val is not None:
1607                    graph.append( '%s=%s' % (attr, quote_if_necessary(val)) )
1608                else:
1609                    graph.append( attr )
1610
1611                graph.append( ';\n' )
1612
1613
1614        edges_done = set()
1615
1616        edge_obj_dicts = list()
1617        for e in self.obj_dict['edges'].itervalues():
1618            edge_obj_dicts.extend(e)
1619
1620        if edge_obj_dicts:
1621            edge_src_set, edge_dst_set = zip( *[obj['points'] for obj in edge_obj_dicts] )
1622            edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set)
1623        else:
1624            edge_src_set, edge_dst_set = set(), set()
1625
1626        node_obj_dicts = list()
1627        for e in self.obj_dict['nodes'].itervalues():
1628            node_obj_dicts.extend(e)
1629
1630        sgraph_obj_dicts = list()
1631        for sg in self.obj_dict['subgraphs'].itervalues():
1632            sgraph_obj_dicts.extend(sg)
1633
1634
1635        obj_list = [ (obj['sequence'], obj) for obj in (edge_obj_dicts + node_obj_dicts + sgraph_obj_dicts) ]
1636        obj_list.sort()
1637
1638        for idx, obj in obj_list:
1639
1640            if obj['type'] == 'node':
1641
1642                node = Node(obj_dict=obj)
1643
1644                if self.obj_dict.get('suppress_disconnected', False):
1645
1646                    if (node.get_name() not in edge_src_set and
1647                        node.get_name() not in edge_dst_set):
1648
1649                        continue
1650
1651                graph.append( node.to_string()+'\n' )
1652
1653            elif obj['type'] == 'edge':
1654
1655                edge = Edge(obj_dict=obj)
1656
1657                if self.obj_dict.get('simplify', False) and edge in edges_done:
1658                    continue
1659
1660                graph.append( edge.to_string() + '\n' )
1661                edges_done.add(edge)
1662
1663            else:
1664
1665                sgraph = Subgraph(obj_dict=obj)
1666
1667                graph.append( sgraph.to_string()+'\n' )
1668
1669        graph.append( '}\n' )
1670
1671        return ''.join(graph)
1672
1673
1674
1675class Subgraph(Graph):
1676
1677    """Class representing a subgraph in Graphviz's dot language.
1678
1679    This class implements the methods to work on a representation
1680    of a subgraph in Graphviz's dot language.
1681
1682    subgraph(graph_name='subG', suppress_disconnected=False, attribute=value, ...)
1683
1684    graph_name:
1685        the subgraph's name
1686    suppress_disconnected:
1687        defaults to false, which will remove from the
1688        subgraph any disconnected nodes.
1689    All the attributes defined in the Graphviz dot language should
1690    be supported.
1691
1692    Attributes can be set through the dynamically generated methods:
1693
1694     set_[attribute name], i.e. set_size, set_fontname
1695
1696    or using the instance's attributes:
1697
1698     Subgraph.obj_dict['attributes'][attribute name], i.e.
1699
1700        subgraph_instance.obj_dict['attributes']['label']
1701        subgraph_instance.obj_dict['attributes']['fontname']
1702    """
1703
1704
1705    # RMF: subgraph should have all the attributes of graph so it can be passed
1706    # as a graph to all methods
1707    #
1708    def __init__(self, graph_name='', obj_dict=None, suppress_disconnected=False,
1709        simplify=False, **attrs):
1710
1711
1712        Graph.__init__(self, graph_name=graph_name, obj_dict=obj_dict,
1713            suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs)
1714
1715        if obj_dict is None:
1716
1717            self.obj_dict['type'] = 'subgraph'
1718
1719
1720
1721
1722class Cluster(Graph):
1723
1724    """Class representing a cluster in Graphviz's dot language.
1725
1726    This class implements the methods to work on a representation
1727    of a cluster in Graphviz's dot language.
1728
1729    cluster(graph_name='subG', suppress_disconnected=False, attribute=value, ...)
1730
1731    graph_name:
1732        the cluster's name (the string 'cluster' will be always prepended)
1733    suppress_disconnected:
1734        defaults to false, which will remove from the
1735        cluster any disconnected nodes.
1736    All the attributes defined in the Graphviz dot language should
1737    be supported.
1738
1739    Attributes can be set through the dynamically generated methods:
1740
1741     set_[attribute name], i.e. set_color, set_fontname
1742
1743    or using the instance's attributes:
1744
1745     Cluster.obj_dict['attributes'][attribute name], i.e.
1746
1747        cluster_instance.obj_dict['attributes']['label']
1748        cluster_instance.obj_dict['attributes']['fontname']
1749    """
1750
1751
1752    def __init__(self, graph_name='subG', obj_dict=None, suppress_disconnected=False,
1753        simplify=False, **attrs):
1754
1755        Graph.__init__(self, graph_name=graph_name, obj_dict=obj_dict,
1756            suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs)
1757
1758        if obj_dict is None:
1759
1760            self.obj_dict['type'] = 'subgraph'
1761            self.obj_dict['name'] = 'cluster_'+graph_name
1762
1763        self.create_attribute_methods(CLUSTER_ATTRIBUTES)
1764
1765
1766
1767
1768
1769
1770class Dot(Graph):
1771    """A container for handling a dot language file.
1772
1773    This class implements methods to write and process
1774    a dot language file. It is a derived class of
1775    the base class 'Graph'.
1776    """
1777
1778
1779
1780    def __init__(self, *argsl, **argsd):
1781        Graph.__init__(self, *argsl, **argsd)
1782
1783        self.shape_files = list()
1784
1785        self.progs = None
1786
1787        self.formats = ['canon', 'cmap', 'cmapx', 'cmapx_np', 'dia', 'dot',
1788            'fig', 'gd', 'gd2', 'gif', 'hpgl', 'imap', 'imap_np', 'ismap',
1789            'jpe', 'jpeg', 'jpg', 'mif', 'mp', 'pcl', 'pdf', 'pic', 'plain',
1790            'plain-ext', 'png', 'ps', 'ps2', 'svg', 'svgz', 'vml', 'vmlz',
1791            'vrml', 'vtx', 'wbmp', 'xdot', 'xlib' ]
1792
1793        self.prog = 'dot'
1794
1795        # Automatically creates all the methods enabling the creation
1796        # of output in any of the supported formats.
1797        for frmt in self.formats:
1798            self.__setattr__(
1799                'create_'+frmt,
1800                lambda f=frmt, prog=self.prog : self.create(format=f, prog=prog))
1801            f = self.__dict__['create_'+frmt]
1802            f.__doc__ = '''Refer to the docstring accompanying the 'create' method for more information.'''
1803
1804        for frmt in self.formats+['raw']:
1805            self.__setattr__(
1806                'write_'+frmt,
1807                lambda path, f=frmt, prog=self.prog : self.write(path, format=f, prog=prog))
1808
1809            f = self.__dict__['write_'+frmt]
1810            f.__doc__ = '''Refer to the docstring accompanying the 'write' method for more information.'''
1811
1812
1813
1814    def __getstate__(self):
1815
1816        dict = copy.copy(self.obj_dict)
1817
1818        return dict
1819
1820    def __setstate__(self, state):
1821
1822        self.obj_dict = state
1823
1824
1825    def set_shape_files(self, file_paths):
1826        """Add the paths of the required image files.
1827
1828        If the graph needs graphic objects to be used as shapes or otherwise
1829        those need to be in the same folder as the graph is going to be rendered
1830        from. Alternatively the absolute path to the files can be specified when
1831        including the graphics in the graph.
1832
1833        The files in the location pointed to by the path(s) specified as arguments
1834        to this method will be copied to the same temporary location where the
1835        graph is going to be rendered.
1836        """
1837
1838        if isinstance( file_paths, basestring ):
1839            self.shape_files.append( file_paths )
1840
1841        if isinstance( file_paths, (list, tuple) ):
1842            self.shape_files.extend( file_paths )
1843
1844
1845    def set_prog(self, prog):
1846        """Sets the default program.
1847
1848        Sets the default program in charge of processing
1849        the dot file into a graph.
1850        """
1851        self.prog = prog
1852
1853
1854    def set_graphviz_executables(self, paths):
1855        """This method allows to manually specify the location of the GraphViz executables.
1856
1857        The argument to this method should be a dictionary where the keys are as follows:
1858
1859            {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': ''}
1860
1861        and the values are the paths to the corresponding executable, including the name
1862        of the executable itself.
1863        """
1864
1865        self.progs = paths
1866
1867
1868    def write(self, path, prog=None, format='raw'):
1869        """Writes a graph to a file.
1870
1871        Given a filename 'path' it will open/create and truncate
1872        such file and write on it a representation of the graph
1873        defined by the dot object and in the format specified by
1874        'format'.
1875        The format 'raw' is used to dump the string representation
1876        of the Dot object, without further processing.
1877        The output can be processed by any of graphviz tools, defined
1878        in 'prog', which defaults to 'dot'
1879        Returns True or False according to the success of the write
1880        operation.
1881
1882        There's also the preferred possibility of using:
1883
1884            write_'format'(path, prog='program')
1885
1886        which are automatically defined for all the supported formats.
1887        [write_ps(), write_gif(), write_dia(), ...]
1888        """
1889
1890        if prog is None:
1891            prog = self.prog
1892
1893        dot_fd = file(path, "w+b")
1894        if format == 'raw':
1895            data = self.to_string()
1896            if isinstance(data, basestring):
1897                if not isinstance(data, unicode):
1898                    try:
1899                        data = unicode(data, 'utf-8')
1900                    except:
1901                        pass
1902
1903            try:
1904                data = data.encode('utf-8')
1905            except:
1906                pass
1907            dot_fd.write(data)
1908        else:
1909            dot_fd.write(self.create(prog, format))
1910        dot_fd.close()
1911
1912        return True
1913
1914
1915
1916    def create(self, prog=None, format='ps'):
1917        """Creates and returns a Postscript representation of the graph.
1918
1919        create will write the graph to a temporary dot file and process
1920        it with the program given by 'prog' (which defaults to 'twopi'),
1921        reading the Postscript output and returning it as a string is the
1922        operation is successful.
1923        On failure None is returned.
1924
1925        There's also the preferred possibility of using:
1926
1927            create_'format'(prog='program')
1928
1929        which are automatically defined for all the supported formats.
1930        [create_ps(), create_gif(), create_dia(), ...]
1931
1932        If 'prog' is a list instead of a string the fist item is expected
1933        to be the program name, followed by any optional command-line
1934        arguments for it:
1935
1936            [ 'twopi', '-Tdot', '-s10' ]
1937        """
1938
1939        if prog is None:
1940            prog = self.prog
1941
1942        if isinstance(prog, (list, tuple)):
1943            prog, args = prog[0], prog[1:]
1944        else:
1945            args = []
1946
1947        if self.progs is None:
1948            self.progs = find_graphviz()
1949            if self.progs is None:
1950                raise InvocationException(
1951                    'GraphViz\'s executables not found' )
1952
1953        if not self.progs.has_key(prog):
1954            raise InvocationException(
1955                'GraphViz\'s executable "%s" not found' % prog )
1956
1957        if not os.path.exists( self.progs[prog] ) or not os.path.isfile( self.progs[prog] ):
1958            raise InvocationException(
1959                'GraphViz\'s executable "%s" is not a file or doesn\'t exist' % self.progs[prog] )
1960
1961
1962        tmp_fd, tmp_name = tempfile.mkstemp()
1963        os.close(tmp_fd)
1964        self.write(tmp_name)
1965        tmp_dir = os.path.dirname(tmp_name )
1966
1967        # For each of the image files...
1968        #
1969        for img in self.shape_files:
1970
1971            # Get its data
1972            #
1973            f = file(img, 'rb')
1974            f_data = f.read()
1975            f.close()
1976
1977            # And copy it under a file with the same name in the temporary directory
1978            #
1979            f = file( os.path.join( tmp_dir, os.path.basename(img) ), 'wb' )
1980            f.write(f_data)
1981            f.close()
1982
1983        cmdline = [self.progs[prog], '-T'+format, tmp_name] + args
1984
1985        p = subprocess.Popen(
1986            cmdline,
1987            cwd=tmp_dir,
1988            stderr=subprocess.PIPE, stdout=subprocess.PIPE)
1989
1990        stderr = p.stderr
1991        stdout = p.stdout
1992
1993        stdout_output = list()
1994        while True:
1995            data = stdout.read()
1996            if not data:
1997                break
1998            stdout_output.append(data)
1999        stdout.close()
2000
2001        stdout_output = ''.join(stdout_output)
2002
2003        if not stderr.closed:
2004            stderr_output = list()
2005            while True:
2006                data = stderr.read()
2007                if not data:
2008                    break
2009                stderr_output.append(data)
2010            stderr.close()
2011
2012            if stderr_output:
2013                stderr_output = ''.join(stderr_output)
2014
2015        #pid, status = os.waitpid(p.pid, 0)
2016        status = p.wait()
2017
2018        if status != 0 :
2019            raise InvocationException(
2020                'Program terminated with status: %d. stderr follows: %s' % (
2021                    status, stderr_output) )
2022        elif stderr_output:
2023            print stderr_output
2024
2025        # For each of the image files...
2026        #
2027        for img in self.shape_files:
2028
2029            # remove it
2030            #
2031            os.unlink( os.path.join( tmp_dir, os.path.basename(img) ) )
2032
2033        os.unlink(tmp_name)
2034
2035        return stdout_output
2036
2037