00001
00002 """Graphviz's dot language Python interface.
00003
00004 This module provides with a full interface to create handle modify
00005 and process graphs in Graphviz's dot language.
00006
00007 References:
00008
00009 pydot Homepage: http://code.google.com/p/pydot/
00010 Graphviz: http://www.graphviz.org/
00011 DOT Language: http://www.graphviz.org/doc/info/lang.html
00012
00013 Programmed and tested with Graphviz 2.26.3 and Python 2.6 on OSX 10.6.4
00014
00015 Copyright (c) 2005-2011 Ero Carrera <ero.carrera@gmail.com>
00016
00017 Distributed under MIT license [http://opensource.org/licenses/mit-license.html].
00018 """
00019
00020 from __future__ import division, print_function
00021
00022 __author__ = 'Ero Carrera'
00023 __version__ = '1.0.29'
00024 __license__ = 'MIT'
00025
00026 import os
00027 import re
00028 import subprocess
00029 import sys
00030 import tempfile
00031 import copy
00032
00033 from operator import itemgetter
00034
00035 try:
00036 from . import _dotparser as dot_parser
00037 except Exception:
00038 print("Couldn't import _dotparser, loading of dot files will not be possible.")
00039
00040
00041 PY3 = not sys.version_info < (3, 0, 0)
00042
00043 if PY3:
00044 NULL_SEP = b''
00045 basestring = str
00046 long = int
00047 unicode = str
00048 else:
00049 NULL_SEP = ''
00050
00051
00052 GRAPH_ATTRIBUTES = set([
00053 'Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor',
00054 'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound',
00055 'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints',
00056 'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames',
00057 'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc',
00058 'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap',
00059 'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist',
00060 'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit',
00061 'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap',
00062 'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir',
00063 'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
00064 'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep',
00065 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start',
00066 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin',
00067
00068 'rank'
00069 ])
00070
00071
00072 EDGE_ATTRIBUTES = set([
00073 'URL', 'arrowhead', 'arrowsize', 'arrowtail',
00074 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
00075 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor',
00076 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel',
00077 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label',
00078 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor',
00079 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget',
00080 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen',
00081 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes',
00082 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport',
00083 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight',
00084 'rank'
00085 ])
00086
00087
00088 NODE_ATTRIBUTES = set([
00089 'URL', 'color', 'colorscheme', 'comment',
00090 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname',
00091 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label',
00092 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth',
00093 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
00094 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style',
00095 'target', 'tooltip', 'vertices', 'width', 'z',
00096
00097 'texlbl', 'texmode'
00098 ])
00099
00100
00101 CLUSTER_ATTRIBUTES = set([
00102 'K', 'URL', 'bgcolor', 'color', 'colorscheme',
00103 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
00104 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor',
00105 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip'
00106 ])
00107
00108
00109 def is_string_like(obj):
00110 """Check if obj is string."""
00111 try:
00112 obj + ''
00113 except (TypeError, ValueError):
00114 return False
00115 return True
00116
00117 def get_fobj(fname, mode='w+'):
00118 """Obtain a proper file object.
00119
00120 Parameters
00121 ----------
00122 fname : string, file object, file descriptor
00123 If a string or file descriptor, then we create a file object. If *fname*
00124 is a file object, then we do nothing and ignore the specified *mode*
00125 parameter.
00126 mode : str
00127 The mode of the file to be opened.
00128
00129 Returns
00130 -------
00131 fobj : file object
00132 The file object.
00133 close : bool
00134 If *fname* was a string, then *close* will be *True* to signify that
00135 the file object should be closed after writing to it. Otherwise, *close*
00136 will be *False* signifying that the user, in essence, created the file
00137 object already and that subsequent operations should not close it.
00138
00139 """
00140 if is_string_like(fname):
00141 fobj = open(fname, mode)
00142 close = True
00143 elif hasattr(fname, 'write'):
00144
00145 fobj = fname
00146 close = False
00147 else:
00148
00149 fobj = os.fdopen(fname, mode)
00150 close = False
00151 return fobj, close
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161 class frozendict(dict):
00162 def _blocked_attribute(obj):
00163 raise AttributeError("A frozendict cannot be modified.")
00164 _blocked_attribute = property(_blocked_attribute)
00165
00166 __delitem__ = __setitem__ = clear = _blocked_attribute
00167 pop = popitem = setdefault = update = _blocked_attribute
00168
00169 def __new__(cls, *args, **kw):
00170 new = dict.__new__(cls)
00171
00172 args_ = []
00173 for arg in args:
00174 if isinstance(arg, dict):
00175 arg = copy.copy(arg)
00176 for k, v in arg.items():
00177 if isinstance(v, frozendict):
00178 arg[k] = v
00179 elif isinstance(v, dict):
00180 arg[k] = frozendict(v)
00181 elif isinstance(v, list):
00182 v_ = list()
00183 for elm in v:
00184 if isinstance(elm, dict):
00185 v_.append(frozendict(elm))
00186 else:
00187 v_.append(elm)
00188 arg[k] = tuple(v_)
00189 args_.append(arg)
00190 else:
00191 args_.append(arg)
00192
00193 dict.__init__(new, *args_, **kw)
00194 return new
00195
00196 def __init__(self, *args, **kw):
00197 pass
00198
00199 def __hash__(self):
00200 try:
00201 return self._cached_hash
00202 except AttributeError:
00203 h = self._cached_hash = hash(tuple(sorted(self.items())))
00204 return h
00205
00206 def __repr__(self):
00207 return "frozendict(%s)" % dict.__repr__(self)
00208
00209
00210 dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
00211
00212 id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE)
00213 id_re_alpha_nums_with_ports = re.compile(
00214 '^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE
00215 )
00216 id_re_num = re.compile('^[0-9,]+$', re.UNICODE)
00217 id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE)
00218 id_re_dbl_quoted = re.compile('^\".*\"$', re.S | re.UNICODE)
00219 id_re_html = re.compile('^<.*>$', re.S | re.UNICODE)
00220
00221
00222 def needs_quotes(s):
00223 """Checks whether a string is a dot language ID.
00224
00225 It will check whether the string is solely composed
00226 by the characters allowed in an ID or not.
00227 If the string is one of the reserved keywords it will
00228 need quotes too but the user will need to add them
00229 manually.
00230 """
00231
00232
00233
00234
00235
00236
00237
00238 if s in dot_keywords:
00239 return False
00240
00241 chars = [ord(c) for c in s if ord(c) > 0x7f or ord(c) == 0]
00242 if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s):
00243 return True
00244
00245 for test_re in [
00246 id_re_alpha_nums, id_re_num, id_re_dbl_quoted,
00247 id_re_html, id_re_alpha_nums_with_ports
00248 ]:
00249 if test_re.match(s):
00250 return False
00251
00252 m = id_re_with_port.match(s)
00253 if m:
00254 return needs_quotes(m.group(1)) or needs_quotes(m.group(2))
00255
00256 return True
00257
00258
00259 def quote_if_necessary(s):
00260
00261 if isinstance(s, bool):
00262 if s is True:
00263 return 'True'
00264 return 'False'
00265
00266 if not isinstance(s, basestring):
00267 return s
00268
00269 if not s:
00270 return s
00271
00272 if needs_quotes(s):
00273 replace = {'"': r'\"', "\n": r'\n', "\r": r'\r'}
00274 for (a, b) in replace.items():
00275 s = s.replace(a, b)
00276
00277 return '"' + s + '"'
00278
00279 return s
00280
00281
00282 def graph_from_dot_data(data):
00283 """Load graph as defined by data in DOT format.
00284
00285 The data is assumed to be in DOT format. It will
00286 be parsed and a Dot class will be returned,
00287 representing the graph.
00288 """
00289
00290 return dot_parser.parse_dot_data(data)
00291
00292
00293 def graph_from_dot_file(path):
00294 """Load graph as defined by a DOT file.
00295
00296 The file is assumed to be in DOT format. It will
00297 be loaded, parsed and a Dot class will be returned,
00298 representing the graph.
00299 """
00300
00301 fd = open(path, 'rb')
00302 data = fd.read()
00303 fd.close()
00304
00305 return graph_from_dot_data(data)
00306
00307
00308 def graph_from_edges(edge_list, node_prefix='', directed=False):
00309 """Creates a basic graph out of an edge list.
00310
00311 The edge list has to be a list of tuples representing
00312 the nodes connected by the edge.
00313 The values can be anything: bool, int, float, str.
00314
00315 If the graph is undirected by default, it is only
00316 calculated from one of the symmetric halves of the matrix.
00317 """
00318
00319 if directed:
00320 graph = Dot(graph_type='digraph')
00321
00322 else:
00323 graph = Dot(graph_type='graph')
00324
00325 for edge in edge_list:
00326
00327 if isinstance(edge[0], str):
00328 src = node_prefix + edge[0]
00329 else:
00330 src = node_prefix + str(edge[0])
00331
00332 if isinstance(edge[1], str):
00333 dst = node_prefix + edge[1]
00334 else:
00335 dst = node_prefix + str(edge[1])
00336
00337 e = Edge(src, dst)
00338 graph.add_edge(e)
00339
00340 return graph
00341
00342
00343 def graph_from_adjacency_matrix(matrix, node_prefix='', directed=False):
00344 """Creates a basic graph out of an adjacency matrix.
00345
00346 The matrix has to be a list of rows of values
00347 representing an adjacency matrix.
00348 The values can be anything: bool, int, float, as long
00349 as they can evaluate to True or False.
00350 """
00351
00352 node_orig = 1
00353
00354 if directed:
00355 graph = Dot(graph_type='digraph')
00356 else:
00357 graph = Dot(graph_type='graph')
00358
00359 for row in matrix:
00360 if not directed:
00361 skip = matrix.index(row)
00362 r = row[skip:]
00363 else:
00364 skip = 0
00365 r = row
00366 node_dest = skip + 1
00367
00368 for e in r:
00369 if e:
00370 graph.add_edge(
00371 Edge(
00372 node_prefix + node_orig,
00373 node_prefix + node_dest
00374 )
00375 )
00376 node_dest += 1
00377 node_orig += 1
00378
00379 return graph
00380
00381
00382 def graph_from_incidence_matrix(matrix, node_prefix='', directed=False):
00383 """Creates a basic graph out of an incidence matrix.
00384
00385 The matrix has to be a list of rows of values
00386 representing an incidence matrix.
00387 The values can be anything: bool, int, float, as long
00388 as they can evaluate to True or False.
00389 """
00390
00391 if directed:
00392 graph = Dot(graph_type='digraph')
00393 else:
00394 graph = Dot(graph_type='graph')
00395
00396 for row in matrix:
00397 nodes = []
00398 c = 1
00399
00400 for node in row:
00401 if node:
00402 nodes.append(c * node)
00403 c += 1
00404
00405 nodes.sort()
00406
00407 if len(nodes) == 2:
00408 graph.add_edge(
00409 Edge(
00410 node_prefix + abs(nodes[0]),
00411 node_prefix + nodes[1]
00412 )
00413 )
00414
00415 if not directed:
00416 graph.set_simplify(True)
00417
00418 return graph
00419
00420
00421 def __find_executables(path):
00422 """Used by find_graphviz
00423
00424 path - single directory as a string
00425
00426 If any of the executables are found, it will return a dictionary
00427 containing the program names as keys and their paths as values.
00428
00429 Otherwise returns None
00430 """
00431
00432 success = False
00433 progs = {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': '', 'sfdp': ''}
00434
00435 was_quoted = False
00436 path = path.strip()
00437 if path.startswith('"') and path.endswith('"'):
00438 path = path[1:-1]
00439 was_quoted = True
00440
00441 if os.path.isdir(path):
00442 for prg in progs.keys():
00443 if progs[prg]:
00444 continue
00445
00446 if os.path.exists(os.path.join(path, prg)):
00447 if was_quoted:
00448 progs[prg] = '"' + os.path.join(path, prg) + '"'
00449 else:
00450 progs[prg] = os.path.join(path, prg)
00451
00452 success = True
00453
00454 elif os.path.exists(os.path.join(path, prg + '.exe')):
00455 if was_quoted:
00456 progs[prg] = '"' + os.path.join(path, prg + '.exe') + '"'
00457 else:
00458 progs[prg] = os.path.join(path, prg + '.exe')
00459
00460 success = True
00461
00462 if success:
00463 return progs
00464 else:
00465 return None
00466
00467
00468
00469
00470 def find_graphviz():
00471 """Locate Graphviz's executables in the system.
00472
00473 Tries three methods:
00474
00475 First: Windows Registry (Windows only)
00476 This requires Mark Hammond's pywin32 is installed.
00477
00478 Secondly: Search the path
00479 It will look for 'dot', 'twopi' and 'neato' in all the directories
00480 specified in the PATH environment variable.
00481
00482 Thirdly: Default install location (Windows only)
00483 It will look for 'dot', 'twopi' and 'neato' in the default install
00484 location under the "Program Files" directory.
00485
00486 It will return a dictionary containing the program names as keys
00487 and their paths as values.
00488
00489 If this fails, it returns None.
00490 """
00491
00492
00493 if os.sys.platform == 'win32':
00494
00495 HKEY_LOCAL_MACHINE = 0x80000002
00496 KEY_QUERY_VALUE = 0x0001
00497
00498 RegOpenKeyEx = None
00499 RegQueryValueEx = None
00500 RegCloseKey = None
00501
00502 try:
00503 import win32api
00504 RegOpenKeyEx = win32api.RegOpenKeyEx
00505 RegQueryValueEx = win32api.RegQueryValueEx
00506 RegCloseKey = win32api.RegCloseKey
00507
00508 except ImportError:
00509
00510 pass
00511
00512 try:
00513 import ctypes
00514
00515 def RegOpenKeyEx(key, subkey, opt, sam):
00516 result = ctypes.c_uint(0)
00517 ctypes.windll.advapi32.RegOpenKeyExA(key, subkey, opt, sam, ctypes.byref(result))
00518 return result.value
00519
00520 def RegQueryValueEx(hkey, valuename):
00521 data_type = ctypes.c_uint(0)
00522 data_len = ctypes.c_uint(1024)
00523 data = ctypes.create_string_buffer(1024)
00524
00525
00526 ctypes.windll.advapi32.RegQueryValueExA(
00527 hkey, valuename, 0, ctypes.byref(data_type),
00528 data, ctypes.byref(data_len)
00529 )
00530
00531 return data.value
00532
00533 RegCloseKey = ctypes.windll.advapi32.RegCloseKey
00534
00535 except ImportError:
00536
00537 pass
00538
00539 if RegOpenKeyEx is not None:
00540
00541 hkey = None
00542 potentialKeys = [
00543 "SOFTWARE\\ATT\\Graphviz",
00544 "SOFTWARE\\AT&T Research Labs\\Graphviz"
00545 ]
00546 for potentialKey in potentialKeys:
00547
00548 try:
00549 hkey = RegOpenKeyEx(
00550 HKEY_LOCAL_MACHINE,
00551 potentialKey, 0, KEY_QUERY_VALUE
00552 )
00553
00554 if hkey is not None:
00555 path = RegQueryValueEx(hkey, "InstallPath")
00556 RegCloseKey(hkey)
00557
00558
00559
00560 if not path:
00561 continue
00562
00563
00564 path = os.path.join(path, "bin")
00565 progs = __find_executables(path)
00566 if progs is not None:
00567
00568 return progs
00569
00570 except Exception:
00571
00572 pass
00573 else:
00574 break
00575
00576
00577 if 'PATH' in os.environ:
00578 for path in os.environ['PATH'].split(os.pathsep):
00579 progs = __find_executables(path)
00580 if progs is not None:
00581
00582 return progs
00583
00584
00585 if os.sys.platform == 'win32':
00586
00587
00588
00589 if 'PROGRAMFILES' in os.environ:
00590
00591
00592 path = os.path.join(os.environ['PROGRAMFILES'], 'ATT', 'GraphViz', 'bin')
00593 else:
00594
00595 path = r"C:\Program Files\att\Graphviz\bin"
00596
00597 progs = __find_executables(path)
00598
00599 if progs is not None:
00600
00601
00602 return progs
00603
00604 for path in (
00605 '/usr/bin', '/usr/local/bin',
00606 '/opt/local/bin',
00607 '/opt/bin', '/sw/bin', '/usr/share',
00608 '/Applications/Graphviz.app/Contents/MacOS/'
00609 ):
00610
00611 progs = __find_executables(path)
00612 if progs is not None:
00613
00614 return progs
00615
00616
00617 return None
00618
00619
00620 class Common(object):
00621 """Common information to several classes.
00622
00623 Should not be directly used, several classes are derived from
00624 this one.
00625 """
00626
00627 def __getstate__(self):
00628
00629 dict = copy.copy(self.obj_dict)
00630
00631 return dict
00632
00633 def __setstate__(self, state):
00634
00635 self.obj_dict = state
00636
00637 def __get_attribute__(self, attr):
00638 """Look for default attributes for this node"""
00639
00640 attr_val = self.obj_dict['attributes'].get(attr, None)
00641
00642 if attr_val is None:
00643
00644
00645 default_node_name = self.obj_dict['type']
00646
00647
00648 if default_node_name in ('subgraph', 'digraph', 'cluster'):
00649 default_node_name = 'graph'
00650
00651 g = self.get_parent_graph()
00652 if g is not None:
00653 defaults = g.get_node(default_node_name)
00654 else:
00655 return None
00656
00657
00658
00659
00660
00661
00662
00663
00664
00665 if not isinstance(defaults, (list, tuple)):
00666 defaults = [defaults]
00667
00668 for default in defaults:
00669 attr_val = default.obj_dict['attributes'].get(attr, None)
00670 if attr_val:
00671 return attr_val
00672 else:
00673 return attr_val
00674
00675 return None
00676
00677 def set_parent_graph(self, parent_graph):
00678
00679 self.obj_dict['parent_graph'] = parent_graph
00680
00681 def get_parent_graph(self):
00682
00683 return self.obj_dict.get('parent_graph', None)
00684
00685 def set(self, name, value):
00686 """Set an attribute value by name.
00687
00688 Given an attribute 'name' it will set its value to 'value'.
00689 There's always the possibility of using the methods:
00690
00691 set_'name'(value)
00692
00693 which are defined for all the existing attributes.
00694 """
00695
00696 self.obj_dict['attributes'][name] = value
00697
00698 def get(self, name):
00699 """Get an attribute value by name.
00700
00701 Given an attribute 'name' it will get its value.
00702 There's always the possibility of using the methods:
00703
00704 get_'name'()
00705
00706 which are defined for all the existing attributes.
00707 """
00708
00709 return self.obj_dict['attributes'].get(name, None)
00710
00711 def get_attributes(self):
00712 """"""
00713
00714 return self.obj_dict['attributes']
00715
00716 def set_sequence(self, seq):
00717
00718 self.obj_dict['sequence'] = seq
00719
00720 def get_sequence(self):
00721
00722 return self.obj_dict['sequence']
00723
00724 def create_attribute_methods(self, obj_attributes):
00725
00726
00727 for attr in obj_attributes:
00728
00729
00730
00731 self.__setattr__(
00732 'set_' + attr,
00733 lambda x, a=attr: self.obj_dict['attributes'].__setitem__(a, x)
00734 )
00735
00736
00737
00738 self.__setattr__('get_' + attr, lambda a=attr: self.__get_attribute__(a))
00739
00740
00741 class Error(Exception):
00742 """General error handling class.
00743 """
00744 def __init__(self, value):
00745 self.value = value
00746
00747 def __str__(self):
00748 return self.value
00749
00750
00751 class InvocationException(Exception):
00752 """To indicate that a ploblem occurred while running any of the GraphViz executables.
00753 """
00754 def __init__(self, value):
00755 self.value = value
00756
00757 def __str__(self):
00758 return self.value
00759
00760
00761 class Node(Common):
00762 """A graph node.
00763
00764 This class represents a graph's node with all its attributes.
00765
00766 node(name, attribute=value, ...)
00767
00768 name: node's name
00769
00770 All the attributes defined in the Graphviz dot language should
00771 be supported.
00772 """
00773
00774 def __init__(self, name='', obj_dict=None, **attrs):
00775
00776
00777
00778
00779
00780
00781 if obj_dict is not None:
00782 self.obj_dict = obj_dict
00783 else:
00784 self.obj_dict = dict()
00785
00786
00787
00788 self.obj_dict['attributes'] = dict(attrs)
00789 self.obj_dict['type'] = 'node'
00790 self.obj_dict['parent_graph'] = None
00791 self.obj_dict['parent_node_list'] = None
00792 self.obj_dict['sequence'] = None
00793
00794
00795
00796 port = None
00797 if isinstance(name, basestring) and not name.startswith('"'):
00798 idx = name.find(':')
00799 if idx > 0 and idx + 1 < len(name):
00800 name, port = name[:idx], name[idx:]
00801
00802 if isinstance(name, (long, int)):
00803 name = str(name)
00804
00805 self.obj_dict['name'] = quote_if_necessary(name)
00806 self.obj_dict['port'] = port
00807
00808 self.create_attribute_methods(NODE_ATTRIBUTES)
00809
00810 def set_name(self, node_name):
00811 """Set the node's name."""
00812
00813 self.obj_dict['name'] = node_name
00814
00815 def get_name(self):
00816 """Get the node's name."""
00817
00818 return self.obj_dict['name']
00819
00820 def get_port(self):
00821 """Get the node's port."""
00822
00823 return self.obj_dict['port']
00824
00825 def add_style(self, style):
00826
00827 styles = self.obj_dict['attributes'].get('style', None)
00828 if not styles and style:
00829 styles = [style]
00830 else:
00831 styles = styles.split(',')
00832 styles.append(style)
00833
00834 self.obj_dict['attributes']['style'] = ','.join(styles)
00835
00836 def to_string(self):
00837 """Returns a string representation of the node in dot language.
00838 """
00839
00840
00841
00842 node = quote_if_necessary(self.obj_dict['name'])
00843
00844 node_attr = list()
00845
00846 for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)):
00847 if value is not None:
00848 node_attr.append('%s=%s' % (attr, quote_if_necessary(value)))
00849 else:
00850 node_attr.append(attr)
00851
00852
00853
00854
00855 if node in ('graph', 'node', 'edge') and len(node_attr) == 0:
00856 return ''
00857
00858 node_attr = ', '.join(node_attr)
00859
00860 if node_attr:
00861 node += ' [' + node_attr + ']'
00862
00863 return node + ';'
00864
00865
00866 class Edge(Common):
00867 """A graph edge.
00868
00869 This class represents a graph's edge with all its attributes.
00870
00871 edge(src, dst, attribute=value, ...)
00872
00873 src: source node's name
00874 dst: destination node's name
00875
00876 All the attributes defined in the Graphviz dot language should
00877 be supported.
00878
00879 Attributes can be set through the dynamically generated methods:
00880
00881 set_[attribute name], i.e. set_label, set_fontname
00882
00883 or directly by using the instance's special dictionary:
00884
00885 Edge.obj_dict['attributes'][attribute name], i.e.
00886
00887 edge_instance.obj_dict['attributes']['label']
00888 edge_instance.obj_dict['attributes']['fontname']
00889
00890 """
00891
00892 def __init__(self, src='', dst='', obj_dict=None, **attrs):
00893
00894 if isinstance(src, (list, tuple)) and dst == '':
00895 src, dst = src
00896
00897 if obj_dict is not None:
00898
00899 self.obj_dict = obj_dict
00900
00901 else:
00902
00903 self.obj_dict = dict()
00904
00905
00906
00907 self.obj_dict['attributes'] = dict(attrs)
00908 self.obj_dict['type'] = 'edge'
00909 self.obj_dict['parent_graph'] = None
00910 self.obj_dict['parent_edge_list'] = None
00911 self.obj_dict['sequence'] = None
00912
00913 if isinstance(src, Node):
00914 src = src.get_name()
00915
00916 if isinstance(dst, Node):
00917 dst = dst.get_name()
00918
00919 points = (quote_if_necessary(src), quote_if_necessary(dst))
00920
00921 self.obj_dict['points'] = points
00922
00923 self.create_attribute_methods(EDGE_ATTRIBUTES)
00924
00925 def get_source(self):
00926 """Get the edges source node name."""
00927
00928 return self.obj_dict['points'][0]
00929
00930 def get_destination(self):
00931 """Get the edge's destination node name."""
00932
00933 return self.obj_dict['points'][1]
00934
00935 def __hash__(self):
00936 return hash(hash(self.get_source()) + hash(self.get_destination()))
00937
00938 def __eq__(self, edge):
00939 """Compare two edges.
00940
00941 If the parent graph is directed, arcs linking
00942 node A to B are considered equal and A->B != B->A
00943
00944 If the parent graph is undirected, any edge
00945 connecting two nodes is equal to any other
00946 edge connecting the same nodes, A->B == B->A
00947 """
00948
00949 if not isinstance(edge, Edge):
00950 raise Error("Can't compare and edge to a non-edge object.")
00951
00952 if self.get_parent_graph().get_top_graph_type() == 'graph':
00953
00954
00955
00956
00957 if ((self.get_source() == edge.get_source() and
00958 self.get_destination() == edge.get_destination()) or
00959 (edge.get_source() == self.get_destination() and
00960 edge.get_destination() == self.get_source())):
00961 return True
00962
00963 else:
00964 if (self.get_source() == edge.get_source() and
00965 self.get_destination() == edge.get_destination()):
00966 return True
00967
00968 return False
00969
00970 def parse_node_ref(self, node_str):
00971
00972 if not isinstance(node_str, str):
00973 return node_str
00974
00975 if node_str.startswith('"') and node_str.endswith('"'):
00976 return node_str
00977
00978 node_port_idx = node_str.rfind(':')
00979
00980 if (node_port_idx > 0 and node_str[0] == '"' and
00981 node_str[node_port_idx - 1] == '"'):
00982 return node_str
00983
00984 if node_port_idx > 0:
00985 a = node_str[:node_port_idx]
00986 b = node_str[node_port_idx + 1:]
00987
00988 node = quote_if_necessary(a)
00989
00990 node += ':' + quote_if_necessary(b)
00991
00992 return node
00993
00994 return node_str
00995
00996 def to_string(self):
00997 """Returns a string representation of the edge in dot language.
00998 """
00999
01000 src = self.parse_node_ref(self.get_source())
01001 dst = self.parse_node_ref(self.get_destination())
01002
01003 if isinstance(src, frozendict):
01004 edge = [Subgraph(obj_dict=src).to_string()]
01005 elif isinstance(src, (int, long)):
01006 edge = [str(src)]
01007 else:
01008 edge = [src]
01009
01010 if (self.get_parent_graph() and
01011 self.get_parent_graph().get_top_graph_type() and
01012 self.get_parent_graph().get_top_graph_type() == 'digraph'):
01013
01014 edge.append('->')
01015
01016 else:
01017 edge.append('--')
01018
01019 if isinstance(dst, frozendict):
01020 edge.append(Subgraph(obj_dict=dst).to_string())
01021 elif isinstance(dst, (int, long)):
01022 edge.append(str(dst))
01023 else:
01024 edge.append(dst)
01025
01026 edge_attr = list()
01027
01028 for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)):
01029 if value is not None:
01030 edge_attr.append('%s=%s' % (attr, quote_if_necessary(value)))
01031 else:
01032 edge_attr.append(attr)
01033
01034 edge_attr = ', '.join(edge_attr)
01035
01036 if edge_attr:
01037 edge.append(' [' + edge_attr + ']')
01038
01039 return ' '.join(edge) + ';'
01040
01041
01042 class Graph(Common):
01043 """Class representing a graph in Graphviz's dot language.
01044
01045 This class implements the methods to work on a representation
01046 of a graph in Graphviz's dot language.
01047
01048 graph(graph_name='G', graph_type='digraph',
01049 strict=False, suppress_disconnected=False, attribute=value, ...)
01050
01051 graph_name:
01052 the graph's name
01053 graph_type:
01054 can be 'graph' or 'digraph'
01055 suppress_disconnected:
01056 defaults to False, which will remove from the
01057 graph any disconnected nodes.
01058 simplify:
01059 if True it will avoid displaying equal edges, i.e.
01060 only one edge between two nodes. removing the
01061 duplicated ones.
01062
01063 All the attributes defined in the Graphviz dot language should
01064 be supported.
01065
01066 Attributes can be set through the dynamically generated methods:
01067
01068 set_[attribute name], i.e. set_size, set_fontname
01069
01070 or using the instance's attributes:
01071
01072 Graph.obj_dict['attributes'][attribute name], i.e.
01073
01074 graph_instance.obj_dict['attributes']['label']
01075 graph_instance.obj_dict['attributes']['fontname']
01076 """
01077
01078 def __init__(
01079 self, graph_name='G', obj_dict=None, graph_type='digraph', strict=False,
01080 suppress_disconnected=False, simplify=False, **attrs):
01081
01082 if obj_dict is not None:
01083 self.obj_dict = obj_dict
01084 else:
01085 self.obj_dict = dict()
01086
01087 self.obj_dict['attributes'] = dict(attrs)
01088
01089 if graph_type not in ['graph', 'digraph']:
01090 raise Error((
01091 'Invalid type "%s". Accepted graph types are: '
01092 'graph, digraph, subgraph' % graph_type
01093 ))
01094
01095 self.obj_dict['name'] = quote_if_necessary(graph_name)
01096 self.obj_dict['type'] = graph_type
01097
01098 self.obj_dict['strict'] = strict
01099 self.obj_dict['suppress_disconnected'] = suppress_disconnected
01100 self.obj_dict['simplify'] = simplify
01101
01102 self.obj_dict['current_child_sequence'] = 1
01103 self.obj_dict['nodes'] = dict()
01104 self.obj_dict['edges'] = dict()
01105 self.obj_dict['subgraphs'] = dict()
01106
01107 self.set_parent_graph(self)
01108
01109 self.create_attribute_methods(GRAPH_ATTRIBUTES)
01110
01111 def get_graph_type(self):
01112 return self.obj_dict['type']
01113
01114 def get_top_graph_type(self):
01115 parent = self
01116 while True:
01117 parent_ = parent.get_parent_graph()
01118 if parent_ == parent:
01119 break
01120 parent = parent_
01121
01122 return parent.obj_dict['type']
01123
01124 def set_graph_defaults(self, **attrs):
01125 self.add_node(Node('graph', **attrs))
01126
01127 def get_graph_defaults(self, **attrs):
01128
01129 graph_nodes = self.get_node('graph')
01130
01131 if isinstance(graph_nodes, (list, tuple)):
01132 return [node.get_attributes() for node in graph_nodes]
01133
01134 return graph_nodes.get_attributes()
01135
01136 def set_node_defaults(self, **attrs):
01137 self.add_node(Node('node', **attrs))
01138
01139 def get_node_defaults(self, **attrs):
01140 graph_nodes = self.get_node('node')
01141
01142 if isinstance(graph_nodes, (list, tuple)):
01143 return [node.get_attributes() for node in graph_nodes]
01144
01145 return graph_nodes.get_attributes()
01146
01147 def set_edge_defaults(self, **attrs):
01148 self.add_node(Node('edge', **attrs))
01149
01150 def get_edge_defaults(self, **attrs):
01151 graph_nodes = self.get_node('edge')
01152
01153 if isinstance(graph_nodes, (list, tuple)):
01154 return [node.get_attributes() for node in graph_nodes]
01155
01156 return graph_nodes.get_attributes()
01157
01158 def set_simplify(self, simplify):
01159 """Set whether to simplify or not.
01160
01161 If True it will avoid displaying equal edges, i.e.
01162 only one edge between two nodes. removing the
01163 duplicated ones.
01164 """
01165
01166 self.obj_dict['simplify'] = simplify
01167
01168 def get_simplify(self):
01169 """Get whether to simplify or not.
01170
01171 Refer to set_simplify for more information.
01172 """
01173
01174 return self.obj_dict['simplify']
01175
01176 def set_type(self, graph_type):
01177 """Set the graph's type, 'graph' or 'digraph'."""
01178
01179 self.obj_dict['type'] = graph_type
01180
01181 def get_type(self):
01182 """Get the graph's type, 'graph' or 'digraph'."""
01183
01184 return self.obj_dict['type']
01185
01186 def set_name(self, graph_name):
01187 """Set the graph's name."""
01188
01189 self.obj_dict['name'] = graph_name
01190
01191 def get_name(self):
01192 """Get the graph's name."""
01193
01194 return self.obj_dict['name']
01195
01196 def set_strict(self, val):
01197 """Set graph to 'strict' mode.
01198
01199 This option is only valid for top level graphs.
01200 """
01201
01202 self.obj_dict['strict'] = val
01203
01204 def get_strict(self, val):
01205 """Get graph's 'strict' mode (True, False).
01206
01207 This option is only valid for top level graphs.
01208 """
01209
01210 return self.obj_dict['strict']
01211
01212 def set_suppress_disconnected(self, val):
01213 """Suppress disconnected nodes in the output graph.
01214
01215 This option will skip nodes in the graph with no incoming or outgoing
01216 edges. This option works also for subgraphs and has effect only in the
01217 current graph/subgraph.
01218 """
01219
01220 self.obj_dict['suppress_disconnected'] = val
01221
01222 def get_suppress_disconnected(self, val):
01223 """Get if suppress disconnected is set.
01224
01225 Refer to set_suppress_disconnected for more information.
01226 """
01227
01228 return self.obj_dict['suppress_disconnected']
01229
01230 def get_next_sequence_number(self):
01231 seq = self.obj_dict['current_child_sequence']
01232 self.obj_dict['current_child_sequence'] += 1
01233 return seq
01234
01235 def add_node(self, graph_node):
01236 """Adds a node object to the graph.
01237
01238 It takes a node object as its only argument and returns
01239 None.
01240 """
01241
01242 if not isinstance(graph_node, Node):
01243 raise TypeError('add_node() received a non node class object: ' + str(graph_node))
01244
01245 node = self.get_node(graph_node.get_name())
01246
01247 if not node:
01248 self.obj_dict['nodes'][graph_node.get_name()] = [graph_node.obj_dict]
01249
01250
01251 graph_node.set_parent_graph(self.get_parent_graph())
01252 else:
01253 self.obj_dict['nodes'][graph_node.get_name()].append(graph_node.obj_dict)
01254
01255 graph_node.set_sequence(self.get_next_sequence_number())
01256
01257 def del_node(self, name, index=None):
01258 """Delete a node from the graph.
01259
01260 Given a node's name all node(s) with that same name
01261 will be deleted if 'index' is not specified or set
01262 to None.
01263 If there are several nodes with that same name and
01264 'index' is given, only the node in that position
01265 will be deleted.
01266
01267 'index' should be an integer specifying the position
01268 of the node to delete. If index is larger than the
01269 number of nodes with that name, no action is taken.
01270
01271 If nodes are deleted it returns True. If no action
01272 is taken it returns False.
01273 """
01274
01275 if isinstance(name, Node):
01276 name = name.get_name()
01277
01278 if name in self.obj_dict['nodes']:
01279 if index is not None and index < len(self.obj_dict['nodes'][name]):
01280 del self.obj_dict['nodes'][name][index]
01281 return True
01282 else:
01283 del self.obj_dict['nodes'][name]
01284 return True
01285
01286 return False
01287
01288 def get_node(self, name):
01289 """Retrieve a node from the graph.
01290
01291 Given a node's name the corresponding Node
01292 instance will be returned.
01293
01294 If one or more nodes exist with that name a list of
01295 Node instances is returned.
01296 An empty list is returned otherwise.
01297 """
01298
01299 match = list()
01300
01301 if name in self.obj_dict['nodes']:
01302 match.extend([
01303 Node(obj_dict=obj_dict)
01304 for obj_dict
01305 in self.obj_dict['nodes'][name]
01306 ])
01307
01308 return match
01309
01310 def get_nodes(self):
01311 """Get the list of Node instances."""
01312
01313 return self.get_node_list()
01314
01315 def get_node_list(self):
01316 """Get the list of Node instances.
01317
01318 This method returns the list of Node instances
01319 composing the graph.
01320 """
01321
01322 node_objs = list()
01323
01324 for node, obj_dict_list in self.obj_dict['nodes'].items():
01325 node_objs.extend([
01326 Node(obj_dict=obj_d)
01327 for obj_d
01328 in obj_dict_list
01329 ])
01330
01331 return node_objs
01332
01333 def add_edge(self, graph_edge):
01334 """Adds an edge object to the graph.
01335
01336 It takes a edge object as its only argument and returns
01337 None.
01338 """
01339
01340 if not isinstance(graph_edge, Edge):
01341 raise TypeError('add_edge() received a non edge class object: ' + str(graph_edge))
01342
01343 edge_points = (graph_edge.get_source(), graph_edge.get_destination())
01344
01345 if edge_points in self.obj_dict['edges']:
01346
01347 edge_list = self.obj_dict['edges'][edge_points]
01348 edge_list.append(graph_edge.obj_dict)
01349 else:
01350 self.obj_dict['edges'][edge_points] = [graph_edge.obj_dict]
01351
01352 graph_edge.set_sequence(self.get_next_sequence_number())
01353 graph_edge.set_parent_graph(self.get_parent_graph())
01354
01355 def del_edge(self, src_or_list, dst=None, index=None):
01356 """Delete an edge from the graph.
01357
01358 Given an edge's (source, destination) node names all
01359 matching edges(s) will be deleted if 'index' is not
01360 specified or set to None.
01361 If there are several matching edges and 'index' is
01362 given, only the edge in that position will be deleted.
01363
01364 'index' should be an integer specifying the position
01365 of the edge to delete. If index is larger than the
01366 number of matching edges, no action is taken.
01367
01368 If edges are deleted it returns True. If no action
01369 is taken it returns False.
01370 """
01371
01372 if isinstance(src_or_list, (list, tuple)):
01373 if dst is not None and isinstance(dst, (int, long)):
01374 index = dst
01375 src, dst = src_or_list
01376 else:
01377 src, dst = src_or_list, dst
01378
01379 if isinstance(src, Node):
01380 src = src.get_name()
01381
01382 if isinstance(dst, Node):
01383 dst = dst.get_name()
01384
01385 if (src, dst) in self.obj_dict['edges']:
01386 if index is not None and index < len(self.obj_dict['edges'][(src, dst)]):
01387 del self.obj_dict['edges'][(src, dst)][index]
01388 return True
01389 else:
01390 del self.obj_dict['edges'][(src, dst)]
01391 return True
01392
01393 return False
01394
01395 def get_edge(self, src_or_list, dst=None):
01396 """Retrieved an edge from the graph.
01397
01398 Given an edge's source and destination the corresponding
01399 Edge instance(s) will be returned.
01400
01401 If one or more edges exist with that source and destination
01402 a list of Edge instances is returned.
01403 An empty list is returned otherwise.
01404 """
01405
01406 if isinstance(src_or_list, (list, tuple)) and dst is None:
01407 edge_points = tuple(src_or_list)
01408 edge_points_reverse = (edge_points[1], edge_points[0])
01409 else:
01410 edge_points = (src_or_list, dst)
01411 edge_points_reverse = (dst, src_or_list)
01412
01413 match = list()
01414
01415 if edge_points in self.obj_dict['edges'] or (
01416 self.get_top_graph_type() == 'graph' and
01417 edge_points_reverse in self.obj_dict['edges']
01418 ):
01419
01420 edges_obj_dict = self.obj_dict['edges'].get(
01421 edge_points,
01422 self.obj_dict['edges'].get(edge_points_reverse, None))
01423
01424 for edge_obj_dict in edges_obj_dict:
01425 match.append(
01426 Edge(edge_points[0], edge_points[1], obj_dict=edge_obj_dict)
01427 )
01428
01429 return match
01430
01431 def get_edges(self):
01432 return self.get_edge_list()
01433
01434 def get_edge_list(self):
01435 """Get the list of Edge instances.
01436
01437 This method returns the list of Edge instances
01438 composing the graph.
01439 """
01440
01441 edge_objs = list()
01442
01443 for edge, obj_dict_list in self.obj_dict['edges'].items():
01444 edge_objs.extend([
01445 Edge(obj_dict=obj_d)
01446 for obj_d
01447 in obj_dict_list
01448 ])
01449
01450 return edge_objs
01451
01452 def add_subgraph(self, sgraph):
01453 """Adds an subgraph object to the graph.
01454
01455 It takes a subgraph object as its only argument and returns
01456 None.
01457 """
01458
01459 if not isinstance(sgraph, Subgraph) and not isinstance(sgraph, Cluster):
01460 raise TypeError('add_subgraph() received a non subgraph class object:' + str(sgraph))
01461
01462 if sgraph.get_name() in self.obj_dict['subgraphs']:
01463
01464 sgraph_list = self.obj_dict['subgraphs'][sgraph.get_name()]
01465 sgraph_list.append(sgraph.obj_dict)
01466
01467 else:
01468 self.obj_dict['subgraphs'][sgraph.get_name()] = [sgraph.obj_dict]
01469
01470 sgraph.set_sequence(self.get_next_sequence_number())
01471
01472 sgraph.set_parent_graph(self.get_parent_graph())
01473
01474 def get_subgraph(self, name):
01475 """Retrieved a subgraph from the graph.
01476
01477 Given a subgraph's name the corresponding
01478 Subgraph instance will be returned.
01479
01480 If one or more subgraphs exist with the same name, a list of
01481 Subgraph instances is returned.
01482 An empty list is returned otherwise.
01483 """
01484
01485 match = list()
01486
01487 if name in self.obj_dict['subgraphs']:
01488 sgraphs_obj_dict = self.obj_dict['subgraphs'].get(name)
01489
01490 for obj_dict_list in sgraphs_obj_dict:
01491
01492 match.append(Subgraph(obj_dict=obj_dict_list))
01493
01494 return match
01495
01496 def get_subgraphs(self):
01497 return self.get_subgraph_list()
01498
01499 def get_subgraph_list(self):
01500 """Get the list of Subgraph instances.
01501
01502 This method returns the list of Subgraph instances
01503 in the graph.
01504 """
01505
01506 sgraph_objs = list()
01507
01508 for sgraph, obj_dict_list in self.obj_dict['subgraphs'].items():
01509 sgraph_objs.extend([
01510 Subgraph(obj_dict=obj_d)
01511 for obj_d
01512 in obj_dict_list
01513 ])
01514
01515 return sgraph_objs
01516
01517 def set_parent_graph(self, parent_graph):
01518
01519 self.obj_dict['parent_graph'] = parent_graph
01520
01521 for obj_list in self.obj_dict['nodes'].values():
01522 for obj in obj_list:
01523 obj['parent_graph'] = parent_graph
01524
01525 for obj_list in self.obj_dict['edges'].values():
01526 for obj in obj_list:
01527 obj['parent_graph'] = parent_graph
01528
01529 for obj_list in self.obj_dict['subgraphs'].values():
01530 for obj in obj_list:
01531 Graph(obj_dict=obj).set_parent_graph(parent_graph)
01532
01533 def to_string(self):
01534 """Returns a string representation of the graph in dot language.
01535
01536 It will return the graph and all its subelements in string from.
01537 """
01538
01539 graph = list()
01540
01541 if self.obj_dict.get('strict', None) is not None:
01542 if self == self.get_parent_graph() and self.obj_dict['strict']:
01543 graph.append('strict ')
01544
01545 if self.obj_dict['name'] == '':
01546 if 'show_keyword' in self.obj_dict and self.obj_dict['show_keyword']:
01547 graph.append('subgraph {\n')
01548 else:
01549 graph.append('{\n')
01550 else:
01551 graph.append('%s %s {\n' % (self.obj_dict['type'], self.obj_dict['name']))
01552
01553 for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)):
01554 if value is not None:
01555 graph.append('%s=%s' % (attr, quote_if_necessary(value)))
01556 else:
01557 graph.append(attr)
01558
01559 graph.append(';\n')
01560
01561 edges_done = set()
01562
01563 edge_obj_dicts = list()
01564 for e in self.obj_dict['edges'].values():
01565 edge_obj_dicts.extend(e)
01566
01567 if edge_obj_dicts:
01568 edge_src_set, edge_dst_set = list(zip(*[obj['points'] for obj in edge_obj_dicts]))
01569 edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set)
01570 else:
01571 edge_src_set, edge_dst_set = set(), set()
01572
01573 node_obj_dicts = list()
01574 for e in self.obj_dict['nodes'].values():
01575 node_obj_dicts.extend(e)
01576
01577 sgraph_obj_dicts = list()
01578 for sg in self.obj_dict['subgraphs'].values():
01579 sgraph_obj_dicts.extend(sg)
01580
01581 obj_list = sorted([
01582 (obj['sequence'], obj)
01583 for obj
01584 in (edge_obj_dicts + node_obj_dicts + sgraph_obj_dicts)
01585 ])
01586
01587 for idx, obj in obj_list:
01588 if obj['type'] == 'node':
01589 node = Node(obj_dict=obj)
01590
01591 if self.obj_dict.get('suppress_disconnected', False):
01592 if (node.get_name() not in edge_src_set and
01593 node.get_name() not in edge_dst_set):
01594 continue
01595
01596 graph.append(node.to_string() + '\n')
01597
01598 elif obj['type'] == 'edge':
01599 edge = Edge(obj_dict=obj)
01600
01601 if self.obj_dict.get('simplify', False) and edge in edges_done:
01602 continue
01603
01604 graph.append(edge.to_string() + '\n')
01605 edges_done.add(edge)
01606 else:
01607 sgraph = Subgraph(obj_dict=obj)
01608 graph.append(sgraph.to_string() + '\n')
01609
01610 graph.append('}\n')
01611
01612 return ''.join(graph)
01613
01614
01615 class Subgraph(Graph):
01616
01617 """Class representing a subgraph in Graphviz's dot language.
01618
01619 This class implements the methods to work on a representation
01620 of a subgraph in Graphviz's dot language.
01621
01622 subgraph(graph_name='subG', suppress_disconnected=False, attribute=value, ...)
01623
01624 graph_name:
01625 the subgraph's name
01626 suppress_disconnected:
01627 defaults to false, which will remove from the
01628 subgraph any disconnected nodes.
01629 All the attributes defined in the Graphviz dot language should
01630 be supported.
01631
01632 Attributes can be set through the dynamically generated methods:
01633
01634 set_[attribute name], i.e. set_size, set_fontname
01635
01636 or using the instance's attributes:
01637
01638 Subgraph.obj_dict['attributes'][attribute name], i.e.
01639
01640 subgraph_instance.obj_dict['attributes']['label']
01641 subgraph_instance.obj_dict['attributes']['fontname']
01642 """
01643
01644
01645
01646
01647 def __init__(
01648 self, graph_name='', obj_dict=None, suppress_disconnected=False,
01649 simplify=False, **attrs):
01650
01651 Graph.__init__(
01652 self, graph_name=graph_name, obj_dict=obj_dict,
01653 suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs)
01654
01655 if obj_dict is None:
01656 self.obj_dict['type'] = 'subgraph'
01657
01658
01659 class Cluster(Graph):
01660
01661 """Class representing a cluster in Graphviz's dot language.
01662
01663 This class implements the methods to work on a representation
01664 of a cluster in Graphviz's dot language.
01665
01666 cluster(graph_name='subG', suppress_disconnected=False, attribute=value, ...)
01667
01668 graph_name:
01669 the cluster's name (the string 'cluster' will be always prepended)
01670 suppress_disconnected:
01671 defaults to false, which will remove from the
01672 cluster any disconnected nodes.
01673 All the attributes defined in the Graphviz dot language should
01674 be supported.
01675
01676 Attributes can be set through the dynamically generated methods:
01677
01678 set_[attribute name], i.e. set_color, set_fontname
01679
01680 or using the instance's attributes:
01681
01682 Cluster.obj_dict['attributes'][attribute name], i.e.
01683
01684 cluster_instance.obj_dict['attributes']['label']
01685 cluster_instance.obj_dict['attributes']['fontname']
01686 """
01687
01688 def __init__(
01689 self, graph_name='subG', obj_dict=None, suppress_disconnected=False,
01690 simplify=False, **attrs):
01691
01692 Graph.__init__(
01693 self, graph_name=graph_name, obj_dict=obj_dict,
01694 suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs
01695 )
01696
01697 if obj_dict is None:
01698 self.obj_dict['type'] = 'subgraph'
01699 self.obj_dict['name'] = 'cluster_' + graph_name
01700
01701 self.create_attribute_methods(CLUSTER_ATTRIBUTES)
01702
01703
01704 class Dot(Graph):
01705 """A container for handling a dot language file.
01706
01707 This class implements methods to write and process
01708 a dot language file. It is a derived class of
01709 the base class 'Graph'.
01710 """
01711
01712 def __init__(self, *argsl, **argsd):
01713 Graph.__init__(self, *argsl, **argsd)
01714
01715 self.shape_files = list()
01716 self.progs = None
01717 self.formats = [
01718 'canon', 'cmap', 'cmapx', 'cmapx_np', 'dia', 'dot',
01719 'fig', 'gd', 'gd2', 'gif', 'hpgl', 'imap', 'imap_np', 'ismap',
01720 'jpe', 'jpeg', 'jpg', 'mif', 'mp', 'pcl', 'pdf', 'pic', 'plain',
01721 'plain-ext', 'png', 'ps', 'ps2', 'svg', 'svgz', 'vml', 'vmlz',
01722 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib'
01723 ]
01724 self.prog = 'dot'
01725
01726
01727
01728 for frmt in self.formats:
01729 self.__setattr__(
01730 'create_' + frmt,
01731 lambda f=frmt, prog=self.prog: self.create(format=f, prog=prog)
01732 )
01733 f = self.__dict__['create_' + frmt]
01734 f.__doc__ = (
01735 '''Refer to the docstring accompanying the'''
01736 ''''create' method for more information.'''
01737 )
01738
01739 for frmt in self.formats + ['raw']:
01740 self.__setattr__(
01741 'write_' + frmt,
01742 lambda path, f=frmt, prog=self.prog: self.write(path, format=f, prog=prog)
01743 )
01744
01745 f = self.__dict__['write_' + frmt]
01746 f.__doc__ = (
01747 '''Refer to the docstring accompanying the'''
01748 ''''write' method for more information.'''
01749 )
01750
01751 def __getstate__(self):
01752 return copy.copy(self.obj_dict)
01753
01754 def __setstate__(self, state):
01755 self.obj_dict = state
01756
01757 def set_shape_files(self, file_paths):
01758 """Add the paths of the required image files.
01759
01760 If the graph needs graphic objects to be used as shapes or otherwise
01761 those need to be in the same folder as the graph is going to be rendered
01762 from. Alternatively the absolute path to the files can be specified when
01763 including the graphics in the graph.
01764
01765 The files in the location pointed to by the path(s) specified as arguments
01766 to this method will be copied to the same temporary location where the
01767 graph is going to be rendered.
01768 """
01769
01770 if isinstance(file_paths, basestring):
01771 self.shape_files.append(file_paths)
01772
01773 if isinstance(file_paths, (list, tuple)):
01774 self.shape_files.extend(file_paths)
01775
01776 def set_prog(self, prog):
01777 """Sets the default program.
01778
01779 Sets the default program in charge of processing
01780 the dot file into a graph.
01781 """
01782 self.prog = prog
01783
01784 def set_graphviz_executables(self, paths):
01785 """This method allows to manually specify the location of the GraphViz executables.
01786
01787 The argument to this method should be a dictionary where the keys are as follows:
01788
01789 {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': ''}
01790
01791 and the values are the paths to the corresponding executable, including the name
01792 of the executable itself.
01793 """
01794
01795 self.progs = paths
01796
01797 def write(self, path, prog=None, format='raw'):
01798 """
01799 Given a filename 'path' it will open/create and truncate
01800 such file and write on it a representation of the graph
01801 defined by the dot object and in the format specified by
01802 'format'. 'path' can also be an open file-like object, such as
01803 a StringIO instance.
01804
01805 The format 'raw' is used to dump the string representation
01806 of the Dot object, without further processing.
01807 The output can be processed by any of graphviz tools, defined
01808 in 'prog', which defaults to 'dot'
01809 Returns True or False according to the success of the write
01810 operation.
01811
01812 There's also the preferred possibility of using:
01813
01814 write_'format'(path, prog='program')
01815
01816 which are automatically defined for all the supported formats.
01817 [write_ps(), write_gif(), write_dia(), ...]
01818
01819 """
01820 if prog is None:
01821 prog = self.prog
01822
01823 fobj, close = get_fobj(path, 'w+b')
01824 try:
01825 if format == 'raw':
01826 data = self.to_string()
01827 if isinstance(data, basestring):
01828 if not isinstance(data, unicode):
01829 try:
01830 data = unicode(data, 'utf-8')
01831 except:
01832 pass
01833
01834 try:
01835 charset = self.get_charset()
01836 if not PY3 or not charset:
01837 charset = 'utf-8'
01838 data = data.encode(charset)
01839 except:
01840 if PY3:
01841 data = data.encode('utf-8')
01842 pass
01843
01844 fobj.write(data)
01845
01846 else:
01847 fobj.write(self.create(prog, format))
01848 finally:
01849 if close:
01850 fobj.close()
01851
01852 return True
01853
01854 def create(self, prog=None, format='ps'):
01855 """Creates and returns a Postscript representation of the graph.
01856
01857 create will write the graph to a temporary dot file and process
01858 it with the program given by 'prog' (which defaults to 'twopi'),
01859 reading the Postscript output and returning it as a string is the
01860 operation is successful.
01861 On failure None is returned.
01862
01863 There's also the preferred possibility of using:
01864
01865 create_'format'(prog='program')
01866
01867 which are automatically defined for all the supported formats.
01868 [create_ps(), create_gif(), create_dia(), ...]
01869
01870 If 'prog' is a list instead of a string the fist item is expected
01871 to be the program name, followed by any optional command-line
01872 arguments for it:
01873
01874 ['twopi', '-Tdot', '-s10']
01875 """
01876
01877 if prog is None:
01878 prog = self.prog
01879
01880 if isinstance(prog, (list, tuple)):
01881 prog, args = prog[0], prog[1:]
01882 else:
01883 args = []
01884
01885 if self.progs is None:
01886 self.progs = find_graphviz()
01887 if self.progs is None:
01888 raise InvocationException(
01889 'GraphViz\'s executables not found')
01890
01891 if prog not in self.progs:
01892 raise InvocationException(
01893 'GraphViz\'s executable "%s" not found' % prog)
01894
01895 if not os.path.exists(self.progs[prog]) or not os.path.isfile(self.progs[prog]):
01896 raise InvocationException(
01897 'GraphViz\'s executable "%s" is not a file or doesn\'t exist' % self.progs[prog])
01898
01899 tmp_fd, tmp_name = tempfile.mkstemp()
01900 os.close(tmp_fd)
01901 self.write(tmp_name)
01902 tmp_dir = os.path.dirname(tmp_name)
01903
01904
01905 for img in self.shape_files:
01906
01907
01908 f = open(img, 'rb')
01909 f_data = f.read()
01910 f.close()
01911
01912
01913 f = open(os.path.join(tmp_dir, os.path.basename(img)), 'wb')
01914 f.write(f_data)
01915 f.close()
01916
01917 cmdline = [self.progs[prog], '-q1 -T' + format, tmp_name] + args
01918
01919 p = subprocess.Popen(
01920 cmdline,
01921 cwd=tmp_dir,
01922 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
01923
01924 stderr = p.stderr
01925 stdout = p.stdout
01926
01927 stdout_output = list()
01928 while True:
01929 data = stdout.read()
01930 if not data:
01931 break
01932 stdout_output.append(data)
01933 stdout.close()
01934
01935 stdout_output = NULL_SEP.join(stdout_output)
01936
01937 if not stderr.closed:
01938 stderr_output = list()
01939 while True:
01940 data = stderr.read()
01941 if not data:
01942 break
01943 stderr_output.append(data)
01944 stderr.close()
01945
01946 if stderr_output:
01947 stderr_output = NULL_SEP.join(stderr_output)
01948 if PY3:
01949 stderr_output = stderr_output.decode(sys.stderr.encoding)
01950
01951
01952 status = p.wait()
01953
01954 if status != 0:
01955 raise InvocationException(
01956 'Program terminated with status: %d. stderr follows: %s' % (
01957 status, stderr_output))
01958 elif stderr_output:
01959 print(stderr_output)
01960
01961
01962 for img in self.shape_files:
01963
01964
01965 os.unlink(os.path.join(tmp_dir, os.path.basename(img)))
01966
01967 os.unlink(tmp_name)
01968
01969 return stdout_output