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