From fa561ada1786a77e737a699e9dcd89efc6fb662d Mon Sep 17 00:00:00 2001 From: Carlos Miguel Jenkins Perez Date: Mon, 1 Dec 2014 17:30:31 -0600 Subject: [PATCH] fix: dev: Improved project layout. --- LICENSE | 2 +- README.rst | 11 +- doc/conf.py | 35 +-- lib/pydotplus/__init__.py | 31 +++ pydot/__init__.py => lib/pydotplus/graph.py | 260 +++++++++++++------- {pydot => lib/pydotplus}/parser.py | 110 ++++++--- lib/pydotplus/version.py | 35 +++ setup.py | 78 ++++-- 8 files changed, 399 insertions(+), 163 deletions(-) create mode 100644 lib/pydotplus/__init__.py rename pydot/__init__.py => lib/pydotplus/graph.py (90%) rename {pydot => lib/pydotplus}/parser.py (81%) create mode 100644 lib/pydotplus/version.py diff --git a/LICENSE b/LICENSE index 159b3c0..fe1be93 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Copyright (c) 2014 Carlos Jenkins Copyright (c) 2014 Lance Hepler -Copyright (c) 2004-2007 Ero Carrera +Copyright (c) 2004-2011 Ero Carrera Copyright (c) 2004-2007 Michael Krause Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.rst b/README.rst index 98e6257..dce3b99 100644 --- a/README.rst +++ b/README.rst @@ -34,11 +34,18 @@ PyDotplus - Python interface to Graphviz's Dot language About ===== -PyDotPlus is a Python 2.7 - Python 3, documented and CI Tested version of the -old pydot project that provides a Python Interface to Graphviz's Dot language. +PyDotPlus is an improved version of the old pydot project that provides a +Python Interface to Graphviz's Dot language. http://pydotplus.readthedocs.org/ +Differences with pydot: + +- Compatible with PyParsing 2.0+. +- Python 2.7 - Python 3 compatible. +- Well documented. +- CI Tested. + Installation ============ diff --git a/doc/conf.py b/doc/conf.py index 04af00c..66590a8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,7 +18,7 @@ import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('../lib/')) # -- General configuration ------------------------------------------------ @@ -186,22 +186,24 @@ htmlhelp_basename = 'PyDotPlusdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'PyDotPlus.tex', u'PyDotPlus Documentation', - u'PyDotPlus Developers', 'manual'), + ( + 'index', 'PyDotPlus.tex', u'PyDotPlus Documentation', + u'PyDotPlus Developers', 'manual' + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -230,8 +232,10 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'pydotplus', u'PyDotPlus Documentation', - [u'PyDotPlus Developers'], 1) + ( + 'index', 'pydotplus', u'PyDotPlus Documentation', + [u'PyDotPlus Developers'], 1 + ) ] # If true, show URL addresses after external links. @@ -244,9 +248,12 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'PyDotPlus', u'PyDotPlus Documentation', - u'PyDotPlus Developers', 'PyDotPlus', 'One line description of project.', - 'Miscellaneous'), + ( + 'index', 'PyDotPlus', u'PyDotPlus Documentation', + u'PyDotPlus Developers', 'PyDotPlus', + 'One line description of project.', + 'Miscellaneous' + ), ] # Documents to append as an appendix to all manuals. diff --git a/lib/pydotplus/__init__.py b/lib/pydotplus/__init__.py new file mode 100644 index 0000000..9f846a9 --- /dev/null +++ b/lib/pydotplus/__init__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Carlos Jenkins +# Copyright (c) 2014 Lance Hepler +# Copyright (c) 2004-2011 Ero Carrera +# Copyright (c) 2004-2007 Michael Krause +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +PyDotPlus module entry. +""" + +from .parser import * # noqa +from .graph import * # noqa diff --git a/pydot/__init__.py b/lib/pydotplus/graph.py similarity index 90% rename from pydot/__init__.py rename to lib/pydotplus/graph.py index a89a7e8..8987ee4 100644 --- a/pydot/__init__.py +++ b/lib/pydotplus/graph.py @@ -1,5 +1,30 @@ -# -*- coding: Latin-1 -*- -"""Graphviz's dot language Python interface. +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Carlos Jenkins +# Copyright (c) 2014 Lance Hepler +# Copyright (c) 2004-2011 Ero Carrera +# Copyright (c) 2004-2007 Michael Krause +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +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. @@ -13,16 +38,10 @@ 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 @@ -63,7 +82,7 @@ GRAPH_ATTRIBUTES = set([ 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin', # for subgraphs 'rank' - ]) +]) EDGE_ATTRIBUTES = set([ @@ -79,7 +98,7 @@ EDGE_ATTRIBUTES = set([ 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport', 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight', 'rank' - ]) +]) NODE_ATTRIBUTES = set([ @@ -92,7 +111,7 @@ NODE_ATTRIBUTES = set([ 'target', 'tooltip', 'vertices', 'width', 'z', # The following are attributes dot2tex 'texlbl', 'texmode' - ]) +]) CLUSTER_ATTRIBUTES = set([ @@ -100,10 +119,10 @@ CLUSTER_ATTRIBUTES = set([ '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 +def is_string_like(obj): # from John Hunter, types-free version """Check if obj is string.""" try: obj + '' @@ -111,13 +130,15 @@ def is_string_like(obj): # from John Hunter, types-free version 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* + 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 @@ -129,9 +150,10 @@ def get_fobj(fname, mode='w+'): 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. + 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): @@ -209,7 +231,7 @@ 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) @@ -241,8 +263,7 @@ def needs_quotes(s): for test_re in [ id_re_alpha_nums, id_re_num, id_re_dbl_quoted, - id_re_html, id_re_alpha_nums_with_ports - ]: + id_re_html, id_re_alpha_nums_with_ports]: if test_re.match(s): return False @@ -372,8 +393,8 @@ def graph_from_adjacency_matrix(matrix, node_prefix='', directed=False): Edge( node_prefix + node_orig, node_prefix + node_dest - ) ) + ) node_dest += 1 node_orig += 1 @@ -410,8 +431,8 @@ def graph_from_incidence_matrix(matrix, node_prefix='', directed=False): Edge( node_prefix + abs(nodes[0]), node_prefix + nodes[1] - ) ) + ) if not directed: graph.set_simplify(True) @@ -431,7 +452,14 @@ def __find_executables(path): """ success = False - progs = {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': '', 'sfdp': ''} + progs = { + 'dot': '', + 'twopi': '', + 'neato': '', + 'circo': '', + 'fdp': '', + 'sfdp': '' + } was_quoted = False path = path.strip() @@ -515,7 +543,9 @@ def find_graphviz(): def RegOpenKeyEx(key, subkey, opt, sam): result = ctypes.c_uint(0) - ctypes.windll.advapi32.RegOpenKeyExA(key, subkey, opt, sam, ctypes.byref(result)) + ctypes.windll.advapi32.RegOpenKeyExA( + key, subkey, opt, sam, ctypes.byref(result) + ) return result.value def RegQueryValueEx(hkey, valuename): @@ -527,7 +557,7 @@ def find_graphviz(): ctypes.windll.advapi32.RegQueryValueExA( hkey, valuename, 0, ctypes.byref(data_type), data, ctypes.byref(data_len) - ) + ) return data.value @@ -543,21 +573,22 @@ def find_graphviz(): 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... + # The regitry variable might exist, left by old + # installations but with no value, in those cases we + # keep searching... if not path: continue @@ -590,7 +621,9 @@ def find_graphviz(): 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') + 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" @@ -606,8 +639,7 @@ def find_graphviz(): '/usr/bin', '/usr/local/bin', '/opt/local/bin', '/opt/bin', '/sw/bin', '/usr/share', - '/Applications/Graphviz.app/Contents/MacOS/' - ): + '/Applications/Graphviz.app/Contents/MacOS/'): progs = __find_executables(path) if progs is not None: @@ -732,11 +764,14 @@ class Common(object): 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)) + self.__setattr__( + 'get_' + attr, + lambda a=attr: self.__get_attribute__(a) + ) class Error(Exception): @@ -750,7 +785,9 @@ class Error(Exception): class InvocationException(Exception): - """To indicate that a ploblem occurred while running any of the GraphViz executables. + """ + To indicate that a ploblem occurred while running any of the GraphViz + executables. """ def __init__(self, value): self.value = value @@ -776,7 +813,8 @@ class Node(Common): # # Nodes will take attributes of all other types because the defaults - # for any GraphViz object are dealt with as if they were Node definitions + # for any GraphViz object are dealt with as if they were Node + # definitions # if obj_dict is not None: @@ -844,7 +882,9 @@ class Node(Common): node_attr = list() - for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)): + 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: @@ -1026,7 +1066,9 @@ class Edge(Common): edge_attr = list() - for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)): + 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: @@ -1077,8 +1119,9 @@ class Graph(Common): """ def __init__( - self, graph_name='G', obj_dict=None, graph_type='digraph', strict=False, - suppress_disconnected=False, simplify=False, **attrs): + 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 @@ -1091,7 +1134,7 @@ class Graph(Common): 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 @@ -1241,17 +1284,24 @@ class Graph(Common): """ if not isinstance(graph_node, Node): - raise TypeError('add_node() received a non node class object: ' + str(graph_node)) + raise TypeError( + 'add_node() received a non node ' + 'class object: {}'.format(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.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) + self.obj_dict['nodes'][graph_node.get_name()].append( + graph_node.obj_dict + ) graph_node.set_sequence(self.get_next_sequence_number()) @@ -1304,7 +1354,7 @@ class Graph(Common): Node(obj_dict=obj_dict) for obj_dict in self.obj_dict['nodes'][name] - ]) + ]) return match @@ -1327,7 +1377,7 @@ class Graph(Common): Node(obj_dict=obj_d) for obj_d in obj_dict_list - ]) + ]) return node_objs @@ -1339,7 +1389,10 @@ class Graph(Common): """ if not isinstance(graph_edge, Edge): - raise TypeError('add_edge() received a non edge class object: ' + str(graph_edge)) + raise TypeError( + 'add_edge() received a non ' + 'edge class object: {}'.format(str(graph_edge)) + ) edge_points = (graph_edge.get_source(), graph_edge.get_destination()) @@ -1384,7 +1437,8 @@ class Graph(Common): 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)]): + 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: @@ -1415,17 +1469,18 @@ class Graph(Common): if edge_points in self.obj_dict['edges'] or ( self.get_top_graph_type() == 'graph' and - edge_points_reverse in self.obj_dict['edges'] - ): + 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) - ) + match.append(Edge( + edge_points[0], + edge_points[1], + obj_dict=edge_obj_dict + )) return match @@ -1446,7 +1501,7 @@ class Graph(Common): Edge(obj_dict=obj_d) for obj_d in obj_dict_list - ]) + ]) return edge_objs @@ -1457,8 +1512,12 @@ class Graph(Common): None. """ - if not isinstance(sgraph, Subgraph) and not isinstance(sgraph, Cluster): - raise TypeError('add_subgraph() received a non subgraph class object:' + str(sgraph)) + if not isinstance(sgraph, Subgraph) and \ + not isinstance(sgraph, Cluster): + raise TypeError( + 'add_subgraph() received a non ' + 'subgraph class object:'.format(str(sgraph)) + ) if sgraph.get_name() in self.obj_dict['subgraphs']: @@ -1489,7 +1548,6 @@ class Graph(Common): 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 @@ -1511,7 +1569,7 @@ class Graph(Common): Subgraph(obj_dict=obj_d) for obj_d in obj_dict_list - ]) + ]) return sgraph_objs @@ -1544,14 +1602,22 @@ class Graph(Common): graph.append('strict ') if self.obj_dict['name'] == '': - if 'show_keyword' in self.obj_dict and self.obj_dict['show_keyword']: + 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'])) + graph.append( + '{} {} {{\n'.format( + self.obj_dict['type'], + self.obj_dict['name'] + ) + ) - for attr, value in sorted(self.obj_dict['attributes'].items(), key=itemgetter(0)): + 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: @@ -1566,7 +1632,9 @@ class Graph(Common): 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 = 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() @@ -1583,7 +1651,7 @@ class Graph(Common): (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': @@ -1620,7 +1688,9 @@ class Subgraph(Graph): 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, ...) + subgraph( + graph_name='subG', suppress_disconnected=False, attribute=value, ... + ) graph_name: the subgraph's name @@ -1651,7 +1721,8 @@ class Subgraph(Graph): Graph.__init__( self, graph_name=graph_name, obj_dict=obj_dict, - suppress_disconnected=suppress_disconnected, simplify=simplify, **attrs) + suppress_disconnected=suppress_disconnected, + simplify=simplify, **attrs) if obj_dict is None: self.obj_dict['type'] = 'subgraph' @@ -1664,7 +1735,9 @@ class Cluster(Graph): 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, ...) + cluster( + graph_name='subG', suppress_disconnected=False, attribute=value, ... + ) graph_name: the cluster's name (the string 'cluster' will be always prepended) @@ -1687,13 +1760,15 @@ class Cluster(Graph): """ def __init__( - self, graph_name='subG', obj_dict=None, suppress_disconnected=False, + 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 - ) + suppress_disconnected=suppress_disconnected, simplify=simplify, + **attrs + ) if obj_dict is None: self.obj_dict['type'] = 'subgraph' @@ -1721,7 +1796,7 @@ class Dot(Graph): '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 @@ -1730,24 +1805,26 @@ class Dot(Graph): 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) - ) + 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) @@ -1759,13 +1836,13 @@ class Dot(Graph): """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. + 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. + 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): @@ -1783,14 +1860,17 @@ class Dot(Graph): self.prog = prog def set_graphviz_executables(self, paths): - """This method allows to manually specify the location of the GraphViz executables. + """ + 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: + 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. + and the values are the paths to the corresponding executable, + including the name of the executable itself. """ self.progs = paths @@ -1893,9 +1973,12 @@ class Dot(Graph): 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]): + 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]) + 'GraphViz\'s executable "{}" is not' + ' a file or doesn\'t exist'.format(self.progs[prog]) + ) tmp_fd, tmp_name = tempfile.mkstemp() os.close(tmp_fd) @@ -1910,7 +1993,8 @@ class Dot(Graph): f_data = f.read() f.close() - # And copy it under a file with the same name in the temporary directory + # 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() diff --git a/pydot/parser.py b/lib/pydotplus/parser.py similarity index 81% rename from pydot/parser.py rename to lib/pydotplus/parser.py index 4cdd482..d0825e2 100644 --- a/pydot/parser.py +++ b/lib/pydotplus/parser.py @@ -1,19 +1,37 @@ -"""Graphviz's dot language parser. +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Carlos Jenkins +# Copyright (c) 2014 Lance Hepler +# Copyright (c) 2004-2011 Ero Carrera +# Copyright (c) 2004-2007 Michael Krause +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +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 @@ -25,7 +43,7 @@ from pyparsing import ( Forward, Group, Optional, Combine, nums, restOfLine, cStyleComment, alphanums, printables, ParseException, ParseResults, CharsNotIn, QuotedString - ) +) PY3 = not sys.version_info < (3, 0, 0) @@ -65,7 +83,7 @@ class DefaultStatement(P_AttrList): return "%s(%s, %r)" % ( self.__class__.__name__, self.default_type, self.attrs - ) + ) top_graphs = list() @@ -134,7 +152,8 @@ def update_parent_graph_hierarchy(g, parent_graph=None, level=0): 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 'parent_graph' in obj and \ + obj['parent_graph'].get_parent_graph() == g: if obj['parent_graph'] is g: pass else: @@ -142,13 +161,18 @@ def update_parent_graph_hierarchy(g, parent_graph=None, level=0): if key_name == 'edges' and len(key) == 2: for idx, vertex in enumerate(obj['points']): - if isinstance(vertex, (pydot.Graph, pydot.Subgraph, pydot.Cluster)): + 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) + vertex['parent_graph'].set_parent_graph( + parent_graph + ) def add_defaults(element, defaults): @@ -158,7 +182,10 @@ def add_defaults(element, defaults): d[key] = value -def add_elements(g, toks, defaults_graph=None, defaults_node=None, defaults_edge=None): +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: @@ -181,7 +208,9 @@ def add_elements(g, toks, defaults_graph=None, defaults_node=None, defaults_edge elif isinstance(element, ParseResults): for e in element: - add_elements(g, [e], defaults_graph, defaults_node, defaults_edge) + add_elements( + g, [e], defaults_graph, defaults_node, defaults_edge + ) elif isinstance(element, DefaultStatement): if element.default_type == 'graph': @@ -198,7 +227,9 @@ def add_elements(g, toks, defaults_graph=None, defaults_node=None, defaults_edge defaults_edge.update(element.attrs) else: - raise ValueError("Unknown DefaultStatement: %s " % element.default_type) + raise ValueError( + "Unknown DefaultStatement: %s " % element.default_type + ) elif isinstance(element, P_AttrList): g.obj_dict['attributes'].update(element.attrs) @@ -290,7 +321,9 @@ def push_edge_stmt(str, loc, toks): 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)) + 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] @@ -304,7 +337,8 @@ def push_edge_stmt(str, loc, toks): 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('')): + if isinstance(n_next, P_AttrList) or \ + not isinstance(n_next[0], type('')): continue n_next_port = do_node_ports(n_next) @@ -372,7 +406,9 @@ def graph_definition(): identifier = Word(alphanums + "_.").setName("identifier") # dblQuotedString - double_quoted_string = QuotedString('"', multiline=True, unquoteResults=False) + double_quoted_string = QuotedString( + '"', multiline=True, unquoteResults=False + ) noncomma_ = "".join([c for c in printables if c != ","]) alphastring_ = OneOrMore(CharsNotIn(noncomma_ + ' ')) @@ -385,18 +421,18 @@ def graph_definition(): html_text = nestedExpr( opener, closer, (CharsNotIn(opener + closer)) - ).setParseAction(parse_html).leaveWhitespace() + ).setParseAction(parse_html).leaveWhitespace() ID = ( identifier | html_text | double_quoted_string | # .setParseAction(strip_quotes) | alphastring_ - ).setName("ID") + ).setName("ID") float_number = Combine( Optional(minus) + OneOrMore(Word(nums + ".")) - ).setName("float_number") + ).setName("float_number") righthand_id = (float_number | ID).setName("righthand_id") @@ -405,23 +441,25 @@ def graph_definition(): port_location = ( OneOrMore(Group(colon + ID)) | Group(colon + lparen + ID + comma + ID + rparen) - ).setName("port_location") + ).setName("port_location") port = ( Group(port_location + Optional(port_angle)) | Group(port_angle + Optional(port_location)) - ).setName("port") + ).setName("port") node_id = (ID + Optional(port)) a_list = OneOrMore( ID + Optional(equals + righthand_id) + Optional(comma.suppress()) - ).setName("a_list") + ).setName("a_list") attr_list = OneOrMore( lbrack.suppress() + Optional(a_list) + rbrack.suppress() - ).setName("attr_list") + ).setName("attr_list") - attr_stmt = (Group(graph_ | node_ | edge_) + attr_list).setName("attr_stmt") + attr_stmt = (Group(graph_ | node_ | edge_) + attr_list).setName( + "attr_stmt" + ) edgeop = (Literal("--") | Literal("->")).setName("edgeop") @@ -429,32 +467,36 @@ def graph_definition(): graph_stmt = Group( lbrace.suppress() + Optional(stmt_list) + rbrace.suppress() + Optional(semi.suppress()) - ).setName("graph_stmt") + ).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") + subgraph = Group( + subgraph_ + Optional(ID) + graph_stmt + ).setName("subgraph") - edge_point << Group(subgraph | graph_stmt | node_id).setName('edge_point') + edge_point << Group(subgraph | graph_stmt | node_id).setName( + 'edge_point' + ) node_stmt = ( node_id + Optional(attr_list) + Optional(semi.suppress()) - ).setName("node_stmt") + ).setName("node_stmt") assignment = (ID + equals + righthand_id).setName("assignment") stmt = ( assignment | edge_stmt | attr_stmt | subgraph | graph_stmt | node_stmt - ).setName("stmt") + ).setName("stmt") stmt_list << OneOrMore(stmt + Optional(semi.suppress())) graphparser = OneOrMore(( Optional(strict_) + Group((graph_ | digraph_)) + Optional(ID) + graph_stmt - ).setResultsName("graph")) + ).setResultsName("graph")) singleLineComment = Group("//" + restOfLine) | Group("#" + restOfLine) diff --git a/lib/pydotplus/version.py b/lib/pydotplus/version.py new file mode 100644 index 0000000..cfd3c23 --- /dev/null +++ b/lib/pydotplus/version.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Carlos Jenkins +# Copyright (c) 2014 Lance Hepler +# Copyright (c) 2004-2011 Ero Carrera +# Copyright (c) 2004-2007 Michael Krause +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +PyDotPlus version module. +""" + +from __future__ import unicode_literals +from __future__ import print_function + +__version__ = '2.0.0' + +__all__ = ['__version__'] diff --git a/setup.py b/setup.py index 0e3dbca..b2e80eb 100644 --- a/setup.py +++ b/setup.py @@ -1,39 +1,69 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Carlos Jenkins +# Copyright (c) 2014 Lance Hepler +# Copyright (c) 2004-2011 Ero Carrera +# Copyright (c) 2004-2007 Michael Krause +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. -try: - from distutils.core import setup -except ImportError: - from setuptools import setup +from setuptools import setup, find_packages -import pydot -import os -os.environ['COPY_EXTENDED_ATTRIBUTES_DISABLE'] = 'true' -os.environ['COPYFILE_DISABLE'] = 'true' +def find_version(filename): + import os + import re + + here = os.path.dirname(os.path.abspath(__file__)) + with open(os.path.join(here, filename)) as fd: + version_match = re.search( + r"^__version__ = ['\"]([^'\"]*)['\"]", fd.read(), re.M + ) + if version_match: + return version_match.group(1) + raise RuntimeError('Unable to find version string.') + setup( - name='pydot', - version=pydot.__version__, - description='Python interface to Graphviz\'s Dot', - author='Ero Carrera', - author_email='ero@dkbza.org', - url='http://code.google.com/p/pydot/', - download_url='http://pydot.googlecode.com/files/pydot-%s.tar.gz' % pydot.__version__, - license='MIT', + name='pydotplus', + version=find_version('lib/pydotplus/version.py'), + package_dir={'' : 'lib'}, + packages=find_packages('lib'), + + # Metadata + author='PyDotPlus Developers', + author_email='carlos@jenkins.co.cr', + description='Python interface to Graphviz\'s Dot Language', + long_description=open('README.rst', 'r').read(), + url='http://pydotplus.readthedocs.org/', + keywords='graphviz dot graphs visualization', - platforms=['any'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', - 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering :: Visualization', 'Topic :: Software Development :: Libraries :: Python Modules' - ], - long_description="\n".join(pydot.__doc__.split('\n')), - packages=['pydot'], - package_dir={'pydot': 'pydot'}, - install_requires=['pyparsing>=2.0.1',], - ) + ], +)