pydot.py
Go to the documentation of this file.
00001 # -*- coding: Latin-1 -*-
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     # for subgraphs
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     # The following are attributes dot2tex
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): # from John Hunter, types-free version
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         # fname is a file-like object, perhaps a StringIO (for example)
00145         fobj = fname
00146         close = False
00147     else:
00148         # assume it is a file descriptor
00149         fobj = os.fdopen(fname, mode)
00150         close = False
00151     return fobj, close
00152 
00153 
00154 #
00155 # Extented version of ASPN's Python Cookbook Recipe:
00156 # Frozen dictionaries.
00157 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/414283
00158 #
00159 # This version freezes dictionaries used as values within dictionaries.
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     # If the name is a reserved keyword it will need quotes but pydot
00233     # can't tell when it's being used as a keyword or when it's simply
00234     # a name. Hence the user needs to supply the quotes when an element
00235     # would use a reserved keyword as name. This function will return
00236     # false indicating that a keyword string, if provided as-is, won't
00237     # need quotes.
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 # The multi-platform version of this 'find_graphviz' function was
00469 # contributed by Peter Cock
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     # Method 1 (Windows only)
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             # Print a messaged suggesting they install these?
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                 # this has a return value, which we should probably check
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             # Print a messaged suggesting they install these?
00537             pass
00538 
00539         if RegOpenKeyEx is not None:
00540             # Get the GraphViz install path from the registry
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                         # The regitry variable might exist, left by old installations
00559                         # but with no value, in those cases we keep searching...
00560                         if not path:
00561                             continue
00562 
00563                         # Now append the "bin" subdirectory:
00564                         path = os.path.join(path, "bin")
00565                         progs = __find_executables(path)
00566                         if progs is not None:
00567                             #print("Used Windows registry")
00568                             return progs
00569 
00570                 except Exception:
00571                     #raise
00572                     pass
00573                 else:
00574                     break
00575 
00576     # Method 2 (Linux, Windows etc)
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                 #print("Used path")
00582                 return progs
00583 
00584     # Method 3 (Windows only)
00585     if os.sys.platform == 'win32':
00586 
00587         # Try and work out the equivalent of "C:\Program Files" on this
00588         # machine (might be on drive D:, or in a different language)
00589         if 'PROGRAMFILES' in os.environ:
00590             # Note, we could also use the win32api to get this
00591             # information, but win32api may not be installed.
00592             path = os.path.join(os.environ['PROGRAMFILES'], 'ATT', 'GraphViz', 'bin')
00593         else:
00594             #Just in case, try the default...
00595             path = r"C:\Program Files\att\Graphviz\bin"
00596 
00597         progs = __find_executables(path)
00598 
00599         if progs is not None:
00600 
00601             #print("Used default install location")
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             #print("Used path")
00614             return progs
00615 
00616     # Failed to find GraphViz
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             # get the defaults for nodes/edges
00644 
00645             default_node_name = self.obj_dict['type']
00646 
00647             # The defaults for graphs are set on a node named 'graph'
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             # Multiple defaults could be set by having repeated 'graph [...]'
00658             # 'node [...]', 'edge [...]' statements. In such case, if the
00659             # same attribute is set in different statements, only the first
00660             # will be returned. In order to get all, one would call the
00661             # get_*_defaults() methods and handle those. Or go node by node
00662             # (of the ones specifying defaults) and modify the attributes
00663             # individually.
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         #for attr in self.obj_dict['attributes']:
00727         for attr in obj_attributes:
00728 
00729             # Generate all the Setter methods.
00730             #
00731             self.__setattr__(
00732                 'set_' + attr,
00733                 lambda x, a=attr: self.obj_dict['attributes'].__setitem__(a, x)
00734                 )
00735 
00736             # Generate all the Getter methods.
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         # Nodes will take attributes of all other types because the defaults
00778         # for any GraphViz object are dealt with as if they were Node definitions
00779         #
00780 
00781         if obj_dict is not None:
00782             self.obj_dict = obj_dict
00783         else:
00784             self.obj_dict = dict()
00785 
00786             # Copy the attributes
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             # Remove the compass point
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         # RMF: special case defaults for node, edge and graph properties.
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         # No point in having nodes setting any defaults if the don't set
00853         # any attributes...
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             # Copy the attributes
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             # If the graph is undirected, the edge has neither
00955             # source nor destination.
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             #self.node_dict[graph_node.get_name()] = graph_node.attributes
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                 #match.extend(Subgraph(obj_dict = obj_d) for obj_d in obj_dict_list)
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     # RMF: subgraph should have all the attributes of graph so it can be passed
01645     # as a graph to all methods
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         # Automatically creates all the methods enabling the creation
01727         # of output in any of the supported formats.
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         # For each of the image files...
01905         for img in self.shape_files:
01906 
01907             # Get its data
01908             f = open(img, 'rb')
01909             f_data = f.read()
01910             f.close()
01911 
01912             # And copy it under a file with the same name in the temporary directory
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         #pid, status = os.waitpid(p.pid, 0)
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         # For each of the image files...
01962         for img in self.shape_files:
01963 
01964             # remove it
01965             os.unlink(os.path.join(tmp_dir, os.path.basename(img)))
01966 
01967         os.unlink(tmp_name)
01968 
01969         return stdout_output


rqt_decision_graph
Author(s):
autogenerated on Wed Aug 26 2015 11:16:47