diff --git a/.gitignore b/.gitignore index d2d6f36..d51960e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +__pycache__ # C extensions *.so diff --git a/README.md b/README.md index 64d3e9b..59ebb97 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,53 @@ nxpd ==== -Pydot drawing of NetworkX graphs with support for IPython notebooks. +`nxpd` is a Python package for visualizing NetworkX graphs using `pydot` +and `graphviz`. Support is also provided for inline displays within IPython +notebooks. + +Installation +============ +Clone this repository: + + git clone https://github.com/chebee7i/nxpd + +Move into the directory and install the package. + + python setup.py install + +Usage +===== + +>>> import networkx as nx +>>> from nxpd import draw +>>> G = nx.cycle(4, create_using=nx.DiGraph()) +>>> draw(G) + +This will display a PNG (by default) using your operating system's default +PNG viewer. Alternatively, if you are in an IPython notebook, then you +might like the image displayed inline. This is achieved by setting the `show` +parameter of the `draw` function. + + >>> draw(G, show='ipynb') + +If you want all graphs to be drawn inline, then you can set a global parameter. + + >>> from nxpd import nxpdParams + >>> nxpdParams['show'] = 'ipynb' + >>> draw(G) + +Any graph/node/edge attribute that is supported by DOT is passed through to +graphviz (via pydot). All others are skipped. + + >>> G = nx.DiGraph() + >>> G.graph['rankdir'] = 'LR' + >>> G.graph['dpi'] = 120 + >>> G.add_cycle(range(4)) + >>> G.add_node(0, color='red', style='filled', fillcolor='pink') + >>> G.add_node(1, shape='square') + >>> G.add_node(3, style='filled', fillcolor='#00ffff') + >>> G.add_edge(0, 1, color='red', style='dashed') + >>> G.add_edge(3, 3, label='a') + >>> draw(G) + +![IPython Notebook Example](images/demo.png) diff --git a/images/demo.png b/images/demo.png new file mode 100644 index 0000000..5a98085 Binary files /dev/null and b/images/demo.png differ diff --git a/nxpd/__init__.py b/nxpd/__init__.py new file mode 100644 index 0000000..35ce93c --- /dev/null +++ b/nxpd/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .nx_pydot import draw_pydot as draw +from .params import nxpdParams + +__all__ = ['draw', 'nxpdParams'] diff --git a/nxpd/nx_pydot.py b/nxpd/nx_pydot.py new file mode 100644 index 0000000..f57c0bb --- /dev/null +++ b/nxpd/nx_pydot.py @@ -0,0 +1,472 @@ +""" +Provide functions for converting to/from NetworkX and pydot+graphviz. + +Note: Much of this module exists in NetworkX 1.8. NetworkX will probably +continue to support basic conversions between NetworkX and pydot+graphviz, +but visualization functionality may be discontinued in future versions. + +pydot only supports Python versions 2.6+ but the 3.x series. A version which +supports 2.6+ and 3.x is available here: + + https://github.com/nlhepler/pydot + +We use this version for a number of reasons: + + 1) It supports 2.6+ and 3.x + 2) It has improved write support. + 3) It is based off pydot 1.0.28 (Jan 2012) which fixes: + https://code.google.com/p/pydot/issues/detail?id=61 + +To simplify installation of nxpd, an updated version of pydot is included +and used internally. + +See Also +-------- +pydot: http://code.google.com/p/pydot/ +Graphviz: http://www.research.att.com/sw/tools/graphviz/ +DOT Language: http://www.graphviz.org/doc/info/lang.html + +""" +# Copyright (C) 2004-2013 by +# Aric Hagberg +# Dan Schult +# Pieter Swart +# All rights reserved. +# BSD license. + +from __future__ import absolute_import + +import os +import sys +import tempfile +import time + +from . import pydot +from .params import nxpdParams +from .utils import get_fobj, default_opener, make_str + +import networkx as nx + +__author__ = "\n".join(["Aric Hagberg (aric.hagberg@gmail.com)", + "chebee7i (chebee7i@gmail.com)"]) + +__all__ = ['write_dot', 'read_dot', 'graphviz_layout', 'pydot_layout', + 'to_pydot', 'from_pydot', 'draw_pydot'] + +def write_dot(G, path): + """Write NetworkX graph G to Graphviz dot format on path. + + Path can be a string or a file handle. + + """ + P = to_pydot(G) + + fobj, close = get_fobj(path, mode) + try: + path.write(P.to_string()) + finally: + if close: + fobj.close() + +def read_dot(path): + """Return a NetworkX MultiGraph or MultiDiGraph from a dot file on path. + + Parameters + ---------- + path : filename or file handle + + Returns + ------- + G : NetworkX multigraph + A MultiGraph or MultiDiGraph. + + Notes + ----- + Use G=nx.Graph(nx.read_dot(path)) to return a Graph instead of a MultiGraph. + + """ + fobj, close = get_fobj(path, mode) + + try: + data = path.read() + finally: + if close: + fobj.close() + + P = pydot.graph_from_dot_data(data) + G = from_pydot(P) + + return G + +def from_pydot(P): + """Return a NetworkX graph from a Pydot graph. + + Parameters + ---------- + P : Pydot graph + A graph created with Pydot + + Returns + ------- + G : NetworkX multigraph + A MultiGraph or MultiDiGraph. + + Examples + -------- + >>> K5 = nx.complete_graph(5) + >>> A = nx.to_pydot(K5) + >>> G = nx.from_pydot(A) # return MultiGraph + >>> G = nx.Graph(nx.from_pydot(A)) # make a Graph instead of MultiGraph + + """ + if P.get_strict(None): # pydot bug: get_strict() shouldn't take argument + multiedges = False + else: + multiedges = True + + if P.get_type() == 'graph': # undirected + if multiedges: + create_using = nx.MultiGraph() + else: + create_using = nx.Graph() + else: + if multiedges: + create_using = nx.MultiDiGraph() + else: + create_using = nx.DiGraph() + + # assign defaults + N = nx.empty_graph(0, create_using) + N.name = P.get_name() + + # add nodes, attributes to N.node_attr + for p in P.get_node_list(): + n = p.get_name().strip('"') + if n in ('node', 'graph', 'edge'): + continue + N.add_node(n, **p.get_attributes()) + + # add edges + for e in P.get_edge_list(): + u = e.get_source() + v = e.get_destination() + attr = e.get_attributes() + s = [] + d = [] + + if isinstance(u, basestring): + s.append(u.strip('"')) + else: + for unodes in u['nodes'].iterkeys(): + s.append(unodes.strip('"')) + + if isinstance(v, basestring): + d.append(v.strip('"')) + else: + for vnodes in v['nodes'].iterkeys(): + d.append(vnodes.strip('"')) + + for source_node in s: + for destination_node in d: + N.add_edge(source_node, destination_node, **attr) + + # add default attributes for graph, nodes, edges + N.graph['graph'] = P.get_attributes() + try: + N.graph['node'] = P.get_node_defaults()[0] + except:# IndexError,TypeError: + N.graph['node'] = {} + try: + N.graph['edge'] = P.get_edge_defaults()[0] + except:# IndexError,TypeError: + N.graph['edge'] = {} + return N + +def filter_attrs(attrs, attr_type): + """ + Helper function to keep only pydot supported attributes. + + All unsupported attributes are filtered out. + + Parameters + ---------- + attrs : dict + A dictionary of attributes. + attr_type : str + The type of attributes. Must be 'edge', 'graph', or 'node'. + + Returns + ------- + d : dict + The filtered attributes. + + """ + if attr_type == 'edge': + accepted = pydot.EDGE_ATTRIBUTES + elif attr_type == 'graph': + accepted = pydot.GRAPH_ATTRIBUTES + elif attr_type == 'node': + accepted = pydot.NODE_ATTRIBUTES + else: + raise Exception("Invalid attr_type.") + + d = dict( [(k,v) for (k,v) in attrs.items() if k in accepted] ) + return d + +def to_pydot(G, raise_exceptions=True): + """Return a pydot graph from a NetworkX graph G. + + All node names are converted to strings. However, no preprocessing is + performed on the edge/graph/node attribute values since some attributes + need to be strings while other need to be floats. If pydot does not handle + needed conversions, then your graph should be modified beforehand. + + Generally, the rule is: If the attribute is a supported Graphviz + attribute, then it will be added to the Pydot graph (and thus, assumed to + be in the proper format for Graphviz). + + Parameters + ---------- + G : NetworkX graph + A graph created with NetworkX. + raise_exceptions : bool + If `True`, raise any exceptions. Otherwise, the exception is ignored + and the procedure continues. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> G.add_edge(2, 10, color='red') + >>> P = nx.to_pydot(G) + + """ + # Set Graphviz graph type. + if G.is_directed(): + graph_type = 'digraph' + else: + graph_type = 'graph' + + strict = G.number_of_selfloops() == 0 and not G.is_multigraph() + + # Create the Pydot graph. + name = G.graph.get('name') + graph_defaults = filter_attrs(G.graph, 'graph') + if name is None: + P = pydot.Dot(graph_type=graph_type, strict=strict, **graph_defaults) + else: + P = pydot.Dot(name, graph_type=graph_type, strict=strict, + **graph_defaults) + + # Set default node attributes, if possible. + node_defaults = filter_attrs(G.graph.get('node', {}), 'node') + if node_defaults: + try: + P.set_node_defaults(**node_defaults) + except: + if raise_exceptions: + raise + + # Set default edge attributes, if possible. + edge_defaults = filter_attrs(G.graph.get('edge', {}), 'edge') + if edge_defaults: + # This adds a node called "edge" to the graph. + try: + P.set_edge_defaults(**edge_defaults) + except: + if raise_exceptions: + raise + + # Add the nodes. + for n,nodedata in G.nodes_iter(data=True): + attrs = filter_attrs(nodedata, 'node') + node = pydot.Node(make_str(n), **attrs) + P.add_node(node) + + # Add the edges. + if G.is_multigraph(): + for u,v,key,edgedata in G.edges_iter(data=True,keys=True): + attrs = filter_attrs(edgedata, 'edge') + uu, vv, kk = make_str(u), make_str(v), make_str(key) + edge = pydot.Edge(uu, vv, key=kk, **attrs) + P.add_edge(edge) + else: + for u,v,edgedata in G.edges_iter(data=True): + attrs = filter_attrs(edgedata, 'edge') + uu, vv = make_str(u), make_str(v) + edge = pydot.Edge(uu, vv, **attrs) + P.add_edge(edge) + return P + +def graphviz_layout(G, prog='neato', root=None, **kwds): + """Create node positions using Pydot and Graphviz. + + Returns a dictionary of positions keyed by node. + + Examples + -------- + >>> G=nx.complete_graph(4) + >>> pos=nx.graphviz_layout(G) + >>> pos=nx.graphviz_layout(G,prog='dot') + + Notes + ----- + This is a wrapper for pydot_layout. + """ + return pydot_layout(G=G, prog=prog, root=root, **kwds) + + +def pydot_layout(G, prog='neato', root=None, **kwds): + """Create node positions using Pydot and Graphviz. + + Returns a dictionary of positions keyed by node. + + Examples + -------- + >>> G = nx.complete_graph(4) + >>> pos = nx.pydot_layout(G) + >>> pos = nx.pydot_layout(G, prog='dot') + + """ + P = to_pydot(G) + if root is not None : + P.set("root",make_str(root)) + + D = P.create_dot(prog=prog) + + if D == "": # no data returned + raise Exception("Graphviz layout with {0} failed.".format(prog)) + + Q = pydot.graph_from_dot_data(D) + + node_pos = {} + for n in G.nodes(): + pydot_node = pydot.Node(make_str(n)).get_name().encode('utf-8') + node = Q.get_node(pydot_node) + + if isinstance(node,list): + node = node[0] + pos = node.get_pos()[1:-1] # strip leading and trailing double quotes + if pos != None: + xx,yy = pos.split(",") + node_pos[n] = (float(xx),float(yy)) + return node_pos + +def draw_pydot(G, filename=None, format=None, prefix=None, suffix=None, + layout='dot', args=None, show=None): + """Draws the graph G using pydot and graphviz. + + Parameters + ---------- + G : graph + A NetworkX graph object (e.g., Graph, DiGraph). + + filename : str, None, file object + The name of the file to save the image to. If None, save to a + temporary file with the name: + nx_PREFIX_RANDOMSTRING_SUFFIX.ext. + File formats are inferred from the extension of the filename, when + provided. If the `format` parameter is not `None`, it overwrites any + inferred value for the extension. + + format : str + An output format. Note that not all may be available on every system + depending on how Graphviz was built. If no filename is provided and + no format is specified, then a 'png' image is created. Other values + for `format` are: + + 'canon', 'cmap', 'cmapx', 'cmapx_np', 'dia', 'dot', + 'fig', 'gd', 'gd2', 'gif', 'hpgl', 'imap', 'imap_np', + 'ismap', 'jpe', 'jpeg', 'jpg', 'mif', 'mp', 'pcl', 'pdf', + 'pic', 'plain', 'plain-ext', 'png', 'ps', 'ps2', 'svg', + 'svgz', 'vml', 'vmlz', 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib' + + prefix : str | None + If `filename` is None, we save to a temporary file. The value of + `prefix` will appear after 'nx_' but before random string + and file extension. If None, then the graph name will be used. + + suffix : str | None + If `filename` is None, we save to a temporary file. The value of + `suffix` will appear at after the prefix and random string but before + the file extension. If None, then no suffix is used. + + layout : str + The graphviz layout program. Pydot is responsible for locating the + binary. Common values for the layout program are: + 'neato','dot','twopi','circo','fdp','nop', 'wc','acyclic','gvpr', + 'gvcolor','ccomps','sccmap','tred' + + args : list + Additional arguments to pass to the Graphviz layout program. + This should be a list of strings. For example, ['-s10', '-maxiter=10']. + + show : bool + If `True`, then the image is displayed using the OS's default viewer + after drawing it. If show equals 'ipynb', then the image is displayed + inline for an IPython notebook. If `None`, then the value of + nxpdParams['show'] is used. By default, it is set to `True`. + + """ + # Determine the output format + if format is None: + # grab extension from filename + if filename is None: + # default to png + ext = 'png' + else: + ext = os.path.splitext(filename)[-1].lower()[1:] + else: + ext = format + + # Determine the "path" to be passed to pydot.Dot.write() + if filename is None: + if prefix is None: + prefix = G.graph.get("name", '') + + if prefix: + fn_prefix = "nx_{0}_".format(prefix) + else: + fn_prefix = "nx_" + + if suffix: + fn_suffix = '_{0}.{1}'.format(suffix, ext) + else: + fn_suffix = '.{0}'.format(ext) + + fobj = tempfile.NamedTemporaryFile(prefix=fn_prefix, + suffix=fn_suffix, + delete=False) + fname = fobj.name + close = True + else: + fobj, close = get_fobj(filename, 'w+b') + fname = fobj.name + + # Include additional command line arguments to the layout program. + if args is None: + args = [] + prog = layout + else: + args = list(args) + prog = [layout] + args + + # Draw the image. + G2 = to_pydot(G) + G2.write(fobj, prog=prog, format=ext) + if close: + fobj.close() + + if show is None: + show = nxpdParams['show'] + + if show: + if show == 'ipynb': + from IPython.core.display import Image + return Image(filename=fname, embed=True) + else: + default_opener(fname) + if sys.platform == 'linux2': + # necessary when opening many images in a row + time.sleep(.5) + + return fname diff --git a/nxpd/params.py b/nxpd/params.py new file mode 100644 index 0000000..cc3d086 --- /dev/null +++ b/nxpd/params.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +Defines global configuration parameters. + +""" + +import warnings + +__all__ = ['nxpdParams', 'reset_params'] + +### Generic validations + +def validate_boolean(b): + """Convert b to a boolean or raise a ValueError.""" + try: + b = b.lower() + except AttributeError: + pass + if b in ('t', 'y', 'yes', 'on', 'true', '1', 1, True): return True + elif b in ('f', 'n', 'no', 'off', 'false', '0', 0, False): return False + else: + raise ValueError('Could not convert {0!r} to boolean'.format(b)) + +def validate_float(s): + """Convert s to float or raise a ValueError.""" + try: + return float(s) + except ValueError: + raise ValueError('Could not convert {0!r} to float'.format(s)) + +def validate_choice(s, choices): + try: + s = s.lower() + except AttributeError: + pass + if s not in choices: + raise ValueError("{0!r} is an invalid specification.".format(s)) + else: + return s + +### Specific validations + +def validate_show(s): + choices = ['ipynb', 'external', 'none'] + return validate_choice(s, choices) + + +### The main parameter class + +class Params(dict): + """ + A dictionary including validation, representing configuration parameters. + + """ + + def __init__(self): + """ + Initialize the Params instance. + + """ + defaults = [(key, tup[0]) for key, tup in defaultParams.items()] + converters = [(key, tup[1]) for key, tup in defaultParams.items()] + + # A dictionary relating params to validators. + self.validate = dict(converters) + dict.__init__(self, defaults) + + def _deprecation_check(self, param): + """ + Raise warning if param is deprecated. + + Return the param to use. This is the alternative parameter if available, + otherwise it is the original parameter. + + """ + if param in deprecatedParams: + alt = deprecatedParams[param] + if alt is None: + msg = "{0!r} is deprecated. There is no replacement." + msg = msg.format(key) + else: + msg = "{0!r} is deprecated. Use {1!r} instead." + msg = msg.format(key, alt) + param = alt + + warnings.warn(msg, DeprecationWarning, stacklevel=2) + + if param not in self.validate: + msg = '{0!r} is not a valid parameter. '.format(key) + msg += 'See nxParams.keys() for a list of valid parameters.' + raise KeyError(msg) + + return param + + def __setitem__(self, key, val): + key = self._deprecation_check(key) + cval = self.validate[key](val) + dict.__setitem__(self, key, cval) + + def __getitem__(self, key): + key = self._deprecation_check(key) + return dict.__getitem__(self, key) + +def reset_params(): + """ + Restore parameters to default values. + + """ + # This modifies the global parameters. + nxpdParams.update(nxpdParamsDefault) + + +### +### Globals +### + + +### TODO: key -> (value, validator, info_string) +defaultParams = { + # parameter : (default value, validator) + 'show': ('external', validate_show), +} + +### Dictionary relating deprecated parameter names to new names. +deprecatedParams = { + # old parameter : new parameter, (use None if no new parameter) +} + +### This is what will be used by nxpd. +nxpdParamsDefault = Params() +nxpdParams = Params() + + diff --git a/nxpd/pydot/__init__.py b/nxpd/pydot/__init__.py new file mode 100644 index 0000000..142b039 --- /dev/null +++ b/nxpd/pydot/__init__.py @@ -0,0 +1,1969 @@ +# -*- coding: Latin-1 -*- +"""Graphviz's dot language Python interface. + +This module provides with a full interface to create handle modify +and process graphs in Graphviz's dot language. + +References: + +pydot Homepage: http://code.google.com/p/pydot/ +Graphviz: http://www.graphviz.org/ +DOT Language: http://www.graphviz.org/doc/info/lang.html + +Programmed and tested with Graphviz 2.26.3 and Python 2.6 on OSX 10.6.4 + +Copyright (c) 2005-2011 Ero Carrera + +Distributed under MIT license [http://opensource.org/licenses/mit-license.html]. +""" + +from __future__ import division, print_function + +__author__ = 'Ero Carrera' +__version__ = '1.0.29' +__license__ = 'MIT' + +import os +import re +import subprocess +import sys +import tempfile +import copy + +from operator import itemgetter + +try: + from . import _dotparser as dot_parser +except Exception: + print("Couldn't import _dotparser, loading of dot files will not be possible.") + + +PY3 = not sys.version_info < (3, 0, 0) + +if PY3: + NULL_SEP = b'' + basestring = str + long = int + unicode = str +else: + NULL_SEP = '' + + +GRAPH_ATTRIBUTES = set([ + 'Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor', + 'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound', + 'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints', + 'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames', + 'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc', + 'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap', + 'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist', + 'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit', + 'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap', + 'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir', + 'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross', + 'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep', + 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start', + 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin', + # for subgraphs + 'rank' + ]) + + +EDGE_ATTRIBUTES = set([ + 'URL', 'arrowhead', 'arrowsize', 'arrowtail', + 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir', + 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor', + 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel', + 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label', + 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor', + 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget', + 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen', + 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes', + 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport', + 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight', + 'rank' + ]) + + +NODE_ATTRIBUTES = set([ + 'URL', 'color', 'colorscheme', 'comment', + 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname', + 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label', + 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth', + 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints', + 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style', + 'target', 'tooltip', 'vertices', 'width', 'z', + # The following are attributes dot2tex + 'texlbl', 'texmode' + ]) + + +CLUSTER_ATTRIBUTES = set([ + 'K', 'URL', 'bgcolor', 'color', 'colorscheme', + 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust', + 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor', + 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip' + ]) + + +def is_string_like(obj): # from John Hunter, types-free version + """Check if obj is string.""" + try: + obj + '' + except (TypeError, ValueError): + return False + return True + +def get_fobj(fname, mode='w+'): + """Obtain a proper file object. + + Parameters + ---------- + fname : string, file object, file descriptor + If a string or file descriptor, then we create a file object. If *fname* + is a file object, then we do nothing and ignore the specified *mode* + parameter. + mode : str + The mode of the file to be opened. + + Returns + ------- + fobj : file object + The file object. + close : bool + If *fname* was a string, then *close* will be *True* to signify that + the file object should be closed after writing to it. Otherwise, *close* + will be *False* signifying that the user, in essence, created the file + object already and that subsequent operations should not close it. + + """ + if is_string_like(fname): + fobj = open(fname, mode) + close = True + elif hasattr(fname, 'write'): + # fname is a file-like object, perhaps a StringIO (for example) + fobj = fname + close = False + else: + # assume it is a file descriptor + fobj = os.fdopen(fname, mode) + close = False + return fobj, close + + +# +# Extented version of ASPN's Python Cookbook Recipe: +# Frozen dictionaries. +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/414283 +# +# This version freezes dictionaries used as values within dictionaries. +# +class frozendict(dict): + def _blocked_attribute(obj): + raise AttributeError("A frozendict cannot be modified.") + _blocked_attribute = property(_blocked_attribute) + + __delitem__ = __setitem__ = clear = _blocked_attribute + pop = popitem = setdefault = update = _blocked_attribute + + def __new__(cls, *args, **kw): + new = dict.__new__(cls) + + args_ = [] + for arg in args: + if isinstance(arg, dict): + arg = copy.copy(arg) + for k, v in arg.items(): + if isinstance(v, frozendict): + arg[k] = v + elif isinstance(v, dict): + arg[k] = frozendict(v) + elif isinstance(v, list): + v_ = list() + for elm in v: + if isinstance(elm, dict): + v_.append(frozendict(elm)) + else: + v_.append(elm) + arg[k] = tuple(v_) + args_.append(arg) + else: + args_.append(arg) + + dict.__init__(new, *args_, **kw) + return new + + def __init__(self, *args, **kw): + pass + + def __hash__(self): + try: + return self._cached_hash + except AttributeError: + h = self._cached_hash = hash(tuple(sorted(self.items()))) + return h + + def __repr__(self): + return "frozendict(%s)" % dict.__repr__(self) + + +dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict'] + +id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE) +id_re_alpha_nums_with_ports = re.compile( + '^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE + ) +id_re_num = re.compile('^[0-9,]+$', re.UNICODE) +id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE) +id_re_dbl_quoted = re.compile('^\".*\"$', re.S | re.UNICODE) +id_re_html = re.compile('^<.*>$', re.S | re.UNICODE) + + +def needs_quotes(s): + """Checks whether a string is a dot language ID. + + It will check whether the string is solely composed + by the characters allowed in an ID or not. + If the string is one of the reserved keywords it will + need quotes too but the user will need to add them + manually. + """ + + # If the name is a reserved keyword it will need quotes but pydot + # can't tell when it's being used as a keyword or when it's simply + # a name. Hence the user needs to supply the quotes when an element + # would use a reserved keyword as name. This function will return + # false indicating that a keyword string, if provided as-is, won't + # need quotes. + if s in dot_keywords: + return False + + chars = [ord(c) for c in s if ord(c) > 0x7f or ord(c) == 0] + if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s): + return True + + for test_re in [ + id_re_alpha_nums, id_re_num, id_re_dbl_quoted, + id_re_html, id_re_alpha_nums_with_ports + ]: + if test_re.match(s): + return False + + m = id_re_with_port.match(s) + if m: + return needs_quotes(m.group(1)) or needs_quotes(m.group(2)) + + return True + + +def quote_if_necessary(s): + + if isinstance(s, bool): + if s is True: + return 'True' + return 'False' + + if not isinstance(s, basestring): + return s + + if not s: + return s + + if needs_quotes(s): + replace = {'"': r'\"', "\n": r'\n', "\r": r'\r'} + for (a, b) in replace.items(): + s = s.replace(a, b) + + return '"' + s + '"' + + return s + + +def graph_from_dot_data(data): + """Load graph as defined by data in DOT format. + + The data is assumed to be in DOT format. It will + be parsed and a Dot class will be returned, + representing the graph. + """ + + return dot_parser.parse_dot_data(data) + + +def graph_from_dot_file(path): + """Load graph as defined by a DOT file. + + The file is assumed to be in DOT format. It will + be loaded, parsed and a Dot class will be returned, + representing the graph. + """ + + fd = open(path, 'rb') + data = fd.read() + fd.close() + + return graph_from_dot_data(data) + + +def graph_from_edges(edge_list, node_prefix='', directed=False): + """Creates a basic graph out of an edge list. + + The edge list has to be a list of tuples representing + the nodes connected by the edge. + The values can be anything: bool, int, float, str. + + If the graph is undirected by default, it is only + calculated from one of the symmetric halves of the matrix. + """ + + if directed: + graph = Dot(graph_type='digraph') + + else: + graph = Dot(graph_type='graph') + + for edge in edge_list: + + if isinstance(edge[0], str): + src = node_prefix + edge[0] + else: + src = node_prefix + str(edge[0]) + + if isinstance(edge[1], str): + dst = node_prefix + edge[1] + else: + dst = node_prefix + str(edge[1]) + + e = Edge(src, dst) + graph.add_edge(e) + + return graph + + +def graph_from_adjacency_matrix(matrix, node_prefix='', directed=False): + """Creates a basic graph out of an adjacency matrix. + + The matrix has to be a list of rows of values + representing an adjacency matrix. + The values can be anything: bool, int, float, as long + as they can evaluate to True or False. + """ + + node_orig = 1 + + if directed: + graph = Dot(graph_type='digraph') + else: + graph = Dot(graph_type='graph') + + for row in matrix: + if not directed: + skip = matrix.index(row) + r = row[skip:] + else: + skip = 0 + r = row + node_dest = skip + 1 + + for e in r: + if e: + graph.add_edge( + Edge( + node_prefix + node_orig, + node_prefix + node_dest + ) + ) + node_dest += 1 + node_orig += 1 + + return graph + + +def graph_from_incidence_matrix(matrix, node_prefix='', directed=False): + """Creates a basic graph out of an incidence matrix. + + The matrix has to be a list of rows of values + representing an incidence matrix. + The values can be anything: bool, int, float, as long + as they can evaluate to True or False. + """ + + if directed: + graph = Dot(graph_type='digraph') + else: + graph = Dot(graph_type='graph') + + for row in matrix: + nodes = [] + c = 1 + + for node in row: + if node: + nodes.append(c * node) + c += 1 + + nodes.sort() + + if len(nodes) == 2: + graph.add_edge( + Edge( + node_prefix + abs(nodes[0]), + node_prefix + nodes[1] + ) + ) + + if not directed: + graph.set_simplify(True) + + return graph + + +def __find_executables(path): + """Used by find_graphviz + + path - single directory as a string + + If any of the executables are found, it will return a dictionary + containing the program names as keys and their paths as values. + + Otherwise returns None + """ + + success = False + progs = {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': '', 'sfdp': ''} + + was_quoted = False + path = path.strip() + if path.startswith('"') and path.endswith('"'): + path = path[1:-1] + was_quoted = True + + if os.path.isdir(path): + for prg in progs.keys(): + if progs[prg]: + continue + + if os.path.exists(os.path.join(path, prg)): + if was_quoted: + progs[prg] = '"' + os.path.join(path, prg) + '"' + else: + progs[prg] = os.path.join(path, prg) + + success = True + + elif os.path.exists(os.path.join(path, prg + '.exe')): + if was_quoted: + progs[prg] = '"' + os.path.join(path, prg + '.exe') + '"' + else: + progs[prg] = os.path.join(path, prg + '.exe') + + success = True + + if success: + return progs + else: + return None + + +# The multi-platform version of this 'find_graphviz' function was +# contributed by Peter Cock +def find_graphviz(): + """Locate Graphviz's executables in the system. + + Tries three methods: + + First: Windows Registry (Windows only) + This requires Mark Hammond's pywin32 is installed. + + Secondly: Search the path + It will look for 'dot', 'twopi' and 'neato' in all the directories + specified in the PATH environment variable. + + Thirdly: Default install location (Windows only) + It will look for 'dot', 'twopi' and 'neato' in the default install + location under the "Program Files" directory. + + It will return a dictionary containing the program names as keys + and their paths as values. + + If this fails, it returns None. + """ + + # Method 1 (Windows only) + if os.sys.platform == 'win32': + + HKEY_LOCAL_MACHINE = 0x80000002 + KEY_QUERY_VALUE = 0x0001 + + RegOpenKeyEx = None + RegQueryValueEx = None + RegCloseKey = None + + try: + import win32api + RegOpenKeyEx = win32api.RegOpenKeyEx + RegQueryValueEx = win32api.RegQueryValueEx + RegCloseKey = win32api.RegCloseKey + + except ImportError: + # Print a messaged suggesting they install these? + pass + + try: + import ctypes + + def RegOpenKeyEx(key, subkey, opt, sam): + result = ctypes.c_uint(0) + ctypes.windll.advapi32.RegOpenKeyExA(key, subkey, opt, sam, ctypes.byref(result)) + return result.value + + def RegQueryValueEx(hkey, valuename): + data_type = ctypes.c_uint(0) + data_len = ctypes.c_uint(1024) + data = ctypes.create_string_buffer(1024) + + # this has a return value, which we should probably check + ctypes.windll.advapi32.RegQueryValueExA( + hkey, valuename, 0, ctypes.byref(data_type), + data, ctypes.byref(data_len) + ) + + return data.value + + RegCloseKey = ctypes.windll.advapi32.RegCloseKey + + except ImportError: + # Print a messaged suggesting they install these? + pass + + if RegOpenKeyEx is not None: + # Get the GraphViz install path from the registry + hkey = None + potentialKeys = [ + "SOFTWARE\\ATT\\Graphviz", + "SOFTWARE\\AT&T Research Labs\\Graphviz" + ] + for potentialKey in potentialKeys: + + try: + hkey = RegOpenKeyEx( + HKEY_LOCAL_MACHINE, + potentialKey, 0, KEY_QUERY_VALUE + ) + + if hkey is not None: + path = RegQueryValueEx(hkey, "InstallPath") + RegCloseKey(hkey) + + # The regitry variable might exist, left by old installations + # but with no value, in those cases we keep searching... + if not path: + continue + + # Now append the "bin" subdirectory: + path = os.path.join(path, "bin") + progs = __find_executables(path) + if progs is not None: + #print("Used Windows registry") + return progs + + except Exception: + #raise + pass + else: + break + + # Method 2 (Linux, Windows etc) + if 'PATH' in os.environ: + for path in os.environ['PATH'].split(os.pathsep): + progs = __find_executables(path) + if progs is not None: + #print("Used path") + return progs + + # Method 3 (Windows only) + if os.sys.platform == 'win32': + + # Try and work out the equivalent of "C:\Program Files" on this + # machine (might be on drive D:, or in a different language) + if 'PROGRAMFILES' in os.environ: + # Note, we could also use the win32api to get this + # information, but win32api may not be installed. + path = os.path.join(os.environ['PROGRAMFILES'], 'ATT', 'GraphViz', 'bin') + else: + #Just in case, try the default... + path = r"C:\Program Files\att\Graphviz\bin" + + progs = __find_executables(path) + + if progs is not None: + + #print("Used default install location") + return progs + + for path in ( + '/usr/bin', '/usr/local/bin', + '/opt/local/bin', + '/opt/bin', '/sw/bin', '/usr/share', + '/Applications/Graphviz.app/Contents/MacOS/' + ): + + progs = __find_executables(path) + if progs is not None: + #print("Used path") + return progs + + # Failed to find GraphViz + return None + + +class Common(object): + """Common information to several classes. + + Should not be directly used, several classes are derived from + this one. + """ + + def __getstate__(self): + + dict = copy.copy(self.obj_dict) + + return dict + + def __setstate__(self, state): + + self.obj_dict = state + + def __get_attribute__(self, attr): + """Look for default attributes for this node""" + + attr_val = self.obj_dict['attributes'].get(attr, None) + + if attr_val is None: + # get the defaults for nodes/edges + + default_node_name = self.obj_dict['type'] + + # The defaults for graphs are set on a node named 'graph' + if default_node_name in ('subgraph', 'digraph', 'cluster'): + default_node_name = 'graph' + + g = self.get_parent_graph() + if g is not None: + defaults = g.get_node(default_node_name) + else: + return None + + # Multiple defaults could be set by having repeated 'graph [...]' + # 'node [...]', 'edge [...]' statements. In such case, if the + # same attribute is set in different statements, only the first + # will be returned. In order to get all, one would call the + # get_*_defaults() methods and handle those. Or go node by node + # (of the ones specifying defaults) and modify the attributes + # individually. + # + if not isinstance(defaults, (list, tuple)): + defaults = [defaults] + + for default in defaults: + attr_val = default.obj_dict['attributes'].get(attr, None) + if attr_val: + return attr_val + else: + return attr_val + + return None + + def set_parent_graph(self, parent_graph): + + self.obj_dict['parent_graph'] = parent_graph + + def get_parent_graph(self): + + return self.obj_dict.get('parent_graph', None) + + def set(self, name, value): + """Set an attribute value by name. + + Given an attribute 'name' it will set its value to 'value'. + There's always the possibility of using the methods: + + set_'name'(value) + + which are defined for all the existing attributes. + """ + + self.obj_dict['attributes'][name] = value + + def get(self, name): + """Get an attribute value by name. + + Given an attribute 'name' it will get its value. + There's always the possibility of using the methods: + + get_'name'() + + which are defined for all the existing attributes. + """ + + return self.obj_dict['attributes'].get(name, None) + + def get_attributes(self): + """""" + + return self.obj_dict['attributes'] + + def set_sequence(self, seq): + + self.obj_dict['sequence'] = seq + + def get_sequence(self): + + return self.obj_dict['sequence'] + + def create_attribute_methods(self, obj_attributes): + + #for attr in self.obj_dict['attributes']: + for attr in obj_attributes: + + # Generate all the Setter methods. + # + self.__setattr__( + 'set_' + attr, + lambda x, a=attr: self.obj_dict['attributes'].__setitem__(a, x) + ) + + # Generate all the Getter methods. + # + self.__setattr__('get_' + attr, lambda a=attr: self.__get_attribute__(a)) + + +class Error(Exception): + """General error handling class. + """ + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + +class InvocationException(Exception): + """To indicate that a ploblem occurred while running any of the GraphViz executables. + """ + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + + +class Node(Common): + """A graph node. + + This class represents a graph's node with all its attributes. + + node(name, attribute=value, ...) + + name: node's name + + All the attributes defined in the Graphviz dot language should + be supported. + """ + + def __init__(self, name='', obj_dict=None, **attrs): + + # + # Nodes will take attributes of all other types because the defaults + # for any GraphViz object are dealt with as if they were Node definitions + # + + if obj_dict is not None: + self.obj_dict = obj_dict + else: + self.obj_dict = dict() + + # Copy the attributes + # + self.obj_dict['attributes'] = dict(attrs) + self.obj_dict['type'] = 'node' + self.obj_dict['parent_graph'] = None + self.obj_dict['parent_node_list'] = None + self.obj_dict['sequence'] = None + + # Remove the compass point + # + port = None + if isinstance(name, basestring) and not name.startswith('"'): + idx = name.find(':') + if idx > 0 and idx + 1 < len(name): + name, port = name[:idx], name[idx:] + + if isinstance(name, (long, int)): + name = str(name) + + self.obj_dict['name'] = quote_if_necessary(name) + self.obj_dict['port'] = port + + self.create_attribute_methods(NODE_ATTRIBUTES) + + def set_name(self, node_name): + """Set the node's name.""" + + self.obj_dict['name'] = node_name + + def get_name(self): + """Get the node's name.""" + + return self.obj_dict['name'] + + def get_port(self): + """Get the node's port.""" + + return self.obj_dict['port'] + + def add_style(self, style): + + styles = self.obj_dict['attributes'].get('style', None) + if not styles and style: + styles = [style] + else: + styles = styles.split(',') + styles.append(style) + + self.obj_dict['attributes']['style'] = ','.join(styles) + + def to_string(self): + """Returns a string representation of the node in dot language. + """ + + # RMF: special case defaults for node, edge and graph properties. + # + node = quote_if_necessary(self.obj_dict['name']) + + node_attr = list() + + for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)): + if value is not None: + node_attr.append('%s=%s' % (attr, quote_if_necessary(value))) + else: + node_attr.append(attr) + + # No point in having nodes setting any defaults if the don't set + # any attributes... + # + if node in ('graph', 'node', 'edge') and len(node_attr) == 0: + return '' + + node_attr = ', '.join(node_attr) + + if node_attr: + node += ' [' + node_attr + ']' + + return node + ';' + + +class Edge(Common): + """A graph edge. + + This class represents a graph's edge with all its attributes. + + edge(src, dst, attribute=value, ...) + + src: source node's name + dst: destination node's name + + All the attributes defined in the Graphviz dot language should + be supported. + + Attributes can be set through the dynamically generated methods: + + set_[attribute name], i.e. set_label, set_fontname + + or directly by using the instance's special dictionary: + + Edge.obj_dict['attributes'][attribute name], i.e. + + edge_instance.obj_dict['attributes']['label'] + edge_instance.obj_dict['attributes']['fontname'] + + """ + + def __init__(self, src='', dst='', obj_dict=None, **attrs): + + if isinstance(src, (list, tuple)) and dst == '': + src, dst = src + + if obj_dict is not None: + + self.obj_dict = obj_dict + + else: + + self.obj_dict = dict() + + # Copy the attributes + # + self.obj_dict['attributes'] = dict(attrs) + self.obj_dict['type'] = 'edge' + self.obj_dict['parent_graph'] = None + self.obj_dict['parent_edge_list'] = None + self.obj_dict['sequence'] = None + + if isinstance(src, Node): + src = src.get_name() + + if isinstance(dst, Node): + dst = dst.get_name() + + points = (quote_if_necessary(src), quote_if_necessary(dst)) + + self.obj_dict['points'] = points + + self.create_attribute_methods(EDGE_ATTRIBUTES) + + def get_source(self): + """Get the edges source node name.""" + + return self.obj_dict['points'][0] + + def get_destination(self): + """Get the edge's destination node name.""" + + return self.obj_dict['points'][1] + + def __hash__(self): + return hash(hash(self.get_source()) + hash(self.get_destination())) + + def __eq__(self, edge): + """Compare two edges. + + If the parent graph is directed, arcs linking + node A to B are considered equal and A->B != B->A + + If the parent graph is undirected, any edge + connecting two nodes is equal to any other + edge connecting the same nodes, A->B == B->A + """ + + if not isinstance(edge, Edge): + raise Error("Can't compare and edge to a non-edge object.") + + if self.get_parent_graph().get_top_graph_type() == 'graph': + + # If the graph is undirected, the edge has neither + # source nor destination. + # + if ((self.get_source() == edge.get_source() and + self.get_destination() == edge.get_destination()) or + (edge.get_source() == self.get_destination() and + edge.get_destination() == self.get_source())): + return True + + else: + if (self.get_source() == edge.get_source() and + self.get_destination() == edge.get_destination()): + return True + + return False + + def parse_node_ref(self, node_str): + + if not isinstance(node_str, str): + return node_str + + if node_str.startswith('"') and node_str.endswith('"'): + return node_str + + node_port_idx = node_str.rfind(':') + + if (node_port_idx > 0 and node_str[0] == '"' and + node_str[node_port_idx - 1] == '"'): + return node_str + + if node_port_idx > 0: + a = node_str[:node_port_idx] + b = node_str[node_port_idx + 1:] + + node = quote_if_necessary(a) + + node += ':' + quote_if_necessary(b) + + return node + + return node_str + + def to_string(self): + """Returns a string representation of the edge in dot language. + """ + + src = self.parse_node_ref(self.get_source()) + dst = self.parse_node_ref(self.get_destination()) + + if isinstance(src, frozendict): + edge = [Subgraph(obj_dict=src).to_string()] + elif isinstance(src, (int, long)): + edge = [str(src)] + else: + edge = [src] + + if (self.get_parent_graph() and + self.get_parent_graph().get_top_graph_type() and + self.get_parent_graph().get_top_graph_type() == 'digraph'): + + edge.append('->') + + else: + edge.append('--') + + if isinstance(dst, frozendict): + edge.append(Subgraph(obj_dict=dst).to_string()) + elif isinstance(dst, (int, long)): + edge.append(str(dst)) + else: + edge.append(dst) + + edge_attr = list() + + for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)): + if value is not None: + edge_attr.append('%s=%s' % (attr, quote_if_necessary(value))) + else: + edge_attr.append(attr) + + edge_attr = ', '.join(edge_attr) + + if edge_attr: + edge.append(' [' + edge_attr + ']') + + return ' '.join(edge) + ';' + + +class Graph(Common): + """Class representing a graph in Graphviz's dot language. + + This class implements the methods to work on a representation + of a graph in Graphviz's dot language. + + graph(graph_name='G', graph_type='digraph', + strict=False, suppress_disconnected=False, attribute=value, ...) + + graph_name: + the graph's name + graph_type: + can be 'graph' or 'digraph' + suppress_disconnected: + defaults to False, which will remove from the + graph any disconnected nodes. + simplify: + if True it will avoid displaying equal edges, i.e. + only one edge between two nodes. removing the + duplicated ones. + + All the attributes defined in the Graphviz dot language should + be supported. + + Attributes can be set through the dynamically generated methods: + + set_[attribute name], i.e. set_size, set_fontname + + or using the instance's attributes: + + Graph.obj_dict['attributes'][attribute name], i.e. + + graph_instance.obj_dict['attributes']['label'] + graph_instance.obj_dict['attributes']['fontname'] + """ + + def __init__( + self, graph_name='G', obj_dict=None, graph_type='digraph', strict=False, + suppress_disconnected=False, simplify=False, **attrs): + + if obj_dict is not None: + self.obj_dict = obj_dict + else: + self.obj_dict = dict() + + self.obj_dict['attributes'] = dict(attrs) + + if graph_type not in ['graph', 'digraph']: + raise Error(( + 'Invalid type "%s". Accepted graph types are: ' + 'graph, digraph, subgraph' % graph_type + )) + + self.obj_dict['name'] = quote_if_necessary(graph_name) + self.obj_dict['type'] = graph_type + + self.obj_dict['strict'] = strict + self.obj_dict['suppress_disconnected'] = suppress_disconnected + self.obj_dict['simplify'] = simplify + + self.obj_dict['current_child_sequence'] = 1 + self.obj_dict['nodes'] = dict() + self.obj_dict['edges'] = dict() + self.obj_dict['subgraphs'] = dict() + + self.set_parent_graph(self) + + self.create_attribute_methods(GRAPH_ATTRIBUTES) + + def get_graph_type(self): + return self.obj_dict['type'] + + def get_top_graph_type(self): + parent = self + while True: + parent_ = parent.get_parent_graph() + if parent_ == parent: + break + parent = parent_ + + return parent.obj_dict['type'] + + def set_graph_defaults(self, **attrs): + self.add_node(Node('graph', **attrs)) + + def get_graph_defaults(self, **attrs): + + graph_nodes = self.get_node('graph') + + if isinstance(graph_nodes, (list, tuple)): + return [node.get_attributes() for node in graph_nodes] + + return graph_nodes.get_attributes() + + def set_node_defaults(self, **attrs): + self.add_node(Node('node', **attrs)) + + def get_node_defaults(self, **attrs): + graph_nodes = self.get_node('node') + + if isinstance(graph_nodes, (list, tuple)): + return [node.get_attributes() for node in graph_nodes] + + return graph_nodes.get_attributes() + + def set_edge_defaults(self, **attrs): + self.add_node(Node('edge', **attrs)) + + def get_edge_defaults(self, **attrs): + graph_nodes = self.get_node('edge') + + if isinstance(graph_nodes, (list, tuple)): + return [node.get_attributes() for node in graph_nodes] + + return graph_nodes.get_attributes() + + def set_simplify(self, simplify): + """Set whether to simplify or not. + + If True it will avoid displaying equal edges, i.e. + only one edge between two nodes. removing the + duplicated ones. + """ + + self.obj_dict['simplify'] = simplify + + def get_simplify(self): + """Get whether to simplify or not. + + Refer to set_simplify for more information. + """ + + return self.obj_dict['simplify'] + + def set_type(self, graph_type): + """Set the graph's type, 'graph' or 'digraph'.""" + + self.obj_dict['type'] = graph_type + + def get_type(self): + """Get the graph's type, 'graph' or 'digraph'.""" + + return self.obj_dict['type'] + + def set_name(self, graph_name): + """Set the graph's name.""" + + self.obj_dict['name'] = graph_name + + def get_name(self): + """Get the graph's name.""" + + return self.obj_dict['name'] + + def set_strict(self, val): + """Set graph to 'strict' mode. + + This option is only valid for top level graphs. + """ + + self.obj_dict['strict'] = val + + def get_strict(self, val): + """Get graph's 'strict' mode (True, False). + + This option is only valid for top level graphs. + """ + + return self.obj_dict['strict'] + + def set_suppress_disconnected(self, val): + """Suppress disconnected nodes in the output graph. + + This option will skip nodes in the graph with no incoming or outgoing + edges. This option works also for subgraphs and has effect only in the + current graph/subgraph. + """ + + self.obj_dict['suppress_disconnected'] = val + + def get_suppress_disconnected(self, val): + """Get if suppress disconnected is set. + + Refer to set_suppress_disconnected for more information. + """ + + return self.obj_dict['suppress_disconnected'] + + def get_next_sequence_number(self): + seq = self.obj_dict['current_child_sequence'] + self.obj_dict['current_child_sequence'] += 1 + return seq + + def add_node(self, graph_node): + """Adds a node object to the graph. + + It takes a node object as its only argument and returns + None. + """ + + if not isinstance(graph_node, Node): + raise TypeError('add_node() received a non node class object: ' + str(graph_node)) + + node = self.get_node(graph_node.get_name()) + + if not node: + self.obj_dict['nodes'][graph_node.get_name()] = [graph_node.obj_dict] + + #self.node_dict[graph_node.get_name()] = graph_node.attributes + graph_node.set_parent_graph(self.get_parent_graph()) + else: + self.obj_dict['nodes'][graph_node.get_name()].append(graph_node.obj_dict) + + graph_node.set_sequence(self.get_next_sequence_number()) + + def del_node(self, name, index=None): + """Delete a node from the graph. + + Given a node's name all node(s) with that same name + will be deleted if 'index' is not specified or set + to None. + If there are several nodes with that same name and + 'index' is given, only the node in that position + will be deleted. + + 'index' should be an integer specifying the position + of the node to delete. If index is larger than the + number of nodes with that name, no action is taken. + + If nodes are deleted it returns True. If no action + is taken it returns False. + """ + + if isinstance(name, Node): + name = name.get_name() + + if name in self.obj_dict['nodes']: + if index is not None and index < len(self.obj_dict['nodes'][name]): + del self.obj_dict['nodes'][name][index] + return True + else: + del self.obj_dict['nodes'][name] + return True + + return False + + def get_node(self, name): + """Retrieve a node from the graph. + + Given a node's name the corresponding Node + instance will be returned. + + If one or more nodes exist with that name a list of + Node instances is returned. + An empty list is returned otherwise. + """ + + match = list() + + if name in self.obj_dict['nodes']: + match.extend([ + Node(obj_dict=obj_dict) + for obj_dict + in self.obj_dict['nodes'][name] + ]) + + return match + + def get_nodes(self): + """Get the list of Node instances.""" + + return self.get_node_list() + + def get_node_list(self): + """Get the list of Node instances. + + This method returns the list of Node instances + composing the graph. + """ + + node_objs = list() + + for node, obj_dict_list in self.obj_dict['nodes'].items(): + node_objs.extend([ + Node(obj_dict=obj_d) + for obj_d + in obj_dict_list + ]) + + return node_objs + + def add_edge(self, graph_edge): + """Adds an edge object to the graph. + + It takes a edge object as its only argument and returns + None. + """ + + if not isinstance(graph_edge, Edge): + raise TypeError('add_edge() received a non edge class object: ' + str(graph_edge)) + + edge_points = (graph_edge.get_source(), graph_edge.get_destination()) + + if edge_points in self.obj_dict['edges']: + + edge_list = self.obj_dict['edges'][edge_points] + edge_list.append(graph_edge.obj_dict) + else: + self.obj_dict['edges'][edge_points] = [graph_edge.obj_dict] + + graph_edge.set_sequence(self.get_next_sequence_number()) + graph_edge.set_parent_graph(self.get_parent_graph()) + + def del_edge(self, src_or_list, dst=None, index=None): + """Delete an edge from the graph. + + Given an edge's (source, destination) node names all + matching edges(s) will be deleted if 'index' is not + specified or set to None. + If there are several matching edges and 'index' is + given, only the edge in that position will be deleted. + + 'index' should be an integer specifying the position + of the edge to delete. If index is larger than the + number of matching edges, no action is taken. + + If edges are deleted it returns True. If no action + is taken it returns False. + """ + + if isinstance(src_or_list, (list, tuple)): + if dst is not None and isinstance(dst, (int, long)): + index = dst + src, dst = src_or_list + else: + src, dst = src_or_list, dst + + if isinstance(src, Node): + src = src.get_name() + + if isinstance(dst, Node): + dst = dst.get_name() + + if (src, dst) in self.obj_dict['edges']: + if index is not None and index < len(self.obj_dict['edges'][(src, dst)]): + del self.obj_dict['edges'][(src, dst)][index] + return True + else: + del self.obj_dict['edges'][(src, dst)] + return True + + return False + + def get_edge(self, src_or_list, dst=None): + """Retrieved an edge from the graph. + + Given an edge's source and destination the corresponding + Edge instance(s) will be returned. + + If one or more edges exist with that source and destination + a list of Edge instances is returned. + An empty list is returned otherwise. + """ + + if isinstance(src_or_list, (list, tuple)) and dst is None: + edge_points = tuple(src_or_list) + edge_points_reverse = (edge_points[1], edge_points[0]) + else: + edge_points = (src_or_list, dst) + edge_points_reverse = (dst, src_or_list) + + match = list() + + if edge_points in self.obj_dict['edges'] or ( + self.get_top_graph_type() == 'graph' and + edge_points_reverse in self.obj_dict['edges'] + ): + + edges_obj_dict = self.obj_dict['edges'].get( + edge_points, + self.obj_dict['edges'].get(edge_points_reverse, None)) + + for edge_obj_dict in edges_obj_dict: + match.append( + Edge(edge_points[0], edge_points[1], obj_dict=edge_obj_dict) + ) + + return match + + def get_edges(self): + return self.get_edge_list() + + def get_edge_list(self): + """Get the list of Edge instances. + + This method returns the list of Edge instances + composing the graph. + """ + + edge_objs = list() + + for edge, obj_dict_list in self.obj_dict['edges'].items(): + edge_objs.extend([ + Edge(obj_dict=obj_d) + for obj_d + in obj_dict_list + ]) + + return edge_objs + + def add_subgraph(self, sgraph): + """Adds an subgraph object to the graph. + + It takes a subgraph object as its only argument and returns + None. + """ + + if not isinstance(sgraph, Subgraph) and not isinstance(sgraph, Cluster): + raise TypeError('add_subgraph() received a non subgraph class object:' + str(sgraph)) + + if sgraph.get_name() in self.obj_dict['subgraphs']: + + sgraph_list = self.obj_dict['subgraphs'][sgraph.get_name()] + sgraph_list.append(sgraph.obj_dict) + + else: + self.obj_dict['subgraphs'][sgraph.get_name()] = [sgraph.obj_dict] + + sgraph.set_sequence(self.get_next_sequence_number()) + + sgraph.set_parent_graph(self.get_parent_graph()) + + def get_subgraph(self, name): + """Retrieved a subgraph from the graph. + + Given a subgraph's name the corresponding + Subgraph instance will be returned. + + If one or more subgraphs exist with the same name, a list of + Subgraph instances is returned. + An empty list is returned otherwise. + """ + + match = list() + + if name in self.obj_dict['subgraphs']: + sgraphs_obj_dict = self.obj_dict['subgraphs'].get(name) + + for obj_dict_list in sgraphs_obj_dict: + #match.extend(Subgraph(obj_dict = obj_d) for obj_d in obj_dict_list) + match.append(Subgraph(obj_dict=obj_dict_list)) + + return match + + def get_subgraphs(self): + return self.get_subgraph_list() + + def get_subgraph_list(self): + """Get the list of Subgraph instances. + + This method returns the list of Subgraph instances + in the graph. + """ + + sgraph_objs = list() + + for sgraph, obj_dict_list in self.obj_dict['subgraphs'].items(): + sgraph_objs.extend([ + Subgraph(obj_dict=obj_d) + for obj_d + in obj_dict_list + ]) + + return sgraph_objs + + def set_parent_graph(self, parent_graph): + + self.obj_dict['parent_graph'] = parent_graph + + for obj_list in self.obj_dict['nodes'].values(): + for obj in obj_list: + obj['parent_graph'] = parent_graph + + for obj_list in self.obj_dict['edges'].values(): + for obj in obj_list: + obj['parent_graph'] = parent_graph + + for obj_list in self.obj_dict['subgraphs'].values(): + for obj in obj_list: + Graph(obj_dict=obj).set_parent_graph(parent_graph) + + def to_string(self): + """Returns a string representation of the graph in dot language. + + It will return the graph and all its subelements in string from. + """ + + graph = list() + + if self.obj_dict.get('strict', None) is not None: + if self == self.get_parent_graph() and self.obj_dict['strict']: + graph.append('strict ') + + if self.obj_dict['name'] == '': + if 'show_keyword' in self.obj_dict and self.obj_dict['show_keyword']: + graph.append('subgraph {\n') + else: + graph.append('{\n') + else: + graph.append('%s %s {\n' % (self.obj_dict['type'], self.obj_dict['name'])) + + for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)): + if value is not None: + graph.append('%s=%s' % (attr, quote_if_necessary(value))) + else: + graph.append(attr) + + graph.append(';\n') + + edges_done = set() + + edge_obj_dicts = list() + for e in self.obj_dict['edges'].values(): + edge_obj_dicts.extend(e) + + if edge_obj_dicts: + edge_src_set, edge_dst_set = list(zip(*[obj['points'] for obj in edge_obj_dicts])) + edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set) + else: + edge_src_set, edge_dst_set = set(), set() + + node_obj_dicts = list() + for e in self.obj_dict['nodes'].values(): + node_obj_dicts.extend(e) + + sgraph_obj_dicts = list() + for sg in self.obj_dict['subgraphs'].values(): + sgraph_obj_dicts.extend(sg) + + obj_list = sorted([ + (obj['sequence'], obj) + for obj + in (edge_obj_dicts + node_obj_dicts + sgraph_obj_dicts) + ]) + + for idx, obj in obj_list: + if obj['type'] == 'node': + node = Node(obj_dict=obj) + + if self.obj_dict.get('suppress_disconnected', False): + if (node.get_name() not in edge_src_set and + node.get_name() not in edge_dst_set): + continue + + graph.append(node.to_string() + '\n') + + elif obj['type'] == 'edge': + edge = Edge(obj_dict=obj) + + if self.obj_dict.get('simplify', False) and edge in edges_done: + continue + + graph.append(edge.to_string() + '\n') + edges_done.add(edge) + else: + sgraph = Subgraph(obj_dict=obj) + graph.append(sgraph.to_string() + '\n') + + graph.append('}\n') + + return ''.join(graph) + + +class Subgraph(Graph): + + """Class representing a subgraph in Graphviz's dot language. + + This class implements the methods to work on a representation + of a subgraph in Graphviz's dot language. + + subgraph(graph_name='subG', suppress_disconnected=False, attribute=value, ...) + + graph_name: + the subgraph's name + suppress_disconnected: + defaults to false, which will remove from the + subgraph any disconnected nodes. + All the attributes defined in the Graphviz dot language should + be supported. + + Attributes can be set through the dynamically generated methods: + + set_[attribute name], i.e. set_size, set_fontname + + or using the instance's attributes: + + Subgraph.obj_dict['attributes'][attribute name], i.e. + + subgraph_instance.obj_dict['attributes']['label'] + subgraph_instance.obj_dict['attributes']['fontname'] + """ + + # RMF: subgraph should have all the attributes of graph so it can be passed + # as a graph to all methods + # + def __init__( + self, graph_name='', obj_dict=None, suppress_disconnected=False, + simplify=False, **attrs): + + Graph.__init__( + self, graph_name=graph_name, obj_dict=obj_dict, + suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs) + + if obj_dict is None: + self.obj_dict['type'] = 'subgraph' + + +class Cluster(Graph): + + """Class representing a cluster in Graphviz's dot language. + + This class implements the methods to work on a representation + of a cluster in Graphviz's dot language. + + cluster(graph_name='subG', suppress_disconnected=False, attribute=value, ...) + + graph_name: + the cluster's name (the string 'cluster' will be always prepended) + suppress_disconnected: + defaults to false, which will remove from the + cluster any disconnected nodes. + All the attributes defined in the Graphviz dot language should + be supported. + + Attributes can be set through the dynamically generated methods: + + set_[attribute name], i.e. set_color, set_fontname + + or using the instance's attributes: + + Cluster.obj_dict['attributes'][attribute name], i.e. + + cluster_instance.obj_dict['attributes']['label'] + cluster_instance.obj_dict['attributes']['fontname'] + """ + + def __init__( + self, graph_name='subG', obj_dict=None, suppress_disconnected=False, + simplify=False, **attrs): + + Graph.__init__( + self, graph_name=graph_name, obj_dict=obj_dict, + suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs + ) + + if obj_dict is None: + self.obj_dict['type'] = 'subgraph' + self.obj_dict['name'] = 'cluster_' + graph_name + + self.create_attribute_methods(CLUSTER_ATTRIBUTES) + + +class Dot(Graph): + """A container for handling a dot language file. + + This class implements methods to write and process + a dot language file. It is a derived class of + the base class 'Graph'. + """ + + def __init__(self, *argsl, **argsd): + Graph.__init__(self, *argsl, **argsd) + + self.shape_files = list() + self.progs = None + self.formats = [ + 'canon', 'cmap', 'cmapx', 'cmapx_np', 'dia', 'dot', + 'fig', 'gd', 'gd2', 'gif', 'hpgl', 'imap', 'imap_np', 'ismap', + 'jpe', 'jpeg', 'jpg', 'mif', 'mp', 'pcl', 'pdf', 'pic', 'plain', + 'plain-ext', 'png', 'ps', 'ps2', 'svg', 'svgz', 'vml', 'vmlz', + 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib' + ] + self.prog = 'dot' + + # Automatically creates all the methods enabling the creation + # of output in any of the supported formats. + for frmt in self.formats: + self.__setattr__( + 'create_' + frmt, + lambda f=frmt, prog=self.prog: self.create(format=f, prog=prog) + ) + f = self.__dict__['create_' + frmt] + f.__doc__ = ( + '''Refer to the docstring accompanying the''' + ''''create' method for more information.''' + ) + + for frmt in self.formats + ['raw']: + self.__setattr__( + 'write_' + frmt, + lambda path, f=frmt, prog=self.prog: self.write(path, format=f, prog=prog) + ) + + f = self.__dict__['write_' + frmt] + f.__doc__ = ( + '''Refer to the docstring accompanying the''' + ''''write' method for more information.''' + ) + + def __getstate__(self): + return copy.copy(self.obj_dict) + + def __setstate__(self, state): + self.obj_dict = state + + def set_shape_files(self, file_paths): + """Add the paths of the required image files. + + If the graph needs graphic objects to be used as shapes or otherwise + those need to be in the same folder as the graph is going to be rendered + from. Alternatively the absolute path to the files can be specified when + including the graphics in the graph. + + The files in the location pointed to by the path(s) specified as arguments + to this method will be copied to the same temporary location where the + graph is going to be rendered. + """ + + if isinstance(file_paths, basestring): + self.shape_files.append(file_paths) + + if isinstance(file_paths, (list, tuple)): + self.shape_files.extend(file_paths) + + def set_prog(self, prog): + """Sets the default program. + + Sets the default program in charge of processing + the dot file into a graph. + """ + self.prog = prog + + def set_graphviz_executables(self, paths): + """This method allows to manually specify the location of the GraphViz executables. + + The argument to this method should be a dictionary where the keys are as follows: + + {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': ''} + + and the values are the paths to the corresponding executable, including the name + of the executable itself. + """ + + self.progs = paths + + def write(self, path, prog=None, format='raw'): + """ + Given a filename 'path' it will open/create and truncate + such file and write on it a representation of the graph + defined by the dot object and in the format specified by + 'format'. 'path' can also be an open file-like object, such as + a StringIO instance. + + The format 'raw' is used to dump the string representation + of the Dot object, without further processing. + The output can be processed by any of graphviz tools, defined + in 'prog', which defaults to 'dot' + Returns True or False according to the success of the write + operation. + + There's also the preferred possibility of using: + + write_'format'(path, prog='program') + + which are automatically defined for all the supported formats. + [write_ps(), write_gif(), write_dia(), ...] + + """ + if prog is None: + prog = self.prog + + fobj, close = get_fobj(path, 'w+b') + try: + if format == 'raw': + data = self.to_string() + if isinstance(data, basestring): + if not isinstance(data, unicode): + try: + data = unicode(data, 'utf-8') + except: + pass + + try: + charset = self.get_charset() + if not PY3 or not charset: + charset = 'utf-8' + data = data.encode(charset) + except: + if PY3: + data = data.encode('utf-8') + pass + + fobj.write(data) + + else: + fobj.write(self.create(prog, format)) + finally: + if close: + fobj.close() + + return True + + def create(self, prog=None, format='ps'): + """Creates and returns a Postscript representation of the graph. + + create will write the graph to a temporary dot file and process + it with the program given by 'prog' (which defaults to 'twopi'), + reading the Postscript output and returning it as a string is the + operation is successful. + On failure None is returned. + + There's also the preferred possibility of using: + + create_'format'(prog='program') + + which are automatically defined for all the supported formats. + [create_ps(), create_gif(), create_dia(), ...] + + If 'prog' is a list instead of a string the fist item is expected + to be the program name, followed by any optional command-line + arguments for it: + + ['twopi', '-Tdot', '-s10'] + """ + + if prog is None: + prog = self.prog + + if isinstance(prog, (list, tuple)): + prog, args = prog[0], prog[1:] + else: + args = [] + + if self.progs is None: + self.progs = find_graphviz() + if self.progs is None: + raise InvocationException( + 'GraphViz\'s executables not found') + + if prog not in self.progs: + raise InvocationException( + 'GraphViz\'s executable "%s" not found' % prog) + + if not os.path.exists(self.progs[prog]) or not os.path.isfile(self.progs[prog]): + raise InvocationException( + 'GraphViz\'s executable "%s" is not a file or doesn\'t exist' % self.progs[prog]) + + tmp_fd, tmp_name = tempfile.mkstemp() + os.close(tmp_fd) + self.write(tmp_name) + tmp_dir = os.path.dirname(tmp_name) + + # For each of the image files... + for img in self.shape_files: + + # Get its data + f = open(img, 'rb') + f_data = f.read() + f.close() + + # And copy it under a file with the same name in the temporary directory + f = open(os.path.join(tmp_dir, os.path.basename(img)), 'wb') + f.write(f_data) + f.close() + + cmdline = [self.progs[prog], '-T' + format, tmp_name] + args + + p = subprocess.Popen( + cmdline, + cwd=tmp_dir, + stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + stderr = p.stderr + stdout = p.stdout + + stdout_output = list() + while True: + data = stdout.read() + if not data: + break + stdout_output.append(data) + stdout.close() + + stdout_output = NULL_SEP.join(stdout_output) + + if not stderr.closed: + stderr_output = list() + while True: + data = stderr.read() + if not data: + break + stderr_output.append(data) + stderr.close() + + if stderr_output: + stderr_output = NULL_SEP.join(stderr_output) + if PY3: + stderr_output = stderr_output.decode(sys.stderr.encoding) + + #pid, status = os.waitpid(p.pid, 0) + status = p.wait() + + if status != 0: + raise InvocationException( + 'Program terminated with status: %d. stderr follows: %s' % ( + status, stderr_output)) + elif stderr_output: + print(stderr_output) + + # For each of the image files... + for img in self.shape_files: + + # remove it + os.unlink(os.path.join(tmp_dir, os.path.basename(img))) + + os.unlink(tmp_name) + + return stdout_output diff --git a/nxpd/pydot/_dotparser.py b/nxpd/pydot/_dotparser.py new file mode 100644 index 0000000..4cdd482 --- /dev/null +++ b/nxpd/pydot/_dotparser.py @@ -0,0 +1,520 @@ +"""Graphviz's dot language parser. + +The dotparser parses graphviz files in dot and dot files and transforms them +into a class representation defined by pydot. + +The module needs pyparsing (tested with version 1.2.2) and pydot + +Author: Michael Krause +Fixes by: Ero Carrera +""" + +from __future__ import division, print_function + +__author__ = ['Michael Krause', 'Ero Carrera'] +__license__ = 'MIT' + +import sys +import pydot +import codecs + +from pyparsing import __version__ as pyparsing_version + +from pyparsing import ( + nestedExpr, Literal, CaselessLiteral, Word, OneOrMore, + Forward, Group, Optional, Combine, nums, restOfLine, + cStyleComment, alphanums, printables, ParseException, + ParseResults, CharsNotIn, QuotedString + ) + + +PY3 = not sys.version_info < (3, 0, 0) + +if PY3: + basestring = str + + +class P_AttrList: + + def __init__(self, toks): + self.attrs = {} + i = 0 + + while i < len(toks): + attrname = toks[i] + if i + 2 < len(toks) and toks[i + 1] == '=': + attrvalue = toks[i + 2] + i += 3 + else: + attrvalue = None + i += 1 + + self.attrs[attrname] = attrvalue + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.attrs) + + +class DefaultStatement(P_AttrList): + + def __init__(self, default_type, attrs): + self.default_type = default_type + self.attrs = attrs + + def __repr__(self): + return "%s(%s, %r)" % ( + self.__class__.__name__, + self.default_type, self.attrs + ) + + +top_graphs = list() + + +def push_top_graph_stmt(str, loc, toks): + attrs = {} + g = None + + for element in toks: + if (isinstance(element, (ParseResults, tuple, list)) and + len(element) == 1 and isinstance(element[0], basestring)): + element = element[0] + + if element == 'strict': + attrs['strict'] = True + + elif element in ['graph', 'digraph']: + attrs = {} + + g = pydot.Dot(graph_type=element, **attrs) + attrs['type'] = element + + top_graphs.append(g) + + elif isinstance(element, basestring): + g.set_name(element) + + elif isinstance(element, pydot.Subgraph): + g.obj_dict['attributes'].update(element.obj_dict['attributes']) + g.obj_dict['edges'].update(element.obj_dict['edges']) + g.obj_dict['nodes'].update(element.obj_dict['nodes']) + g.obj_dict['subgraphs'].update(element.obj_dict['subgraphs']) + g.set_parent_graph(g) + + elif isinstance(element, P_AttrList): + attrs.update(element.attrs) + + elif isinstance(element, (ParseResults, list)): + add_elements(g, element) + + else: + raise ValueError("Unknown element statement: %r " % element) + + for g in top_graphs: + update_parent_graph_hierarchy(g) + + if len(top_graphs) == 1: + return top_graphs[0] + + return top_graphs + + +def update_parent_graph_hierarchy(g, parent_graph=None, level=0): + if parent_graph is None: + parent_graph = g + + for key_name in ('edges',): + if isinstance(g, pydot.frozendict): + item_dict = g + else: + item_dict = g.obj_dict + + if key_name not in item_dict: + continue + + for key, objs in item_dict[key_name].items(): + for obj in objs: + if 'parent_graph' in obj and obj['parent_graph'].get_parent_graph() == g: + if obj['parent_graph'] is g: + pass + else: + obj['parent_graph'].set_parent_graph(parent_graph) + + if key_name == 'edges' and len(key) == 2: + for idx, vertex in enumerate(obj['points']): + if isinstance(vertex, (pydot.Graph, pydot.Subgraph, pydot.Cluster)): + vertex.set_parent_graph(parent_graph) + if isinstance(vertex, pydot.frozendict): + if vertex['parent_graph'] is g: + pass + else: + vertex['parent_graph'].set_parent_graph(parent_graph) + + +def add_defaults(element, defaults): + d = element.__dict__ + for key, value in defaults.items(): + if not d.get(key): + d[key] = value + + +def add_elements(g, toks, defaults_graph=None, defaults_node=None, defaults_edge=None): + if defaults_graph is None: + defaults_graph = {} + if defaults_node is None: + defaults_node = {} + if defaults_edge is None: + defaults_edge = {} + + for elm_idx, element in enumerate(toks): + if isinstance(element, (pydot.Subgraph, pydot.Cluster)): + add_defaults(element, defaults_graph) + g.add_subgraph(element) + + elif isinstance(element, pydot.Node): + add_defaults(element, defaults_node) + g.add_node(element) + + elif isinstance(element, pydot.Edge): + add_defaults(element, defaults_edge) + g.add_edge(element) + + elif isinstance(element, ParseResults): + for e in element: + add_elements(g, [e], defaults_graph, defaults_node, defaults_edge) + + elif isinstance(element, DefaultStatement): + if element.default_type == 'graph': + default_graph_attrs = pydot.Node('graph', **element.attrs) + g.add_node(default_graph_attrs) + + elif element.default_type == 'node': + default_node_attrs = pydot.Node('node', **element.attrs) + g.add_node(default_node_attrs) + + elif element.default_type == 'edge': + default_edge_attrs = pydot.Node('edge', **element.attrs) + g.add_node(default_edge_attrs) + defaults_edge.update(element.attrs) + + else: + raise ValueError("Unknown DefaultStatement: %s " % element.default_type) + + elif isinstance(element, P_AttrList): + g.obj_dict['attributes'].update(element.attrs) + + else: + raise ValueError("Unknown element statement: %r" % element) + + +def push_graph_stmt(str, loc, toks): + g = pydot.Subgraph('') + add_elements(g, toks) + return g + + +def push_subgraph_stmt(str, loc, toks): + g = pydot.Subgraph('') + + for e in toks: + if len(e) == 3: + e[2].set_name(e[1]) + if e[0] == 'subgraph': + e[2].obj_dict['show_keyword'] = True + return e[2] + else: + if e[0] == 'subgraph': + e[1].obj_dict['show_keyword'] = True + return e[1] + + return g + + +def push_default_stmt(str, loc, toks): + # The pydot class instances should be marked as + # default statements to be inherited by actual + # graphs, nodes and edges. + default_type = toks[0][0] + if len(toks) > 1: + attrs = toks[1].attrs + else: + attrs = {} + + if default_type in ['graph', 'node', 'edge']: + return DefaultStatement(default_type, attrs) + else: + raise ValueError("Unknown default statement: %r " % toks) + + +def push_attr_list(str, loc, toks): + p = P_AttrList(toks) + return p + + +def get_port(node): + if len(node) > 1: + if isinstance(node[1], ParseResults): + if len(node[1][0]) == 2: + if node[1][0][0] == ':': + return node[1][0][1] + return None + + +def do_node_ports(node): + node_port = '' + + if len(node) > 1: + node_port = ''.join([str(a) + str(b) for a, b in node[1]]) + + return node_port + + +def push_edge_stmt(str, loc, toks): + tok_attrs = [a for a in toks if isinstance(a, P_AttrList)] + attrs = {} + + for a in tok_attrs: + attrs.update(a.attrs) + + e = [] + + if isinstance(toks[0][0], pydot.Graph): + n_prev = pydot.frozendict(toks[0][0].obj_dict) + else: + n_prev = toks[0][0] + do_node_ports(toks[0]) + + if isinstance(toks[2][0], ParseResults): + n_next_list = [[n.get_name()] for n in toks[2][0]] + for n_next in [n for n in n_next_list]: + n_next_port = do_node_ports(n_next) + e.append(pydot.Edge(n_prev, n_next[0] + n_next_port, **attrs)) + + elif isinstance(toks[2][0], pydot.Graph): + e.append(pydot.Edge(n_prev, pydot.frozendict(toks[2][0].obj_dict), **attrs)) + + elif isinstance(toks[2][0], pydot.Node): + node = toks[2][0] + + if node.get_port() is not None: + name_port = node.get_name() + ":" + node.get_port() + else: + name_port = node.get_name() + + e.append(pydot.Edge(n_prev, name_port, **attrs)) + + elif isinstance(toks[2][0], type('')): + for n_next in [n for n in tuple(toks)[2::2]]: + if isinstance(n_next, P_AttrList) or not isinstance(n_next[0], type('')): + continue + + n_next_port = do_node_ports(n_next) + e.append(pydot.Edge(n_prev, n_next[0] + n_next_port, **attrs)) + + n_prev = n_next[0] + n_next_port + + else: + # UNEXPECTED EDGE TYPE + pass + + return e + + +def push_node_stmt(s, loc, toks): + + if len(toks) == 2: + attrs = toks[1].attrs + else: + attrs = {} + + node_name = toks[0] + if isinstance(node_name, list) or isinstance(node_name, tuple): + if len(node_name) > 0: + node_name = node_name[0] + + n = pydot.Node(str(node_name), **attrs) + return n + + +graphparser = None + + +def graph_definition(): + global graphparser + + if not graphparser: + # punctuation + colon = Literal(":") + lbrace = Literal("{") + rbrace = Literal("}") + lbrack = Literal("[") + rbrack = Literal("]") + lparen = Literal("(") + rparen = Literal(")") + equals = Literal("=") + comma = Literal(",") + # dot = Literal(".") + # slash = Literal("/") + # bslash = Literal("\\") + # star = Literal("*") + semi = Literal(";") + at = Literal("@") + minus = Literal("-") + + # keywords + strict_ = CaselessLiteral("strict") + graph_ = CaselessLiteral("graph") + digraph_ = CaselessLiteral("digraph") + subgraph_ = CaselessLiteral("subgraph") + node_ = CaselessLiteral("node") + edge_ = CaselessLiteral("edge") + + # token definitions + identifier = Word(alphanums + "_.").setName("identifier") + + # dblQuotedString + double_quoted_string = QuotedString('"', multiline=True, unquoteResults=False) + + noncomma_ = "".join([c for c in printables if c != ","]) + alphastring_ = OneOrMore(CharsNotIn(noncomma_ + ' ')) + + def parse_html(s, loc, toks): + return '<%s>' % ''.join(toks[0]) + + opener = '<' + closer = '>' + html_text = nestedExpr( + opener, closer, + (CharsNotIn(opener + closer)) + ).setParseAction(parse_html).leaveWhitespace() + + ID = ( + identifier | html_text | + double_quoted_string | # .setParseAction(strip_quotes) | + alphastring_ + ).setName("ID") + + float_number = Combine( + Optional(minus) + + OneOrMore(Word(nums + ".")) + ).setName("float_number") + + righthand_id = (float_number | ID).setName("righthand_id") + + port_angle = (at + ID).setName("port_angle") + + port_location = ( + OneOrMore(Group(colon + ID)) | + Group(colon + lparen + ID + comma + ID + rparen) + ).setName("port_location") + + port = ( + Group(port_location + Optional(port_angle)) | + Group(port_angle + Optional(port_location)) + ).setName("port") + + node_id = (ID + Optional(port)) + a_list = OneOrMore( + ID + Optional(equals + righthand_id) + Optional(comma.suppress()) + ).setName("a_list") + + attr_list = OneOrMore( + lbrack.suppress() + Optional(a_list) + rbrack.suppress() + ).setName("attr_list") + + attr_stmt = (Group(graph_ | node_ | edge_) + attr_list).setName("attr_stmt") + + edgeop = (Literal("--") | Literal("->")).setName("edgeop") + + stmt_list = Forward() + graph_stmt = Group( + lbrace.suppress() + Optional(stmt_list) + + rbrace.suppress() + Optional(semi.suppress()) + ).setName("graph_stmt") + + edge_point = Forward() + + edgeRHS = OneOrMore(edgeop + edge_point) + edge_stmt = edge_point + edgeRHS + Optional(attr_list) + + subgraph = Group(subgraph_ + Optional(ID) + graph_stmt).setName("subgraph") + + edge_point << Group(subgraph | graph_stmt | node_id).setName('edge_point') + + node_stmt = ( + node_id + Optional(attr_list) + Optional(semi.suppress()) + ).setName("node_stmt") + + assignment = (ID + equals + righthand_id).setName("assignment") + stmt = ( + assignment | edge_stmt | attr_stmt | + subgraph | graph_stmt | node_stmt + ).setName("stmt") + stmt_list << OneOrMore(stmt + Optional(semi.suppress())) + + graphparser = OneOrMore(( + Optional(strict_) + Group((graph_ | digraph_)) + + Optional(ID) + graph_stmt + ).setResultsName("graph")) + + singleLineComment = Group("//" + restOfLine) | Group("#" + restOfLine) + + # actions + graphparser.ignore(singleLineComment) + graphparser.ignore(cStyleComment) + + assignment.setParseAction(push_attr_list) + a_list.setParseAction(push_attr_list) + edge_stmt.setParseAction(push_edge_stmt) + node_stmt.setParseAction(push_node_stmt) + attr_stmt.setParseAction(push_default_stmt) + + subgraph.setParseAction(push_subgraph_stmt) + graph_stmt.setParseAction(push_graph_stmt) + graphparser.setParseAction(push_top_graph_stmt) + + return graphparser + + +def parse_dot_data(data): + global top_graphs + + top_graphs = list() + + if PY3: + if isinstance(data, bytes): + # this is extremely hackish + try: + idx = data.index(b'charset') + 7 + while data[idx] in b' \t\n\r=': + idx += 1 + fst = idx + while data[idx] not in b' \t\n\r];,': + idx += 1 + charset = data[fst:idx].strip(b'"\'').decode('ascii') + data = data.decode(charset) + except: + data = data.decode('utf-8') + else: + if data.startswith(codecs.BOM_UTF8): + data = data.decode('utf-8') + + try: + + graphparser = graph_definition() + + if pyparsing_version >= '1.2': + graphparser.parseWithTabs() + + tokens = graphparser.parseString(data) + + if len(tokens) == 1: + return tokens[0] + else: + return [g for g in tokens] + + except ParseException: + err = sys.exc_info()[1] + print(err.line) + print(" " * (err.column - 1) + "^") + print(err) + return None diff --git a/nxpd/utils.py b/nxpd/utils.py new file mode 100644 index 0000000..b21ec16 --- /dev/null +++ b/nxpd/utils.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +import os +import subprocess +import sys + +def default_opener(filename): + """Opens `filename` using system's default program. + + Parameters + ---------- + filename : str + The path of the file to be opened. + + """ + cmds = {'darwin': ['open'], + 'linux2': ['xdg-open'], + 'win32': ['cmd.exe', '/c', 'start', '']} + cmd = cmds[sys.platform] + [filename] + subprocess.call(cmd) + +def is_string_like(obj): # from John Hunter, types-free version + """Check if obj is string.""" + try: + obj + '' + except (TypeError, ValueError): + return False + return True + +def get_fobj(fname, mode='w+'): + """Obtain a proper file object. + + Parameters + ---------- + fname : string, file object, file descriptor + If a string or file descriptor, then we create a file object. If *fname* + is a file object, then we do nothing and ignore the specified *mode* + parameter. + mode : str + The mode of the file to be opened. + + Returns + ------- + fobj : file object + The file object. + close : bool + If *fname* was a string, then *close* will be *True* to signify that + the file object should be closed after writing to it. Otherwise, *close* + will be *False* signifying that the user, in essence, created the file + object already and that subsequent operations should not close it. + + """ + if is_string_like(fname): + fobj = open(fname, mode) + close = True + elif hasattr(fname, 'write'): + # fname is a file-like object, perhaps a StringIO (for example) + fobj = fname + close = False + else: + # assume it is a file descriptor + fobj = os.fdopen(fname, mode) + close = False + return fobj, close + +def make_str(t): + """Return the string representation of t.""" + if is_string_like(t): return t + return str(t) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ef50db3 --- /dev/null +++ b/setup.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Setup script for `nxpd`. + +""" + +from __future__ import print_function + +import os +import sys + +from distutils.core import setup + +def main(): + + requires = [ + 'networkx(>=1.6)', + 'pyparsing(>=2.0.1)', + ] + + packages = [ + 'nxpd', + 'nxpd.pydot', + ] + + description = """ +`nxpd` is a Python package for visualizing NetworkX graphs using `pydot` +and `graphviz`. Support is also provided for inline displays within IPython +notebooks. +""" + setup( + name = "nxpd", + version = "0.1", + url = "https://github.com/chebee7i/nxpd", + + packages = packages, + provides = ['nxpd'], + requires = requires, + + author = "chebee7i", + author_email = "chebee7i@gmail.com", + description = "NetworkX Pydot Draw", + long_description = description, + license = "Unlicense", + ) + +if __name__ == '__main__': + + v = sys.version_info[:2] + if v < (2, 6): + msg = "nxpd requires Python 2.6 or newer.\n" + print(msg) + sys.exit(-1) + + if sys.argv[-1] == 'setup.py': + print("To install, run 'python setup.py install'.\n") + + main()