dot_parser.py:
-Improved the parsing of attributes with no explicit value but implicit defaults -Improved handling of subgraphs -Improved handling of whitespace within HTML node names/labels pydot.py: -Updated Graph, Cluster, Subgraph, Node and Edge attributes to reflect the latest GraphViz version (2.26.3) -Improved the parsing of attributes with no explicit value but implicit defaults -Improved handling of boolean attributes -Fixed issue 17, 12 -Fixed issues 19, 29, 35, 37 finding the Graphviz binary in Windows -Added method del_node() to delete Nodes from the graph -Added method del_edges() to delete Edges from the graph -get_node() will now always return a list of nodes -get_edge() will now always return a list of edges -get_subgraph() will now always return a list of edges -Other minor improvements git-svn-id: http://pydot.googlecode.com/svn/trunk@10 06aa9b79-7134-0410-ae7e-c1cd3e483e87
This commit is contained in:
parent
73906aa58f
commit
0afd5e81b9
|
@ -7,6 +7,7 @@ into a class representation defined by pydot.
|
|||
The module needs pyparsing (tested with version 1.2.2) and pydot (tested with 0.9.10)
|
||||
|
||||
Author: Michael Krause <michael@krause-software.de>
|
||||
Fixes by: Ero Carrera <ero@dkbza.org>
|
||||
"""
|
||||
|
||||
__author__ = ['Michael Krause', 'Ero Carrera']
|
||||
|
@ -17,6 +18,7 @@ import sys
|
|||
import glob
|
||||
import pydot
|
||||
import re
|
||||
import codecs
|
||||
|
||||
from pyparsing import __version__ as pyparsing_version
|
||||
|
||||
|
@ -26,7 +28,6 @@ from pyparsing import ( nestedExpr, Literal, CaselessLiteral, Word, Upcase, OneO
|
|||
ParseException, ParseResults, CharsNotIn, _noncomma, dblQuotedString, QuotedString, ParserElement )
|
||||
|
||||
|
||||
|
||||
class P_AttrList:
|
||||
|
||||
def __init__(self, toks):
|
||||
|
@ -35,12 +36,17 @@ class P_AttrList:
|
|||
i = 0
|
||||
|
||||
while i < len(toks):
|
||||
|
||||
attrname = toks[i]
|
||||
attrvalue = toks[i+1]
|
||||
if i+2 < len(toks) and toks[i+1] == '=':
|
||||
attrvalue = toks[i+2]
|
||||
i += 3
|
||||
else:
|
||||
attrvalue = None
|
||||
i += 1
|
||||
|
||||
self.attrs[attrname] = attrvalue
|
||||
i += 2
|
||||
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
return "%s(%r)" % (self.__class__.__name__, self.attrs)
|
||||
|
@ -191,14 +197,19 @@ def push_graph_stmt(str, loc, toks):
|
|||
return g
|
||||
|
||||
|
||||
def push_subgraph_stmt(str, loc, toks):
|
||||
def push_subgraph_stmt(str, loc, toks):
|
||||
|
||||
g = pydot.Subgraph('')
|
||||
|
||||
for e in toks:
|
||||
if len(e)==3:
|
||||
g = e[2]
|
||||
g.set_name(e[1])
|
||||
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
|
||||
|
||||
|
@ -208,7 +219,6 @@ 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.
|
||||
# print "push_default_stmt", toks
|
||||
#
|
||||
default_type = toks[0][0]
|
||||
if len(toks) > 1:
|
||||
|
@ -366,19 +376,17 @@ def graph_definition():
|
|||
|
||||
double_quoted_string = QuotedString('"', multiline=True, unquoteResults=False) # dblQuotedString
|
||||
|
||||
alphastring_ = OneOrMore(CharsNotIn(_noncomma))
|
||||
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( lambda t:t[0] ))
|
||||
).setParseAction(parse_html)
|
||||
( CharsNotIn( opener + closer ) )
|
||||
).setParseAction(parse_html).leaveWhitespace()
|
||||
|
||||
ID = ( identifier | html_text |
|
||||
double_quoted_string | #.setParseAction(strip_quotes) |
|
||||
|
@ -399,16 +407,16 @@ def graph_definition():
|
|||
Group(port_angle + Optional(port_location))).setName("port")
|
||||
|
||||
node_id = (ID + Optional(port))
|
||||
a_list = OneOrMore(ID + Optional(equals.suppress() + righthand_id) +
|
||||
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")
|
||||
|
@ -421,17 +429,17 @@ def graph_definition():
|
|||
|
||||
subgraph = Group(subgraph_ + Optional(ID) + graph_stmt).setName("subgraph")
|
||||
|
||||
edge_point << Group(subgraph | graph_stmt | node_id )
|
||||
|
||||
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.suppress() + righthand_id).setName("assignment")
|
||||
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_)) +
|
||||
|
||||
graphparser = OneOrMore( (Optional(strict_) + Group((graph_ | digraph_)) +
|
||||
Optional(ID) + graph_stmt).setResultsName("graph") )
|
||||
|
||||
|
||||
singleLineComment = Group("//" + restOfLine) | Group("#" + restOfLine)
|
||||
|
||||
|
||||
|
@ -439,18 +447,18 @@ def graph_definition():
|
|||
|
||||
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
|
||||
|
||||
|
||||
|
@ -459,7 +467,12 @@ def parse_dot_data(data):
|
|||
global top_graphs
|
||||
|
||||
top_graphs = list()
|
||||
|
||||
|
||||
|
||||
if data.startswith( codecs.BOM_UTF8 ):
|
||||
data = data.decode( 'utf-8' ).lstrip( unicode(codecs.BOM_UTF8, "utf8") )
|
||||
data = data.encode( 'ascii' )
|
||||
|
||||
try:
|
||||
|
||||
graphparser = graph_definition()
|
||||
|
|
513
pydot.py
513
pydot.py
|
@ -11,13 +11,13 @@ Graphviz: http://www.research.att.com/sw/tools/graphviz/
|
|||
DOT Language: http://www.research.att.com/~erg/graphviz/info/lang.html
|
||||
|
||||
Programmed and tested with Graphviz 1.16 and Python 2.3.4 on GNU/Linux
|
||||
by Ero Carrera (c) 2004 [ero@dkbza.org]
|
||||
by Ero Carrera (c) 2004-2010 [ero@dkbza.org]
|
||||
|
||||
Distributed under MIT license [http://opensource.org/licenses/mit-license.html].
|
||||
"""
|
||||
|
||||
__author__ = 'Ero Carrera'
|
||||
__version__ = '1.0.2'
|
||||
__version__ = '1.0.3'
|
||||
__license__ = 'MIT'
|
||||
|
||||
import os
|
||||
|
@ -32,51 +32,53 @@ except Exception, e:
|
|||
|
||||
|
||||
|
||||
GRAPH_ATTRIBUTES = set( ['Damping', 'K', 'URL', 'bb', 'bgcolor', 'center', 'charset',
|
||||
'clusterrank', 'colorscheme', 'comment', 'compound', 'concentrate',
|
||||
'defaultdist', 'dim', 'diredgeconstraints', 'dpi', 'epsilon', 'esep',
|
||||
'fontcolor', 'fontname', 'fontnames', 'fontpath', 'fontsize', 'label',
|
||||
'labeljust', 'labelloc', 'landscape', 'layers', 'layersep', 'levelsgap',
|
||||
'lp', 'margin', 'maxiter', 'mclimit', 'mindist', 'mode', 'model',
|
||||
'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit', 'nslimit1', 'ordering',
|
||||
'orientation', 'outputorder', 'overlap', 'pack', 'packmode', 'pad',
|
||||
'page', 'pagedir', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
|
||||
'resolution', 'root', 'rotate', 'searchsize', 'sep', 'showboxes', 'size',
|
||||
'splines', 'start', 'stylesheet', 'target', 'truecolor', 'viewport',
|
||||
'voro_margin'] + ['rank'] )
|
||||
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', 'label', 'labelURL', 'labelangle',
|
||||
'labeldistance', 'labelfloat', 'labelfontcolor',
|
||||
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', 'pos', 'samehead', 'sametail',
|
||||
'showboxes', 'style', 'tailURL', 'tailclip', 'tailhref',
|
||||
'taillabel', 'tailport', 'tailtarget', 'tailtooltip',
|
||||
'target', 'tooltip', 'weight'] + ['rank'] )
|
||||
'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', 'image', 'imagescale', 'label',
|
||||
'layer', 'margin', 'nojustify', 'orientation', 'peripheries',
|
||||
'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
|
||||
'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'style',
|
||||
'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', 'lp', 'nojustify', 'pencolor', 'peripheries',
|
||||
'style', 'target', 'tooltip'] )
|
||||
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'] )
|
||||
|
||||
|
||||
#
|
||||
|
@ -101,7 +103,7 @@ class frozendict(dict):
|
|||
for arg in args:
|
||||
if isinstance(arg, dict):
|
||||
arg = copy.copy(arg)
|
||||
for k, v in arg.items():
|
||||
for k, v in arg.iteritems():
|
||||
if isinstance(v, dict):
|
||||
arg[k] = frozendict(v)
|
||||
elif isinstance(v, list):
|
||||
|
@ -126,7 +128,7 @@ class frozendict(dict):
|
|||
try:
|
||||
return self._cached_hash
|
||||
except AttributeError:
|
||||
h = self._cached_hash = hash(tuple(sorted(self.items())))
|
||||
h = self._cached_hash = hash(tuple(sorted(self.iteritems())))
|
||||
return h
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -136,7 +138,7 @@ class frozendict(dict):
|
|||
dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
|
||||
|
||||
id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_:,]*$')
|
||||
id_re_num = re.compile('^[0-9]+$')
|
||||
id_re_num = re.compile('^[0-9,]+$')
|
||||
id_re_with_port = re.compile('^.*:([^"]+|[^"]*\"[^"]*\"[^"]*)$')
|
||||
id_re_dbl_quoted = re.compile('^\".*\"$', re.S)
|
||||
id_re_html = re.compile('^<.*>$', re.S)
|
||||
|
@ -155,8 +157,8 @@ def needs_quotes( s ):
|
|||
return False
|
||||
|
||||
chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0]
|
||||
if chars:
|
||||
return False
|
||||
if chars and not id_re_dbl_quoted.match(s):
|
||||
return True
|
||||
|
||||
res = id_re_alpha_nums.match(s)
|
||||
if not res:
|
||||
|
@ -177,6 +179,11 @@ def needs_quotes( s ):
|
|||
|
||||
def quote_if_necessary(s):
|
||||
|
||||
if isinstance(s, bool):
|
||||
if s is True:
|
||||
return 'True'
|
||||
return 'False'
|
||||
|
||||
if not isinstance( s, basestring ):
|
||||
return s
|
||||
|
||||
|
@ -233,8 +240,18 @@ def graph_from_edges(edge_list, node_prefix='', directed=False):
|
|||
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( node_prefix + edge[0], node_prefix + edge[1] )
|
||||
e = Edge( src, dst )
|
||||
graph.add_edge(e)
|
||||
|
||||
return graph
|
||||
|
@ -338,7 +355,7 @@ def __find_executables(path):
|
|||
|
||||
if os.path.isdir(path) :
|
||||
|
||||
for prg in progs.keys():
|
||||
for prg in progs.iterkeys():
|
||||
|
||||
if progs[prg]:
|
||||
continue
|
||||
|
@ -399,31 +416,90 @@ def find_graphviz():
|
|||
# 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, win32con
|
||||
|
||||
# Get the GraphViz install path from the registry
|
||||
#
|
||||
hkey = win32api.RegOpenKeyEx( win32con.HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\ATT\Graphviz", 0, win32con.KEY_QUERY_VALUE )
|
||||
|
||||
path = win32api.RegQueryValueEx( hkey, "InstallPath" )[0]
|
||||
win32api.RegCloseKey( hkey )
|
||||
|
||||
# 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 ImportError :
|
||||
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 )
|
||||
|
||||
res = 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, excp:
|
||||
#raise excp
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
|
||||
# Method 2 (Linux, Windows etc)
|
||||
#
|
||||
if os.environ.has_key('PATH'):
|
||||
|
@ -464,6 +540,7 @@ def find_graphviz():
|
|||
|
||||
for path in (
|
||||
'/usr/bin', '/usr/local/bin',
|
||||
'/opt/local/bin',
|
||||
'/opt/bin', '/sw/bin', '/usr/share',
|
||||
'/Applications/Graphviz.app/Contents/MacOS/' ):
|
||||
|
||||
|
@ -487,20 +564,14 @@ class Common:
|
|||
|
||||
def __getstate__(self):
|
||||
|
||||
dict = copy.copy(self.__dict__)
|
||||
for attr in self.attributes.keys():
|
||||
|
||||
del dict['set_'+attr]
|
||||
del dict['get_'+attr]
|
||||
dict = copy.copy(self.obj_dict)
|
||||
|
||||
return dict
|
||||
|
||||
|
||||
def __setstate__(self, state):
|
||||
|
||||
for k, v in state.items():
|
||||
|
||||
self.__setattr__(k, v)
|
||||
self.obj_dict = state
|
||||
|
||||
|
||||
def __get_attribute__(self, attr):
|
||||
|
@ -518,8 +589,20 @@ class Common:
|
|||
default_node_name = 'graph'
|
||||
|
||||
defaults = self.get_parent_graph().get_node( default_node_name )
|
||||
if defaults:
|
||||
attr_val = defaults.obj_dict['attributes'].get(attr, 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:
|
||||
|
@ -549,14 +632,7 @@ class Common:
|
|||
which are defined for all the existing attributes.
|
||||
"""
|
||||
|
||||
if self.obj_dict['attributes'].has_key(name):
|
||||
self.obj_dict['attributes'][name] = value
|
||||
return True
|
||||
|
||||
# Attribute is not known
|
||||
#
|
||||
return False
|
||||
|
||||
self.obj_dict['attributes'][name] = value
|
||||
|
||||
|
||||
def get(self, name):
|
||||
|
@ -718,8 +794,11 @@ class Node(object, Common):
|
|||
|
||||
node_attr = list()
|
||||
|
||||
for attr, value in self.obj_dict['attributes'].items():
|
||||
node_attr.append( attr + '=' + quote_if_necessary(value) )
|
||||
for attr, value in self.obj_dict['attributes'].iteritems():
|
||||
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
|
||||
|
@ -754,15 +833,22 @@ class Edge(object, Common ):
|
|||
|
||||
set_[attribute name], i.e. set_label, set_fontname
|
||||
|
||||
or using the instance's attributes:
|
||||
or directly by using the instance's special dictionary:
|
||||
|
||||
Edge.[attribute name], i.e. edge_instance.label, edge_instance.fontname
|
||||
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
|
||||
|
@ -804,6 +890,11 @@ class Edge(object, Common ):
|
|||
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.
|
||||
|
||||
|
@ -823,8 +914,8 @@ class Edge(object, Common ):
|
|||
# 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() == get_destination() and self.get_destination() == edge.get_source() ) ):
|
||||
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:
|
||||
|
@ -841,7 +932,7 @@ class Edge(object, Common ):
|
|||
if not isinstance(node_str, str):
|
||||
return node_str
|
||||
|
||||
if node_str.startswith('"') and node_str.endswith('"') and node_str.count('"') % 2 != 0:
|
||||
if node_str.startswith('"') and node_str.endswith('"'):
|
||||
|
||||
return node_str
|
||||
|
||||
|
@ -874,36 +965,42 @@ class Edge(object, Common ):
|
|||
|
||||
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 self.obj_dict['attributes'].items():
|
||||
for attr, value in self.obj_dict['attributes'].iteritems():
|
||||
|
||||
edge_attr.append( attr + '=' + quote_if_necessary(value) )
|
||||
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) + ';'
|
||||
|
||||
|
||||
|
@ -940,7 +1037,10 @@ class Graph(object, Common):
|
|||
|
||||
or using the instance's attributes:
|
||||
|
||||
Graph.[attribute name], i.e. graph_instance.label, graph_instance.fontname
|
||||
Graph.obj_dict['attributes'][attribute name], i.e.
|
||||
|
||||
graph_instance.obj_dict['attributes']['label']
|
||||
graph_instance.obj_dict['attributes']['fontname']
|
||||
"""
|
||||
|
||||
|
||||
|
@ -1003,8 +1103,12 @@ class Graph(object, Common):
|
|||
def get_graph_defaults(self, **attrs):
|
||||
|
||||
graph_nodes = self.get_node('graph')
|
||||
|
||||
return [ node.get_attributes() for node in graph_nodes ]
|
||||
|
||||
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):
|
||||
|
@ -1017,7 +1121,10 @@ class Graph(object, Common):
|
|||
|
||||
graph_nodes = self.get_node('node')
|
||||
|
||||
return [ node.get_attributes() for node in graph_nodes ]
|
||||
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):
|
||||
|
@ -1030,7 +1137,10 @@ class Graph(object, Common):
|
|||
|
||||
graph_nodes = self.get_node('edge')
|
||||
|
||||
return [ node.get_attributes() for node in graph_nodes ]
|
||||
if isinstance( graph_nodes, (list, tuple)):
|
||||
return [ node.get_attributes() for node in graph_nodes ]
|
||||
|
||||
return graph_nodes.get_attributes()
|
||||
|
||||
|
||||
|
||||
|
@ -1106,7 +1216,7 @@ class Graph(object, Common):
|
|||
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
|
||||
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.
|
||||
"""
|
||||
|
@ -1133,6 +1243,7 @@ class Graph(object, Common):
|
|||
return seq
|
||||
|
||||
|
||||
|
||||
def add_node(self, graph_node):
|
||||
"""Adds a node object to the graph.
|
||||
|
||||
|
@ -1161,16 +1272,48 @@ class Graph(object, Common):
|
|||
|
||||
|
||||
|
||||
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 self.obj_dict['nodes'].has_key(name):
|
||||
|
||||
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):
|
||||
"""Retrieved a node from the graph.
|
||||
"""Retrieve a node from the graph.
|
||||
|
||||
Given a node's name the corresponding Node
|
||||
instance will be returned.
|
||||
|
||||
If multiple nodes exist with that name, a list of
|
||||
If one or more nodes exist with that name a list of
|
||||
Node instances is returned.
|
||||
If only one node exists, the instance is returned.
|
||||
None is returned otherwise.
|
||||
An empty list is returned otherwise.
|
||||
"""
|
||||
|
||||
match = list()
|
||||
|
@ -1179,14 +1322,11 @@ class Graph(object, Common):
|
|||
|
||||
match.extend( [ Node( obj_dict = obj_dict ) for obj_dict in self.obj_dict['nodes'][name] ])
|
||||
|
||||
if len(match)==1:
|
||||
return match[0]
|
||||
|
||||
return match
|
||||
|
||||
|
||||
def get_nodes(self):
|
||||
"""Return an iterator."""
|
||||
"""Get the list of Node instances."""
|
||||
|
||||
return self.get_node_list()
|
||||
|
||||
|
@ -1200,7 +1340,7 @@ class Graph(object, Common):
|
|||
|
||||
node_objs = list()
|
||||
|
||||
for node, obj_dict_list in self.obj_dict['nodes'].items():
|
||||
for node, obj_dict_list in self.obj_dict['nodes'].iteritems():
|
||||
node_objs.extend( [ Node( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
|
||||
|
||||
return node_objs
|
||||
|
@ -1234,37 +1374,78 @@ class Graph(object, Common):
|
|||
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def get_edge(self, src, dst):
|
||||
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 self.obj_dict['edges'].has_key( (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:
|
||||
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 will be returned.
|
||||
Edge instance(s) will be returned.
|
||||
|
||||
If multiple edges exist with that source and destination,
|
||||
If one or more edges exist with that source and destination
|
||||
a list of Edge instances is returned.
|
||||
If only one edge exists, the instance is returned.
|
||||
None is returned otherwise.
|
||||
An empty list is returned otherwise.
|
||||
"""
|
||||
|
||||
edge_points = (src, dst)
|
||||
|
||||
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 self.obj_dict['edges'].has_key( (src, dst) ) or (
|
||||
self.get_top_graph_type() == 'graph' and self.obj_dict['edges'].has_key( (dst, src) )):
|
||||
if self.obj_dict['edges'].has_key( edge_points ) or (
|
||||
self.get_top_graph_type() == 'graph' and self.obj_dict['edges'].has_key( edge_points_reverse )):
|
||||
|
||||
edges_obj_dict = self.obj_dict['edges'].get(
|
||||
(src, dst),
|
||||
self.obj_dict['edges'].get( (dst, src), None ))
|
||||
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 ) )
|
||||
|
||||
if len(match)==1:
|
||||
return match[0]
|
||||
|
||||
|
||||
return match
|
||||
|
||||
|
||||
|
@ -1281,7 +1462,7 @@ class Graph(object, Common):
|
|||
|
||||
edge_objs = list()
|
||||
|
||||
for edge, obj_dict_list in self.obj_dict['edges'].items():
|
||||
for edge, obj_dict_list in self.obj_dict['edges'].iteritems():
|
||||
edge_objs.extend( [ Edge( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
|
||||
|
||||
return edge_objs
|
||||
|
@ -1319,30 +1500,27 @@ class Graph(object, Common):
|
|||
Given a subgraph's name the corresponding
|
||||
Subgraph instance will be returned.
|
||||
|
||||
If multiple subgraphs exist with the same name, a list of
|
||||
If one or more subgraphs exist with the same name, a list of
|
||||
Subgraph instances is returned.
|
||||
If only one Subgraph exists, the instance is returned.
|
||||
None is returned otherwise.
|
||||
An empty list is returned otherwise.
|
||||
"""
|
||||
|
||||
match = None
|
||||
match = list()
|
||||
|
||||
if self.obj_dict['subgraphs'].has_key( sgraph.get_name() ):
|
||||
if self.obj_dict['subgraphs'].has_key( name ):
|
||||
|
||||
sgraphs_obj_dict = self.obj_dict['subgraphs'].get( sgraph.get_name() )
|
||||
sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name )
|
||||
|
||||
for obj_dict_list in sgraphs_obj_dict:
|
||||
match = [ Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list ]
|
||||
#match.extend( Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list )
|
||||
match.append( Subgraph( obj_dict = obj_dict_list ) )
|
||||
|
||||
if len(match)==1:
|
||||
return match[0]
|
||||
|
||||
return match
|
||||
|
||||
|
||||
def get_subgraphs(self):
|
||||
|
||||
return get_subgraph_list()
|
||||
return self.get_subgraph_list()
|
||||
|
||||
|
||||
def get_subgraph_list(self):
|
||||
|
@ -1354,7 +1532,7 @@ class Graph(object, Common):
|
|||
|
||||
sgraph_objs = list()
|
||||
|
||||
for sgraph, obj_dict_list in self.obj_dict['subgraphs'].items():
|
||||
for sgraph, obj_dict_list in self.obj_dict['subgraphs'].iteritems():
|
||||
sgraph_objs.extend( [ Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
|
||||
|
||||
return sgraph_objs
|
||||
|
@ -1365,15 +1543,15 @@ class Graph(object, Common):
|
|||
|
||||
self.obj_dict['parent_graph'] = parent_graph
|
||||
|
||||
for obj_list in self.obj_dict['nodes'].values():
|
||||
for obj_list in self.obj_dict['nodes'].itervalues():
|
||||
for obj in obj_list:
|
||||
obj['parent_graph'] = parent_graph
|
||||
|
||||
for obj_list in self.obj_dict['edges'].values():
|
||||
for obj_list in self.obj_dict['edges'].itervalues():
|
||||
for obj in obj_list:
|
||||
obj['parent_graph'] = parent_graph
|
||||
|
||||
for obj_list in self.obj_dict['subgraphs'].values():
|
||||
for obj_list in self.obj_dict['subgraphs'].itervalues():
|
||||
for obj in obj_list:
|
||||
Graph(obj_dict=obj).set_parent_graph(parent_graph)
|
||||
|
||||
|
@ -1395,19 +1573,23 @@ class Graph(object, Common):
|
|||
graph.append('strict ')
|
||||
|
||||
if self.obj_dict['name'] == '':
|
||||
graph.append( '{\n' )
|
||||
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 in self.obj_dict['attributes'].keys():
|
||||
for attr in self.obj_dict['attributes'].iterkeys():
|
||||
|
||||
if self.obj_dict['attributes'].get(attr, None) is not None:
|
||||
|
||||
graph.append( '%s=' % attr )
|
||||
val = self.obj_dict['attributes'].get(attr)
|
||||
|
||||
graph.append( quote_if_necessary(val) )
|
||||
if val is not None:
|
||||
graph.append( '%s=%s' % (attr, quote_if_necessary(val)) )
|
||||
else:
|
||||
graph.append( attr )
|
||||
|
||||
graph.append( ';\n' )
|
||||
|
||||
|
@ -1415,7 +1597,7 @@ class Graph(object, Common):
|
|||
edges_done = set()
|
||||
|
||||
edge_obj_dicts = list()
|
||||
for e in self.obj_dict['edges'].values():
|
||||
for e in self.obj_dict['edges'].itervalues():
|
||||
edge_obj_dicts.extend(e)
|
||||
|
||||
if edge_obj_dicts:
|
||||
|
@ -1425,11 +1607,11 @@ class Graph(object, Common):
|
|||
edge_src_set, edge_dst_set = set(), set()
|
||||
|
||||
node_obj_dicts = list()
|
||||
for e in self.obj_dict['nodes'].values():
|
||||
for e in self.obj_dict['nodes'].itervalues():
|
||||
node_obj_dicts.extend(e)
|
||||
|
||||
sgraph_obj_dicts = list()
|
||||
for sg in self.obj_dict['subgraphs'].values():
|
||||
for sg in self.obj_dict['subgraphs'].itervalues():
|
||||
sgraph_obj_dicts.extend(sg)
|
||||
|
||||
|
||||
|
@ -1455,7 +1637,7 @@ class Graph(object, Common):
|
|||
|
||||
edge = Edge(obj_dict=obj)
|
||||
|
||||
if self.obj_dict.get('simplify', False) and elm in edges_done:
|
||||
if self.obj_dict.get('simplify', False) and edge in edges_done:
|
||||
continue
|
||||
|
||||
graph.append( edge.to_string() + '\n' )
|
||||
|
@ -1496,8 +1678,10 @@ class Subgraph(Graph):
|
|||
|
||||
or using the instance's attributes:
|
||||
|
||||
Subgraph.[attribute name], i.e.
|
||||
subgraph_instance.label, subgraph_instance.fontname
|
||||
Subgraph.obj_dict['attributes'][attribute name], i.e.
|
||||
|
||||
subgraph_instance.obj_dict['attributes']['label']
|
||||
subgraph_instance.obj_dict['attributes']['fontname']
|
||||
"""
|
||||
|
||||
|
||||
|
@ -1541,8 +1725,10 @@ class Cluster(Graph):
|
|||
|
||||
or using the instance's attributes:
|
||||
|
||||
Cluster.[attribute name], i.e.
|
||||
cluster_instance.color, cluster_instance.fontname
|
||||
Cluster.obj_dict['attributes'][attribute name], i.e.
|
||||
|
||||
cluster_instance.obj_dict['attributes']['label']
|
||||
cluster_instance.obj_dict['attributes']['fontname']
|
||||
"""
|
||||
|
||||
|
||||
|
@ -1557,6 +1743,8 @@ class Cluster(Graph):
|
|||
self.obj_dict['type'] = 'subgraph'
|
||||
self.obj_dict['name'] = 'cluster_'+graph_name
|
||||
|
||||
self.create_attribute_methods(CLUSTER_ATTRIBUTES)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1603,23 +1791,19 @@ class Dot(Graph):
|
|||
|
||||
f = self.__dict__['write_'+frmt]
|
||||
f.__doc__ = '''Refer to the docstring accompanying the 'write' method for more information.'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def __getstate__(self):
|
||||
|
||||
dict = copy.copy(self.__dict__)
|
||||
for attr in self.attributes:
|
||||
del dict['set_'+attr]
|
||||
del dict['get_'+attr]
|
||||
|
||||
for k in [ x for x in dict.keys() if
|
||||
x.startswith('write_') or x.startswith('create_') ]:
|
||||
|
||||
del dict[k]
|
||||
|
||||
|
||||
dict = copy.copy(self.obj_dict)
|
||||
|
||||
return dict
|
||||
|
||||
def __setstate__(self, state):
|
||||
|
||||
self.obj_dict = state
|
||||
|
||||
|
||||
def set_shape_files(self, file_paths):
|
||||
"""Add the paths of the required image files.
|
||||
|
@ -1772,8 +1956,7 @@ class Dot(Graph):
|
|||
stdout_output.append(data)
|
||||
stdout.close()
|
||||
|
||||
if stdout_output:
|
||||
stdout_output = ''.join(stdout_output)
|
||||
stdout_output = ''.join(stdout_output)
|
||||
|
||||
if not stderr.closed:
|
||||
stderr_output = list()
|
||||
|
|
2
setup.py
2
setup.py
|
@ -28,4 +28,4 @@ setup( name = 'pydot',
|
|||
long_description = "\n".join(pydot.__doc__.split('\n')),
|
||||
py_modules = ['pydot', 'dot_parser'],
|
||||
install_requires = ['pyparsing', 'setuptools'],
|
||||
data_files = [('.', ['ChangeLog', 'LICENSE', 'README'])] )
|
||||
data_files = [('.', ['LICENSE', 'README'])] )
|
||||
|
|
Loading…
Reference in New Issue