From 1c522fbda337fcf1c2290d3fab8fa82bb931062e Mon Sep 17 00:00:00 2001 From: Alfredo Moralejo Date: Sep 08 2021 13:06:26 +0000 Subject: Import pydot-1.4.1-5.el9 in CloudSIG xena --- diff --git a/.pydot.metadata b/.pydot.metadata new file mode 100644 index 0000000..9a13a87 --- /dev/null +++ b/.pydot.metadata @@ -0,0 +1 @@ +8d2c6ec504aa746159fbf1522d4683e7453dc57e SOURCES/pydot-1.4.1.tar.gz diff --git a/SOURCES/0002-support-python3.patch b/SOURCES/0002-support-python3.patch new file mode 100644 index 0000000..6602ed7 --- /dev/null +++ b/SOURCES/0002-support-python3.patch @@ -0,0 +1,3493 @@ +From 050e291cbf63fd190f1ec14d65d1154a23ce8756 Mon Sep 17 00:00:00 2001 +From: Sandro Tosi +Date: Sun, 10 Jan 2016 00:25:45 +0000 +Subject: support python3 + +Origin: https://github.com/nlhepler/pydot/tree/adf18a858a63b321b7e4ffd964a24d73add1bf4f +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=739858 +Reviewed-by: Sandro Tosi +Last-Update: 2016-01-10 + +--- + dot_parser.py | 429 ++++++++-------- + pydot.py | 1553 +++++++++++++++++++++++++++------------------------------ + setup.py | 6 +- + 3 files changed, 954 insertions(+), 1034 deletions(-) + +diff --git a/dot_parser.py b/dot_parser.py +index dedd61a..4cdd482 100644 +--- a/dot_parser.py ++++ b/dot_parser.py +@@ -1,4 +1,3 @@ +-# -*- coding: Latin-1 -*- + """Graphviz's dot language parser. + + The dotparser parses graphviz files in dot and dot files and transforms them +@@ -10,236 +9,215 @@ Author: Michael Krause + Fixes by: Ero Carrera + """ + ++from __future__ import division, print_function ++ + __author__ = ['Michael Krause', 'Ero Carrera'] + __license__ = 'MIT' + +- + import sys +-import glob + import pydot +-import re + import codecs + + from pyparsing import __version__ as pyparsing_version + +-from pyparsing import ( nestedExpr, Literal, CaselessLiteral, Word, Upcase, OneOrMore, ZeroOrMore, +- Forward, NotAny, delimitedList, oneOf, Group, Optional, Combine, alphas, nums, +- restOfLine, cStyleComment, nums, alphanums, printables, empty, quotedString, +- ParseException, ParseResults, CharsNotIn, _noncomma, dblQuotedString, QuotedString, ParserElement ) ++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] ++ 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): + ++ 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) ++ return "%s(%s, %r)" % ( ++ self.__class__.__name__, ++ self.default_type, self.attrs ++ ) + + + top_graphs = list() + +-def push_top_graph_stmt(str, loc, toks): + ++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) ): +- ++ 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']: + ++ 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 ) +- ++ ++ 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.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 +- +- ++ raise ValueError("Unknown element statement: %r " % element) ++ + for g in top_graphs: + update_parent_graph_hierarchy(g) +- +- if len( top_graphs ) == 1: ++ ++ 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',): + ++ for key_name in ('edges',): + if isinstance(g, pydot.frozendict): + item_dict = g + else: + item_dict = g.obj_dict +- +- if not item_dict.has_key( key_name ): ++ ++ 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 '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)): ++ 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 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 +- ++ 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 ++ raise ValueError("Unknown element statement: %r" % element) + + +-def push_graph_stmt(str, loc, toks): +- ++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: ++ if len(e) == 3: + e[2].set_name(e[1]) + if e[0] == 'subgraph': + e[2].obj_dict['show_keyword'] = True +@@ -253,11 +231,9 @@ def push_subgraph_stmt(str, loc, toks): + + + 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 +@@ -267,90 +243,80 @@ def push_default_stmt(str, loc, toks): + if default_type in ['graph', 'node', 'edge']: + return DefaultStatement(default_type, attrs) + else: +- raise ValueError, "Unknown default statement: %r " % toks ++ 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 len(node) > 1: + if isinstance(node[1], ParseResults): +- if len(node[1][0])==2: +- if node[1][0][0]==':': ++ if len(node[1][0]) == 2: ++ if node[1][0][0] == ':': + return node[1][0][1] +- + return None + +- +-def do_node_ports(node): + ++def do_node_ports(node): + node_port = '' ++ + if len(node) > 1: +- node_port = ''.join( [str(a)+str(b) for a,b in 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] ) ++ 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] ] ++ 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)) ++ 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 +- ++ 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 + ++ return e + + + def push_node_stmt(s, loc, toks): +@@ -359,30 +325,25 @@ def push_node_stmt(s, loc, toks): + 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: ++ if len(node_name) > 0: + node_name = node_name[0] +- ++ + n = pydot.Node(str(node_name), **attrs) + return n + + +- +- +- +- + graphparser = None + +-def graph_definition(): + ++def graph_definition(): + global graphparser +- ++ + if not graphparser: +- + # punctuation +- colon = Literal(":") ++ colon = Literal(":") + lbrace = Literal("{") + rbrace = Literal("}") + lbrack = Literal("[") +@@ -390,142 +351,170 @@ def graph_definition(): + lparen = Literal("(") + rparen = Literal(")") + equals = Literal("=") +- comma = Literal(",") +- dot = Literal(".") +- slash = Literal("/") +- bslash = Literal("\\") +- star = Literal("*") +- semi = Literal(";") +- at = Literal("@") +- minus = 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") +- +- ++ 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") +- +- double_quoted_string = QuotedString('"', multiline=True, unquoteResults=False) # dblQuotedString ++ identifier = Word(alphanums + "_.").setName("identifier") ++ ++ # dblQuotedString ++ double_quoted_string = QuotedString('"', multiline=True, unquoteResults=False) + +- alphastring_ = OneOrMore(CharsNotIn(_noncomma + ' ')) ++ 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") ++ 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") +- ++ ++ 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") +- ++ 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") +- +- ++ 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") +- ++ ++ 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 = ( ++ 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") ) +- ++ ++ 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 data.startswith(codecs.BOM_UTF8): +- data = data.decode( 'utf-8' ) +- ++ 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: +- +- print err.line +- print " "*(err.column-1) + "^" +- print err ++ ++ except ParseException: ++ err = sys.exc_info()[1] ++ print(err.line) ++ print(" " * (err.column - 1) + "^") ++ print(err) + return None +diff --git a/pydot.py b/pydot.py +index e9bd2a1..c20db18 100644 +--- a/pydot.py ++++ b/pydot.py +@@ -17,24 +17,40 @@ Copyright (c) 2005-2011 Ero Carrera + Distributed under MIT license [http://opensource.org/licenses/mit-license.html]. + """ + +-__revision__ = "$LastChangedRevision: 28 $" ++from __future__ import division, print_function ++ + __author__ = 'Ero Carrera' +-__version__ = '1.0.%d' % int( __revision__[21:-2] ) ++__version__ = '1.0.29' + __license__ = 'MIT' + + import os + import re + import subprocess ++import sys + import tempfile + import copy ++ ++from operator import itemgetter ++ + try: + import dot_parser +-except Exception, e: +- print "Couldn't import dot_parser, loading of dot files will not be possible." +- ++except Exception: ++ print("Couldn't import dot_parser, loading of dot files will not be possible.") ++ + ++PY3 = not sys.version_info < (3, 0, 0) + +-GRAPH_ATTRIBUTES = set( ['Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor', ++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', +@@ -46,13 +62,15 @@ GRAPH_ATTRIBUTES = set( ['Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor', + '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', ++ 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start', + 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin', +- # for subgraphs +- 'rank' ] ) ++ # for subgraphs ++ 'rank' ++ ]) + + +-EDGE_ATTRIBUTES = set( ['URL', 'arrowhead', 'arrowsize', 'arrowtail', ++EDGE_ATTRIBUTES = set([ ++ 'URL', 'arrowhead', 'arrowsize', 'arrowtail', + 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir', + 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor', + 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel', +@@ -63,10 +81,12 @@ EDGE_ATTRIBUTES = set( ['URL', 'arrowhead', 'arrowsize', 'arrowtail', + 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes', + 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport', + 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight', +- 'rank' ] ) ++ 'rank' ++ ]) + + +-NODE_ATTRIBUTES = set( ['URL', 'color', 'colorscheme', 'comment', ++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', +@@ -74,14 +94,62 @@ NODE_ATTRIBUTES = set( ['URL', 'color', 'colorscheme', 'comment', + 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style', + 'target', 'tooltip', 'vertices', 'width', 'z', + # The following are attributes dot2tex +- 'texlbl', 'texmode' ] ) ++ 'texlbl', 'texmode' ++ ]) + + +-CLUSTER_ATTRIBUTES = set( ['K', 'URL', 'bgcolor', 'color', 'colorscheme', ++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'] ) +- ++ '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: +@@ -92,7 +160,7 @@ CLUSTER_ATTRIBUTES = set( ['K', 'URL', 'bgcolor', 'color', 'colorscheme', + # + class frozendict(dict): + def _blocked_attribute(obj): +- raise AttributeError, "A frozendict cannot be modified." ++ raise AttributeError("A frozendict cannot be modified.") + _blocked_attribute = property(_blocked_attribute) + + __delitem__ = __setitem__ = clear = _blocked_attribute +@@ -105,7 +173,7 @@ class frozendict(dict): + for arg in args: + if isinstance(arg, dict): + arg = copy.copy(arg) +- for k, v in arg.iteritems(): ++ for k, v in arg.items(): + if isinstance(v, frozendict): + arg[k] = v + elif isinstance(v, dict): +@@ -114,13 +182,13 @@ class frozendict(dict): + v_ = list() + for elm in v: + if isinstance(elm, dict): +- v_.append( frozendict(elm) ) ++ v_.append(frozendict(elm)) + else: +- v_.append( elm ) ++ v_.append(elm) + arg[k] = tuple(v_) +- args_.append( arg ) ++ args_.append(arg) + else: +- args_.append( arg ) ++ args_.append(arg) + + dict.__init__(new, *args_, **kw) + return new +@@ -132,7 +200,7 @@ class frozendict(dict): + try: + return self._cached_hash + except AttributeError: +- h = self._cached_hash = hash(tuple(sorted(self.iteritems()))) ++ h = self._cached_hash = hash(tuple(sorted(self.items()))) + return h + + def __repr__(self): +@@ -142,23 +210,25 @@ class frozendict(dict): + 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_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) ++id_re_dbl_quoted = re.compile('^\".*\"$', re.S | re.UNICODE) ++id_re_html = re.compile('^<.*>$', re.S | re.UNICODE) + + +-def needs_quotes( s ): ++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 +@@ -168,11 +238,14 @@ def needs_quotes( s ): + if s in dot_keywords: + return False + +- chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0] ++ 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]: ++ ++ 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 + +@@ -184,109 +257,109 @@ def needs_quotes( s ): + + + def quote_if_necessary(s): ++ # Older versions of graphviz throws a syntax error for empty values without ++ # quotes, e.g. [label=] ++ if s == '': ++ return '""' + + if isinstance(s, bool): + if s is True: + return 'True' + return 'False' + +- if not isinstance( s, basestring ): ++ 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(): ++ replace = {'"': r'\"', "\n": r'\n', "\r": r'\r'} ++ for (a, b) in replace.items(): + s = s.replace(a, b) + + return '"' + s + '"' +- +- 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, ++ 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, ++ be loaded, parsed and a Dot class will be returned, + representing the graph. + """ +- +- fd = file(path, 'rb') ++ ++ fd = open(path, 'rb') + data = fd.read() + fd.close() +- +- return graph_from_dot_data(data) + ++ 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 ) ++ e = Edge(src, dst) + graph.add_edge(e) +- ++ + return graph + + +-def graph_from_adjacency_matrix(matrix, node_prefix= u'', directed=False): ++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) +@@ -294,320 +367,297 @@ def graph_from_adjacency_matrix(matrix, node_prefix= u'', directed=False): + else: + skip = 0 + r = row +- node_dest = skip+1 +- ++ node_dest = skip + 1 ++ + for e in r: + if e: + graph.add_edge( +- Edge( node_prefix + node_orig, +- node_prefix + node_dest) ) ++ Edge( ++ node_prefix + node_orig, ++ node_prefix + node_dest ++ ) ++ ) + node_dest += 1 + node_orig += 1 +- +- return graph + ++ 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. + """ +- +- node_orig = 1 +- ++ + 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) ++ nodes.append(c * node) + c += 1 +- nodes.sort() +- ++ ++ nodes.sort() ++ + if len(nodes) == 2: +- graph.add_edge( +- Edge( node_prefix + abs(nodes[0]), +- node_prefix + nodes[1] )) ++ 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.iterkeys(): +- ++ 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 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') ): + ++ 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 ++ ++ HKEY_LOCAL_MACHINE = 0x80000002 ++ KEY_QUERY_VALUE = 0x0001 + + RegOpenKeyEx = None + RegQueryValueEx = None + RegCloseKey = None +- ++ + try: +- import win32api, win32con ++ 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 ): ++ ++ 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)) +- ++ 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", +- ] ++ "SOFTWARE\\AT&T Research Labs\\Graphviz" ++ ] + for potentialKey in potentialKeys: +- ++ + try: +- hkey = RegOpenKeyEx( HKEY_LOCAL_MACHINE, +- potentialKey, 0, KEY_QUERY_VALUE ) +- ++ hkey = RegOpenKeyEx( ++ HKEY_LOCAL_MACHINE, ++ potentialKey, 0, KEY_QUERY_VALUE ++ ) ++ + if hkey is not None: +- path = RegQueryValueEx( hkey, "InstallPath" ) +- RegCloseKey( hkey ) +- ++ 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" ++ if progs is not None: ++ #print("Used Windows registry") + return progs +- +- except Exception, excp: +- #raise excp ++ ++ except Exception: ++ #raise + pass + else: + break +- +- + + # Method 2 (Linux, Windows etc) +- # +- if os.environ.has_key('PATH'): +- ++ 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" ++ 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 os.environ.has_key('PROGRAMFILES'): +- ++ 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 + ++ 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/' ): +- ++ '/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" ++ if progs is not None: ++ #print("Used path") + return progs + + # Failed to find GraphViz +- # + return None +- + +-class Common: ++ ++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 + ++ 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 ) ++ 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 +@@ -618,84 +668,78 @@ class Common: + # + 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) + ++ 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 + ++ 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) ) +- ++ 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): +@@ -703,6 +747,7 @@ class Error(Exception): + """ + def __init__(self, value): + self.value = value ++ + def __str__(self): + return self.value + +@@ -712,119 +757,108 @@ class InvocationException(Exception): + """ + def __init__(self, value): + self.value = value ++ + def __str__(self): + return self.value + + +- +-class Node(object, Common): ++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): +- ++ 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 +- ++ 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): ++ 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['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'] + ++ 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 ] ++ styles = [style] + else: + styles = styles.split(',') +- styles.append( style ) +- +- self.obj_dict['attributes']['style'] = ','.join( styles ) +- ++ 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 self.obj_dict['attributes'].iteritems(): ++ 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) ) ) ++ node_attr.append('%s=%s' % (attr, quote_if_necessary(value))) + else: +- node_attr.append( attr ) +- +- ++ 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: +@@ -833,207 +867,191 @@ class Node(object, Common): + return node + ';' + + +- +-class Edge(object, Common ): ++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: +- ++ ++ 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.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 ++ 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) ) +- ++ ++ points = (quote_if_necessary(src), quote_if_necessary(dst)) ++ + self.obj_dict['points'] = points +- +- self.create_attribute_methods(EDGE_ATTRIBUTES) + ++ 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()) ) +- +- ++ 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." +- ++ 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() ) ): ++ 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() : ++ 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]=='"': +- ++ ++ if (node_port_idx > 0 and node_str[0] == '"' and ++ node_str[node_port_idx - 1] == '"'): + return node_str +- +- if node_port_idx>0: +- ++ ++ if node_port_idx > 0: + a = node_str[:node_port_idx] +- b = node_str[node_port_idx+1:] ++ b = node_str[node_port_idx + 1:] + + node = quote_if_necessary(a) + +- node += ':'+quote_if_necessary(b) ++ 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() ) +- ++ 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() ] ++ edge = [Subgraph(obj_dict=src).to_string()] + elif isinstance(src, (int, long)): +- edge = [ str(src) ] ++ 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( '->' ) +- ++ 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( '--' ) +- ++ edge.append('--') ++ + if isinstance(dst, frozendict): +- edge.append( Subgraph(obj_dict=dst).to_string() ) ++ edge.append(Subgraph(obj_dict=dst).to_string()) + elif isinstance(dst, (int, long)): +- edge.append( str(dst) ) ++ edge.append(str(dst)) + else: +- edge.append( dst ) +- ++ edge.append(dst) + + edge_attr = list() +- +- for attr, value in self.obj_dict['attributes'].iteritems(): +- ++ ++ 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) ) ) ++ edge_attr.append('%s=%s' % (attr, quote_if_necessary(value))) + else: +- edge_attr.append( attr ) ++ edge_attr.append(attr) + + edge_attr = ', '.join(edge_attr) +- ++ + if edge_attr: +- edge.append( ' [' + edge_attr + ']' ) ++ edge.append(' [' + edge_attr + ']') + + return ' '.join(edge) + ';' +- +- +- +- +- +-class Graph(object, Common): ++ ++ ++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', ++ ++ graph(graph_name='G', graph_type='digraph', + strict=False, suppress_disconnected=False, attribute=value, ...) +- ++ + graph_name: + the graph's name + graph_type: +@@ -1045,222 +1063,178 @@ class Graph(object, Common): + 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.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): ++ 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 +- +- ++ 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) ) +- ++ 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): ++ if isinstance(graph_nodes, (list, tuple)): ++ return [node.get_attributes() for node in graph_nodes] + +- self.add_node( Node('node', **attrs) ) ++ 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) ) ++ 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() ++ 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 +- + ++ 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 +- + ++ self.obj_dict['name'] = graph_name + + def get_name(self): + """Get the graph's name.""" +- +- return self.obj_dict['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 +- + ++ 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'] +- + ++ 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 +- + ++ 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. +@@ -1268,106 +1242,101 @@ class Graph(object, Common): + 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.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()) + +- +- + 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 ++ ++ '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 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 self.obj_dict['nodes'].has_key(name): +- +- match.extend( [ Node( obj_dict = obj_dict ) for obj_dict in self.obj_dict['nodes'][name] ]) +- +- return match + ++ 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'].iteritems(): +- node_objs.extend( [ Node( obj_dict = obj_d ) for obj_d in obj_dict_list ] ) +- +- return node_objs + ++ 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. + """ +@@ -1375,78 +1344,70 @@ class Graph(object, Common): + 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() ) ++ edge_points = (graph_edge.get_source(), graph_edge.get_destination()) + +- if self.obj_dict['edges'].has_key(edge_points): ++ 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] + +- 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() ) +- +- ++ 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 ++ ++ '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 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 (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: ++ 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: +@@ -1454,224 +1415,205 @@ class Graph(object, Common): + edge_points_reverse = (dst, src_or_list) + + match = list() +- +- 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 )): +- ++ ++ 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 )) +- ++ 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 + +- + 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'].iteritems(): +- edge_objs.extend( [ Edge( obj_dict = obj_d ) for obj_d in obj_dict_list ] ) +- +- return edge_objs + ++ 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 self.obj_dict['subgraphs'].has_key(sgraph.get_name()): +- +- sgraph_list = self.obj_dict['subgraphs'][ sgraph.get_name() ] +- sgraph_list.append( sgraph.obj_dict ) +- ++ ++ 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() ) ++ 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 self.obj_dict['subgraphs'].has_key( name ): +- +- sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name ) +- ++ ++ 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 ++ #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'].iteritems(): +- sgraph_objs.extend( [ Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list ] ) +- +- return sgraph_objs +- + ++ 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'].itervalues(): ++ ++ 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'].itervalues(): ++ 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'].itervalues(): ++ 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']: +- ++ 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' ) ++ graph.append('subgraph {\n') + else: +- graph.append( '{\n' ) ++ graph.append('{\n') + else: +- graph.append( '%s %s {\n' % (self.obj_dict['type'], self.obj_dict['name']) ) +- ++ graph.append('%s %s {\n' % (self.obj_dict['type'], self.obj_dict['name'])) + +- for attr in self.obj_dict['attributes'].iterkeys(): +- +- if self.obj_dict['attributes'].get(attr, None) is not None: +- +- val = self.obj_dict['attributes'].get(attr) +- if val is not None: +- graph.append( '%s=%s' % (attr, quote_if_necessary(val)) ) +- else: +- graph.append( attr ) +- +- graph.append( ';\n' ) ++ 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'].itervalues(): ++ for e in self.obj_dict['edges'].values(): + edge_obj_dicts.extend(e) +- ++ + if edge_obj_dicts: +- edge_src_set, edge_dst_set = 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() +- ++ + node_obj_dicts = list() +- for e in self.obj_dict['nodes'].itervalues(): ++ for e in self.obj_dict['nodes'].values(): + node_obj_dicts.extend(e) + + sgraph_obj_dicts = list() +- for sg in self.obj_dict['subgraphs'].itervalues(): ++ for sg in self.obj_dict['subgraphs'].values(): + sgraph_obj_dicts.extend(sg) + +- +- obj_list = [ (obj['sequence'], obj) for obj in (edge_obj_dicts + node_obj_dicts + sgraph_obj_dicts) ] +- obj_list.sort() +- ++ 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): +- ++ node.get_name() not in edge_dst_set): + continue +- +- graph.append( node.to_string()+'\n' ) + +- elif obj['type'] == 'edge': ++ 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' ) ++ ++ graph.append(edge.to_string() + '\n') + edges_done.add(edge) +- + else: +- + sgraph = Subgraph(obj_dict=obj) +- +- graph.append( sgraph.to_string()+'\n' ) ++ graph.append(sgraph.to_string() + '\n') + +- graph.append( '}\n' ) +- +- return ''.join(graph) ++ graph.append('}\n') + ++ return ''.join(graph) + + + class Subgraph(Graph): +@@ -1680,9 +1622,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, ...) +- ++ + graph_name: + the subgraph's name + suppress_disconnected: +@@ -1690,46 +1632,43 @@ class Subgraph(Graph): + 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.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): +- ++ 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, ++ 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: +@@ -1737,38 +1676,35 @@ class Cluster(Graph): + 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.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): ++ 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) ++ 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.obj_dict['name'] = 'cluster_' + graph_name + + self.create_attribute_methods(CLUSTER_ATTRIBUTES) + + +- +- +- +- + class Dot(Graph): + """A container for handling a dot language file. + +@@ -1776,144 +1712,148 @@ class Dot(Graph): + 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', ++ 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' ] +- ++ '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']: ++ '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.''' +- +- +- ++ '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): +- +- dict = copy.copy(self.obj_dict) +- +- return dict +- ++ 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 ) +- +- ++ ++ 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 + ++ self.progs = paths + + def write(self, path, prog=None, format='raw'): +- """Writes a graph to a file. +- ++ """ + 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'. ++ '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 +- +- dot_fd = file(path, "w+b") +- 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: +- data = data.encode('utf-8') +- except: +- pass +- dot_fd.write(data) +- else: +- dot_fd.write(self.create(prog, format)) +- dot_fd.close() + +- return True +- ++ 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. +@@ -1923,75 +1863,71 @@ class Dot(Graph): + 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' ] ++ ++ ['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 not self.progs.has_key(prog): ++ '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] ): ++ '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] ) +- +- ++ '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 ) +- ++ tmp_dir = os.path.dirname(tmp_name) ++ + # For each of the image files... +- # + for img in self.shape_files: +- ++ + # Get its data +- # +- f = file(img, 'rb') ++ 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 = file( os.path.join( tmp_dir, os.path.basename(img) ), 'wb' ) ++ 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 +- ++ ++ 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() +@@ -1999,9 +1935,9 @@ class Dot(Graph): + break + stdout_output.append(data) + stdout.close() +- +- stdout_output = ''.join(stdout_output) +- ++ ++ stdout_output = NULL_SEP.join(stdout_output) ++ + if not stderr.closed: + stderr_output = list() + while True: +@@ -2010,29 +1946,28 @@ class Dot(Graph): + break + stderr_output.append(data) + stderr.close() +- ++ + if stderr_output: +- stderr_output = ''.join(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 : ++ ++ if status != 0: + raise InvocationException( + 'Program terminated with status: %d. stderr follows: %s' % ( +- status, stderr_output) ) ++ status, stderr_output)) + elif stderr_output: +- print 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(os.path.join(tmp_dir, os.path.basename(img))) + + os.unlink(tmp_name) +- +- return stdout_output + ++ return stdout_output +diff --git a/setup.py b/setup.py +index 27328d8..92890d7 100644 +--- a/setup.py ++++ b/setup.py +@@ -1,10 +1,6 @@ + #!/usr/bin/env python + +-try: +- from distutils.core import setup +-except ImportError, excp: +- from setuptools import setup +- ++from setuptools import setup + import pydot + import os + diff --git a/SPECS/pydot.spec b/SPECS/pydot.spec new file mode 100644 index 0000000..ed11c89 --- /dev/null +++ b/SPECS/pydot.spec @@ -0,0 +1,225 @@ +%global common_desc \ +An interface for creating both directed and non directed graphs from Python. \ +Currently all attributes implemented in the Dot language are supported (up \ +to Graphviz 2.16). \ + \ +Output can be inlined in Postscript into interactive scientific environments \ +like TeXmacs, or output in any of the format's supported by the Graphviz \ +tools dot, neato, twopi. + +Name: pydot +Version: 1.4.1 +Release: 5%{?dist} +Summary: Python interface to Graphviz's Dot language + +License: MIT +URL: https://github.com/erocarrera/pydot +Source0: https://github.com/erocarrera/pydot/archive/v%{version}/%{name}-%{version}.tar.gz +BuildArch: noarch + +%description +%{common_desc} + +%package -n python3-%{name} +Summary: Python3 interface to Graphviz's Dot language + +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3dist(chardet) +BuildRequires: python3dist(nose) +BuildRequires: python3dist(pyparsing) +BuildRequires: python3dist(setuptools) +BuildRequires: graphviz +Requires: graphviz + +Provides: %{name} = %{version}-%{release} +%{?python_provide:%python_provide python3-%{name}} + +%description -n python3-%{name} +%{common_desc} + +%prep +%autosetup + +%build +%py3_build + +%install +%py3_install + +%check +EXCLUDE_ARG="" +EXCLUDE_ARG="$EXCLUDE_ARG -e test_graph_with_shapefiles -e test_graphviz_regression_tests -e test_my_regression_tests" +PYTHONPATH=%{buildroot}%{python3_sitelib} nosetests-%{python3_version} -v test/pydot_unittest.py $EXCLUDE_ARG + +%files -n python3-%{name} +%doc ChangeLog README.md +%license LICENSE +%{python3_sitelib}/dot_parser.* +%{python3_sitelib}/pydot.* +%{python3_sitelib}/__pycache__/dot_parser.* +%{python3_sitelib}/__pycache__/pydot.* +%{python3_sitelib}/pydot-%{version}-py%{python3_version}.egg-info/ + +%changelog +* Wed Jan 27 2021 Fedora Release Engineering - 1.4.1-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Tue Jul 28 2020 Fedora Release Engineering - 1.4.1-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild + +* Mon May 25 2020 Miro Hrončok - 1.4.1-3 +- Rebuilt for Python 3.9 + +* Thu Jan 30 2020 Fedora Release Engineering - 1.4.1-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Thu Dec 26 2019 Fabian Affolter - 1.4.1-1 +- Update check section +- Update to latest upstream release 1.4.1 + +* Thu Oct 03 2019 Miro Hrončok - 1.2.4-9 +- Rebuilt for Python 3.8.0rc1 (#1748018) + +* Sun Aug 18 2019 Miro Hrončok - 1.2.4-8 +- Rebuilt for Python 3.8 + +* Fri Jul 26 2019 Fedora Release Engineering - 1.2.4-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild + +* Sat Feb 02 2019 Fedora Release Engineering - 1.2.4-6 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild + +* Tue Jan 15 2019 Igor Gnatenko - 1.2.4-5 +- Enable python dependency generator + +* Mon Jan 14 2019 Miro Hrončok - 1.2.4-4 +- Subpackage python2-pydot has been removed + See https://fedoraproject.org/wiki/Changes/Mass_Python_2_Package_Removal + +* Mon Oct 15 2018 Randy Barlow - 1.2.4-3 +- Bring the Python 2 subpackage back (#1637711). + +* Mon Oct 1 2018 Tom Callaway - 1.2.4-2 +- just py3 + +* Wed Sep 12 2018 Jerry James - 1.2.4-1 +- update to 1.2.4 + +* Fri Jul 13 2018 Fedora Release Engineering - 1.0.28-19 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild + +* Tue Jun 19 2018 Miro Hrončok - 1.0.28-18 +- Rebuilt for Python 3.7 + +* Fri Feb 09 2018 Fedora Release Engineering - 1.0.28-17 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild + +* Thu Jul 27 2017 Fedora Release Engineering - 1.0.28-16 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild + +* Sat Feb 11 2017 Fedora Release Engineering - 1.0.28-15 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild + +* Mon Dec 19 2016 Miro Hrončok - 1.0.28-14 +- Rebuild for Python 3.6 + +* Mon Oct 17 2016 Björn Esser - 1.0.28-13 +- Drop obsolete stuff +- Move %%description to a common macro +- Add conditionals to build on epel +- Clean trailing whitespaces + +* Tue Jul 19 2016 Fedora Release Engineering - 1.0.28-12 +- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages + +* Fri Apr 15 2016 Tom Callaway - 1.0.28-11 +- use debian's python3 fix (tested against bz1312815) + +* Fri Apr 8 2016 Tom Callaway - 1.0.28-10 +- properly obsolete old "pydot" packages (bz1323980) + +* Thu Feb 04 2016 Fedora Release Engineering - 1.0.28-9 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild + +* Tue Jan 19 2016 Tom Callaway - 1.0.28-8 +- python 3 support + +* Thu Jun 18 2015 Fedora Release Engineering - 1.0.28-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild + +* Sat Jun 07 2014 Fedora Release Engineering - 1.0.28-6 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild + +* Mon Dec 9 2013 Tom Callaway - 1.0.28-5 +- fix for pyparsing2 + +* Sun Aug 04 2013 Fedora Release Engineering - 1.0.28-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild + +* Thu Feb 14 2013 Fedora Release Engineering - 1.0.28-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_19_Mass_Rebuild + +* Sat Jul 21 2012 Fedora Release Engineering - 1.0.28-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild + +* Fri Mar 2 2012 Tom Callaway - 1.0.28-1 +- update to 1.0.28 + +* Sat Jan 14 2012 Fedora Release Engineering - 1.0.25-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild + +* Tue Oct 11 2011 Tom Callaway - 1.0.25-2 +- apply fix for pebl relating to catching AttributeError, thanks to Thomas Spura + +* Thu Apr 21 2011 Tom Callaway - 1.0.25-1 +- update to 1.0.25 + +* Thu Mar 3 2011 Tom Callaway - 1.0.23-1 +- update to 1.0.23 + +* Tue Feb 08 2011 Fedora Release Engineering - 1.0.4-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild + +* Tue Jan 4 2011 Tom Callaway - 1.0.4-1 +- update to 1.0.4 + +* Wed Nov 3 2010 Tom "spot" Callaway - 1.0.3-1 +- update to 1.0.3 + +* Wed Jul 21 2010 David Malcolm - 1.0.2-7 +- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild + +* Fri Jul 31 2009 Tom "spot" Callaway - 1.0.2-6 +- somehow, the egg info didn't make it into the rebuild... + +* Sun Jul 26 2009 Fedora Release Engineering - 1.0.2-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild + +* Mon Jul 6 2009 Tom "spot" Callaway - 1.0.2-4 +- fix pydot crash with accented character (bugzilla 481540) + +* Thu Feb 26 2009 Fedora Release Engineering - 1.0.2-3 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_11_Mass_Rebuild + +* Sat Nov 29 2008 Ignacio Vazquez-Abrams - 1.0.2-2 +- Rebuild for Python 2.6 + +* Fri Sep 12 2008 Tom "spot" Callaway - 1.0.2-1 +- update to 1.0.2 + +* Thu Dec 14 2006 Jason L Tibbitts III - 0.9.10-5 +- Rebuild for new Python +- Add BR: python-devel + +* Fri Sep 15 2006 Tom "spot" Callaway 0.9.10-4 +- bump for fc6 + +* Thu Oct 6 2005 Tom "spot" Callaway 0.9.10-3 +- We really do need pyparsing as a BR + +* Thu Oct 6 2005 Tom "spot" Callaway 0.9.10-2 +- change BR to R for graphviz, pyparsing + +* Sat Sep 17 2005 Tom "spot" Callaway 0.9.10-1 +- initial package for Fedora Extras