diff --git a/.travis.yml b/.travis.yml index 30fba6f..6d46fc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,18 @@ matrix: arch: ppc64le - python: 3.9 arch: ppc64le + # Additional job to run linters that only need to run once. + - name: black + language: python + python: 3.6 + arch: amd64 + addons: + apt: + packages: [] + install: + - pip install -e .[dev] + script: + - black --check --diff . addons: apt: @@ -35,6 +47,3 @@ install: script: - cd test/ - python pydot_unittest.py - -# after_success: -# - coveralls diff --git a/README.md b/README.md index 8dbcb06..64e37ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://www.travis-ci.com/pydot/pydot.svg?branch=master)](https://www.travis-ci.com/pydot/pydot) [![PyPI](https://img.shields.io/pypi/v/pydot.svg)](https://pypi.org/project/pydot/) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) About @@ -49,7 +50,7 @@ start with. Here are 3 common options: ```python import pydot - graphs = pydot.graph_from_dot_file('example.dot') + graphs = pydot.graph_from_dot_file("example.dot") graph = graphs[0] ``` @@ -80,19 +81,19 @@ start with. Here are 3 common options: ```python import pydot - graph = pydot.Dot('my_graph', graph_type='graph', bgcolor='yellow') + graph = pydot.Dot("my_graph", graph_type="graph", bgcolor="yellow") # Add nodes - my_node = pydot.Node('a', label='Foo') + my_node = pydot.Node("a", label="Foo") graph.add_node(my_node) # Or, without using an intermediate variable: - graph.add_node(pydot.Node('b', shape='circle')) + graph.add_node(pydot.Node("b", shape="circle")) # Add edges - my_edge = pydot.Edge('a', 'b', color='blue') + my_edge = pydot.Edge("a", "b", color="blue") graph.add_edge(my_edge) # Or, without using an intermediate variable: - graph.add_edge(pydot.Edge('b', 'c', color='blue')) + graph.add_edge(pydot.Edge("b", "c", color="blue")) ``` Imagine using these basic building blocks from your Python program @@ -123,14 +124,14 @@ You can now further manipulate your graph using pydot methods: - Add further nodes and edges: ```python - graph.add_edge(pydot.Edge('b', 'd', style='dotted')) + graph.add_edge(pydot.Edge("b", "d", style="dotted")) ``` - Edit attributes of graph, nodes and edges: ```python - graph.set_bgcolor('lightyellow') - graph.get_node('b')[0].set_shape('box') + graph.set_bgcolor("lightyellow") + graph.get_node("b")[0].set_shape("box") ``` Output @@ -154,7 +155,7 @@ Here are 3 different output options: the `write_*` methods: ```python - graph.write_png('output.png') + graph.write_png("output.png") ``` 2. Retrieve the DOT string. @@ -169,7 +170,7 @@ Here are 3 different output options: # As a string: output_raw_dot = graph.to_string() # Or, save it as a DOT-file: - graph.write_raw('output_raw.dot') + graph.write_raw("output_raw.dot") ``` - The Graphviz DOT: You can use it to check how Graphviz lays out @@ -180,7 +181,7 @@ Here are 3 different output options: # As a bytes literal: output_graphviz_dot = graph.create_dot() # Or, save it as a DOT-file: - graph.write_dot('output_graphviz.dot') + graph.write_dot("output_graphviz.dot") ``` 3. Convert to a NetworkX graph. diff --git a/dot_parser.py b/dot_parser.py index a6b8ee0..8401753 100644 --- a/dot_parser.py +++ b/dot_parser.py @@ -12,19 +12,30 @@ import sys from pyparsing import ( - nestedExpr, Literal, CaselessLiteral, - Word, OneOrMore, + nestedExpr, + Literal, + CaselessLiteral, + Word, + OneOrMore, Forward, - Group, Optional, Combine, - restOfLine, cStyleComment, nums, alphanums, + Group, + Optional, + Combine, + restOfLine, + cStyleComment, + nums, + alphanums, printables, - ParseException, ParseResults, CharsNotIn, - QuotedString) + ParseException, + ParseResults, + CharsNotIn, + QuotedString, +) import pydot -__author__ = ['Michael Krause', 'Ero Carrera'] -__license__ = 'MIT' +__author__ = ["Michael Krause", "Ero Carrera"] +__license__ = "MIT" PY3 = sys.version_info >= (3, 0, 0) @@ -35,15 +46,14 @@ class P_AttrList(object): - 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 @@ -56,14 +66,16 @@ def __repr__(self): 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() @@ -75,21 +87,23 @@ def push_top_graph_stmt(str, loc, toks): for element in toks: - if (isinstance(element, (ParseResults, tuple, list)) and - len(element) == 1 and - isinstance(element[0], str_type)): + if ( + isinstance(element, (ParseResults, tuple, list)) + and len(element) == 1 + and isinstance(element[0], str_type) + ): element = element[0] - if element == 'strict': - attrs['strict'] = True + 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 + attrs["type"] = element top_graphs.append(g) @@ -98,10 +112,10 @@ def push_top_graph_stmt(str, loc, toks): 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) @@ -113,7 +127,8 @@ def push_top_graph_stmt(str, loc, toks): else: raise ValueError( - 'Unknown element statement: {s}'.format(s=element)) + "Unknown element statement: {s}".format(s=element) + ) for g in top_graphs: update_parent_graph_hierarchy(g) @@ -128,7 +143,7 @@ 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 @@ -140,25 +155,29 @@ def update_parent_graph_hierarchy(g, parent_graph=None, level=0): for key, objs in item_dict[key_name].items(): for obj in objs: - if ('parent_graph' in obj and - obj['parent_graph'].get_parent_graph() == g): - if obj['parent_graph'] is 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)): + obj["parent_graph"].set_parent_graph(parent_graph) + + if key_name == "edges" and len(key) == 2: + for idx, vertex in enumerate(obj["points"]): + if isinstance( + vertex, + (pydot.Graph, pydot.Subgraph, pydot.Cluster), + ): vertex.set_parent_graph(parent_graph) if isinstance(vertex, pydot.frozendict): - if vertex['parent_graph'] is g: + if vertex["parent_graph"] is g: pass else: - vertex['parent_graph'].set_parent_graph( - parent_graph) + vertex["parent_graph"].set_parent_graph( + parent_graph + ) def add_defaults(element, defaults): @@ -168,8 +187,9 @@ def add_defaults(element, defaults): d[key] = value -def add_elements(g, toks, defaults_graph=None, - defaults_node=None, defaults_edge=None): +def add_elements( + g, toks, defaults_graph=None, defaults_node=None, defaults_edge=None +): if defaults_graph is None: defaults_graph = {} if defaults_node is None: @@ -197,58 +217,62 @@ def add_elements(g, toks, defaults_graph=None, elif isinstance(element, ParseResults): for e in element: - add_elements(g, [e], defaults_graph, - defaults_node, defaults_edge) + add_elements( + g, [e], defaults_graph, defaults_node, defaults_edge + ) elif isinstance(element, DefaultStatement): - if element.default_type == 'graph': + if element.default_type == "graph": - default_graph_attrs = pydot.Node('graph', **element.attrs) + default_graph_attrs = pydot.Node("graph", **element.attrs) g.add_node(default_graph_attrs) - elif element.default_type == 'node': + elif element.default_type == "node": - default_node_attrs = pydot.Node('node', **element.attrs) + default_node_attrs = pydot.Node("node", **element.attrs) g.add_node(default_node_attrs) - elif element.default_type == 'edge': + elif element.default_type == "edge": - default_edge_attrs = pydot.Node('edge', **element.attrs) + 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}'.format( - s=element.default_type)) + "Unknown DefaultStatement: {s}".format( + s=element.default_type + ) + ) elif isinstance(element, P_AttrList): - g.obj_dict['attributes'].update(element.attrs) + g.obj_dict["attributes"].update(element.attrs) else: raise ValueError( - 'Unknown element statement: {s}'.format(s=element)) + "Unknown element statement: {s}".format(s=element) + ) def push_graph_stmt(str, loc, toks): - g = pydot.Subgraph('') + g = pydot.Subgraph("") add_elements(g, toks) return g def push_subgraph_stmt(str, loc, toks): - g = pydot.Subgraph('') + g = pydot.Subgraph("") for e in toks: if len(e) == 3: e[2].set_name(e[1]) - if e[0] == 'subgraph': - e[2].obj_dict['show_keyword'] = True + 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 + if e[0] == "subgraph": + e[1].obj_dict["show_keyword"] = True return e[1] return g @@ -265,11 +289,10 @@ def push_default_stmt(str, loc, toks): else: attrs = {} - if default_type in ['graph', 'node', 'edge']: + if default_type in ["graph", "node", "edge"]: return DefaultStatement(default_type, attrs) else: - raise ValueError( - 'Unknown default statement: {s}'.format(s=toks)) + raise ValueError("Unknown default statement: {s}".format(s=toks)) def push_attr_list(str, loc, toks): @@ -281,16 +304,16 @@ def get_port(node): if len(node) > 1: if isinstance(node[1], ParseResults): if len(node[1][0]) == 2: - if node[1][0][0] == ':': + if node[1][0][0] == ":": return node[1][0][1] return None def do_node_ports(node): - node_port = '' + 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 @@ -311,16 +334,16 @@ def push_edge_stmt(str, loc, toks): 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)) + e.append( + pydot.Edge(n_prev, pydot.frozendict(toks[2][0].obj_dict), **attrs) + ) elif isinstance(toks[2][0], pydot.Node): @@ -338,18 +361,21 @@ def push_edge_stmt(str, loc, toks): for n_next in [n for n in tuple(toks)[2::2]]: - if (isinstance(n_next, P_AttrList) or - not isinstance(n_next[0], str_type)): + if isinstance(n_next, P_AttrList) or not isinstance( + n_next[0], str_type + ): continue 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)) - n_prev = n_next[0]+n_next_port + n_prev = n_next[0] + n_next_port else: raise Exception( - 'Edge target {r} with type {s} unsupported.'.format( - r=toks[2][0], s=type(toks[2][0]))) + "Edge target {r} with type {s} unsupported.".format( + r=toks[2][0], s=type(toks[2][0]) + ) + ) return e @@ -406,27 +432,29 @@ def graph_definition(): identifier = Word(alphanums + "_.").setName("identifier") double_quoted_string = QuotedString( - '"', multiline=True, unquoteResults=False, escChar='\\' + '"', multiline=True, unquoteResults=False, escChar="\\" ) noncomma = "".join([c for c in printables if c != ","]) - 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(parse_html).leaveWhitespace() + 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 | - alphastring_).setName("ID") + ID = ( + identifier | html_text | double_quoted_string | alphastring_ + ).setName("ID") float_number = Combine( - Optional(minus) + - OneOrMore(Word(nums + ".")) + Optional(minus) + OneOrMore(Word(nums + ".")) ).setName("float_number") righthand_id = (float_number | ID).setName("righthand_id") @@ -434,39 +462,36 @@ def parse_html(s, loc, toks): port_angle = (at + ID).setName("port_angle") port_location = ( - OneOrMore(Group(colon + ID)) | - Group(colon + lparen + ID + comma + ID + rparen) + 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)) + Group(port_location + Optional(port_angle)) + | Group(port_angle + Optional(port_location)) ).setName("port") - node_id = (ID + Optional(port)) + node_id = ID + Optional(port) a_list = OneOrMore( - ID + - Optional(equals + righthand_id) + - Optional(comma.suppress()) + ID + Optional(equals + righthand_id) + Optional(comma.suppress()) ).setName("a_list") attr_list = OneOrMore( - lbrack.suppress() + - Optional(a_list) + - rbrack.suppress() + lbrack.suppress() + Optional(a_list) + rbrack.suppress() ).setName("attr_list") - attr_stmt = (Group(graph_ | node_ | edge_) + - attr_list).setName("attr_stmt") + attr_stmt = (Group(graph_ | node_ | edge_) + attr_list).setName( + "attr_stmt" + ) edgeop = (Literal("--") | Literal("->")).setName("edgeop") stmt_list = Forward() graph_stmt = Group( - lbrace.suppress() + - Optional(stmt_list) + - rbrace.suppress() + - Optional(semi.suppress()) + lbrace.suppress() + + Optional(stmt_list) + + rbrace.suppress() + + Optional(semi.suppress()) ).setName("graph_stmt") edge_point = Forward() @@ -474,27 +499,39 @@ def parse_html(s, loc, toks): edgeRHS = OneOrMore(edgeop + edge_point) edge_stmt = edge_point + edgeRHS + Optional(attr_list) - subgraph = Group( - subgraph_ + Optional(ID) + graph_stmt).setName("subgraph") + subgraph = Group(subgraph_ + Optional(ID) + graph_stmt).setName( + "subgraph" + ) - edge_point << Group( - subgraph | graph_stmt | node_id).setName('edge_point') + edge_point << Group(subgraph | graph_stmt | node_id).setName( + "edge_point" + ) node_stmt = ( - node_id + Optional(attr_list) + - Optional(semi.suppress())).setName("node_stmt") + 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")) + ( + Optional(strict_) + + Group((graph_ | digraph_)) + + Optional(ID) + + graph_stmt + ).setResultsName("graph") + ) - singleLineComment = Group( - "//" + restOfLine) | Group("#" + restOfLine) + singleLineComment = Group("//" + restOfLine) | Group("#" + restOfLine) # actions diff --git a/pydot.py b/pydot.py index c8c461d..851bea8 100644 --- a/pydot.py +++ b/pydot.py @@ -17,12 +17,13 @@ warnings.warn( "`pydot` could not import `dot_parser`, " "so `pydot` will be unable to parse DOT files. " - "The error was: {e}".format(e=e)) + "The error was: {e}".format(e=e) + ) -__author__ = 'Ero Carrera' -__version__ = '2.0.0.dev0' -__license__ = 'MIT' +__author__ = "Ero Carrera" +__version__ = "2.0.0.dev0" +__license__ = "MIT" PY3 = sys.version_info >= (3, 0, 0) @@ -32,91 +33,94 @@ str_type = basestring +# fmt: off GRAPH_ATTRIBUTES = { - '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', + "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' + "rank" } EDGE_ATTRIBUTES = { - 'URL', 'arrowhead', 'arrowsize', 'arrowtail', - 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir', - 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor', - 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel', - 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label', - 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor', - 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget', - 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen', - 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes', - 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport', - 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight', - 'rank' + "URL", "arrowhead", "arrowsize", "arrowtail", + "color", "colorscheme", "comment", "constraint", "decorate", "dir", + "edgeURL", "edgehref", "edgetarget", "edgetooltip", "fontcolor", + "fontname", "fontsize", "headURL", "headclip", "headhref", "headlabel", + "headport", "headtarget", "headtooltip", "href", "id", "label", + "labelURL", "labelangle", "labeldistance", "labelfloat", "labelfontcolor", + "labelfontname", "labelfontsize", "labelhref", "labeltarget", + "labeltooltip", "layer", "len", "lhead", "lp", "ltail", "minlen", + "nojustify", "penwidth", "pos", "samehead", "sametail", "showboxes", + "style", "tailURL", "tailclip", "tailhref", "taillabel", "tailport", + "tailtarget", "tailtooltip", "target", "tooltip", "weight", + "rank" } NODE_ATTRIBUTES = { - 'URL', 'color', 'colorscheme', 'comment', - 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname', - 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label', - 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth', - 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints', - 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style', - 'target', 'tooltip', 'vertices', 'width', 'z', + "URL", "color", "colorscheme", "comment", + "distortion", "fillcolor", "fixedsize", "fontcolor", "fontname", + "fontsize", "group", "height", "id", "image", "imagescale", "label", + "labelloc", "layer", "margin", "nojustify", "orientation", "penwidth", + "peripheries", "pin", "pos", "rects", "regular", "root", "samplepoints", + "shape", "shapefile", "showboxes", "sides", "skew", "sortv", "style", + "target", "tooltip", "vertices", "width", "z", # The following are attributes dot2tex - 'texlbl', 'texmode' + "texlbl", "texmode" } CLUSTER_ATTRIBUTES = { - 'K', 'URL', 'bgcolor', 'color', 'colorscheme', - 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust', - 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor', - 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip' + "K", "URL", "bgcolor", "color", "colorscheme", + "fillcolor", "fontcolor", "fontname", "fontsize", "label", "labeljust", + "labelloc", "lheight", "lp", "lwidth", "nojustify", "pencolor", + "penwidth", "peripheries", "sortv", "style", "target", "tooltip" } +# fmt: on DEFAULT_PROGRAMS = { - 'dot', - 'twopi', - 'neato', - 'circo', - 'fdp', - 'sfdp', + "dot", + "twopi", + "neato", + "circo", + "fdp", + "sfdp", } def is_windows(): # type: () -> bool - return os.name == 'nt' + return os.name == "nt" def is_anaconda(): # type: () -> bool import glob - conda_pattern = os.path.join(sys.prefix, 'conda-meta\\graphviz*.json') + + conda_pattern = os.path.join(sys.prefix, "conda-meta\\graphviz*.json") return glob.glob(conda_pattern) != [] def get_executable_extension(): # type: () -> str if is_windows(): - return '.bat' if is_anaconda() else '.exe' + return ".bat" if is_anaconda() else ".exe" else: - return '' + return "" def call_graphviz(program, arguments, working_dir, **kwargs): @@ -131,12 +135,12 @@ def call_graphviz(program, arguments, working_dir, **kwargs): arguments = [] env = { - 'PATH': os.environ.get('PATH', ''), - 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''), - 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''), + "PATH": os.environ.get("PATH", ""), + "LD_LIBRARY_PATH": os.environ.get("LD_LIBRARY_PATH", ""), + "SYSTEMROOT": os.environ.get("SYSTEMROOT", ""), } - program_with_args = [program, ] + arguments + program_with_args = [program] + arguments process = subprocess.Popen( program_with_args, @@ -160,9 +164,9 @@ def call_graphviz(program, arguments, working_dir, **kwargs): # This version freezes dictionaries used as values within dictionaries. # 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 @@ -210,15 +214,16 @@ def __repr__(self): return "frozendict(%s)" % dict.__repr__(self) -dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict'] +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 = re.compile("^[_a-zA-Z][a-zA-Z0-9_,]*$", re.UNICODE) id_re_alpha_nums_with_ports = re.compile( - '^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE) -id_re_num = re.compile('^[0-9,]+$', re.UNICODE) -id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE) -id_re_dbl_quoted = re.compile('^\".*\"$', re.S | re.UNICODE) -id_re_html = re.compile('^<.*>$', re.S | re.UNICODE) + '^[_a-zA-Z][a-zA-Z0-9_,:"]*[a-zA-Z0-9_,"]+$', re.UNICODE +) +id_re_num = re.compile("^[0-9,]+$", re.UNICODE) +id_re_with_port = re.compile("^([^:]*):([^:]*)$", re.UNICODE) +id_re_dbl_quoted = re.compile('^".*"$', re.S | re.UNICODE) +id_re_html = re.compile("^<.*>$", re.S | re.UNICODE) def needs_quotes(s): @@ -240,13 +245,17 @@ 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 @@ -261,8 +270,8 @@ def quote_if_necessary(s): """Enclose attribute value in quotes, if needed.""" if isinstance(s, bool): if s is True: - return 'True' - return 'False' + return "True" + return "False" if not isinstance(s, str_type): return s @@ -271,9 +280,11 @@ def quote_if_necessary(s): return s if needs_quotes(s): - replace = {'"': r'\"', - "\n": r'\n', - "\r": r'\r'} + replace = { + '"': r"\"", + "\n": r"\n", + "\r": r"\r", + } for (a, b) in replace.items(): s = s.replace(a, b) @@ -304,7 +315,7 @@ def graph_from_dot_file(path, encoding=None): @return: Graphs that result from parsing. @rtype: `list` of `pydot.Dot` """ - with io.open(path, 'rt', encoding=encoding) as f: + with io.open(path, "rt", encoding=encoding) as f: s = f.read() if not PY3: s = unicode(s) @@ -312,7 +323,7 @@ def graph_from_dot_file(path, encoding=None): return graphs -def graph_from_edges(edge_list, node_prefix='', directed=False): +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 @@ -324,10 +335,10 @@ def graph_from_edges(edge_list, node_prefix='', directed=False): """ if directed: - graph = Dot(graph_type='digraph') + graph = Dot(graph_type="digraph") else: - graph = Dot(graph_type='graph') + graph = Dot(graph_type="graph") for edge in edge_list: @@ -347,7 +358,7 @@ def graph_from_edges(edge_list, node_prefix='', directed=False): return graph -def graph_from_adjacency_matrix(matrix, node_prefix=u'', directed=False): +def graph_from_adjacency_matrix(matrix, node_prefix=u"", directed=False): """Creates a basic graph out of an adjacency matrix. The matrix has to be a list of rows of values @@ -359,9 +370,9 @@ def graph_from_adjacency_matrix(matrix, node_prefix=u'', directed=False): node_orig = 1 if directed: - graph = Dot(graph_type='digraph') + graph = Dot(graph_type="digraph") else: - graph = Dot(graph_type='graph') + graph = Dot(graph_type="graph") for row in matrix: if not directed: @@ -370,20 +381,23 @@ 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('%s%s' % (node_prefix, node_orig), - '%s%s' % (node_prefix, node_dest))) + Edge( + "%s%s" % (node_prefix, node_orig), + "%s%s" % (node_prefix, node_dest), + ) + ) node_dest += 1 node_orig += 1 return graph -def graph_from_incidence_matrix(matrix, node_prefix='', directed=False): +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 @@ -393,9 +407,9 @@ def graph_from_incidence_matrix(matrix, node_prefix='', directed=False): """ if directed: - graph = Dot(graph_type='digraph') + graph = Dot(graph_type="digraph") else: - graph = Dot(graph_type='graph') + graph = Dot(graph_type="graph") for row in matrix: nodes = [] @@ -403,14 +417,17 @@ def graph_from_incidence_matrix(matrix, node_prefix='', directed=False): for node in row: if node: - nodes.append(c*node) + nodes.append(c * node) c += 1 nodes.sort() if len(nodes) == 2: graph.add_edge( - Edge('%s%s' % (node_prefix, abs(nodes[0])), - '%s%s' % (node_prefix, nodes[1]))) + Edge( + "%s%s" % (node_prefix, abs(nodes[0])), + "%s%s" % (node_prefix, nodes[1]), + ) + ) if not directed: graph.set_simplify(True) @@ -435,15 +452,15 @@ def __setstate__(self, state): def __get_attribute__(self, attr): """Look for default attributes for this node""" - attr_val = self.obj_dict['attributes'].get(attr, None) + 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'] + 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' + if default_node_name in ("subgraph", "digraph", "cluster"): + default_node_name = "graph" g = self.get_parent_graph() if g is not None: @@ -463,7 +480,7 @@ def __get_attribute__(self, attr): defaults = [defaults] for default in defaults: - attr_val = default.obj_dict['attributes'].get(attr, None) + attr_val = default.obj_dict["attributes"].get(attr, None) if attr_val: return attr_val else: @@ -472,10 +489,10 @@ def __get_attribute__(self, attr): return None def set_parent_graph(self, parent_graph): - self.obj_dict['parent_graph'] = 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. @@ -487,7 +504,7 @@ def set(self, name, value): which are defined for all the existing attributes. """ - self.obj_dict['attributes'][name] = value + self.obj_dict["attributes"][name] = value def get(self, name): """Get an attribute value by name. @@ -499,40 +516,40 @@ def get(self, name): which are defined for all the existing attributes. """ - return self.obj_dict['attributes'].get(name, None) + return self.obj_dict["attributes"].get(name, None) def get_attributes(self): """Get attributes of the object""" - return self.obj_dict['attributes'] + return self.obj_dict["attributes"] def set_sequence(self, seq): """Set sequence""" - self.obj_dict['sequence'] = seq + self.obj_dict["sequence"] = seq def get_sequence(self): """Get sequence""" - return self.obj_dict['sequence'] + return self.obj_dict["sequence"] def create_attribute_methods(self, obj_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) + "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) + "get_" + attr, lambda a=attr: self.__get_attribute__(a) ) class Error(Exception): - """General error handling class. - """ + """General error handling class.""" def __init__(self, value): self.value = value @@ -542,8 +559,7 @@ def __str__(self): class InvocationException(Exception): - """Indicate problem while running any GraphViz executable. - """ + """Indicate problem while running any GraphViz executable.""" def __init__(self, value): self.value = value @@ -565,7 +581,7 @@ class Node(Common): 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 @@ -582,25 +598,25 @@ def __init__(self, name='', obj_dict=None, **attrs): # 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, str_type) and not name.startswith('"'): - idx = name.find(':') - if idx > 0 and idx+1 < len(name): + idx = name.find(":") + if idx > 0 and idx + 1 < len(name): name, port = name[:idx], name[idx:] if isinstance(name, int): name = str(name) - self.obj_dict['name'] = quote_if_necessary(name) - self.obj_dict['port'] = port + self.obj_dict["name"] = quote_if_necessary(name) + self.obj_dict["port"] = port self.create_attribute_methods(NODE_ATTRIBUTES) @@ -609,57 +625,55 @@ def __str__(self): def set_name(self, node_name): """Set the node's name.""" - self.obj_dict['name'] = node_name + self.obj_dict["name"] = node_name def get_name(self): """Get the node's name.""" - return self.obj_dict['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) + styles = self.obj_dict["attributes"].get("style", None) if not styles and style: styles = [style] else: - styles = styles.split(',') + styles = styles.split(",") styles.append(style) - self.obj_dict['attributes']['style'] = ','.join(styles) + self.obj_dict["attributes"]["style"] = ",".join(styles) def to_string(self): """Return string representation of node in DOT language.""" # RMF: special case defaults for node, edge and graph properties. # - node = quote_if_necessary(self.obj_dict['name']) + node = quote_if_necessary(self.obj_dict["name"]) node_attr = list() - for attr in sorted(self.obj_dict['attributes']): - value = self.obj_dict['attributes'][attr] - if value == '': + for attr in sorted(self.obj_dict["attributes"]): + value = self.obj_dict["attributes"][attr] + if value == "": value = '""' 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) # 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 '' + if node in ("graph", "node", "edge") and len(node_attr) == 0: + return "" - node_attr = ', '.join(node_attr) + node_attr = ", ".join(node_attr) if node_attr: - node += ' [' + node_attr + ']' + node += " [" + node_attr + "]" - return node + ';' + return node + ";" class Edge(Common): @@ -678,7 +692,7 @@ class Edge(Common): 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 @@ -691,22 +705,21 @@ class Edge(Common): """ - def __init__(self, src='', dst='', obj_dict=None, **attrs): + def __init__(self, src="", dst="", obj_dict=None, **attrs): self.obj_dict = dict() if isinstance(src, (Node, Subgraph, Cluster)): src = src.get_name() if isinstance(dst, (Node, Subgraph, Cluster)): dst = dst.get_name() - points = (quote_if_necessary(src), - quote_if_necessary(dst)) - self.obj_dict['points'] = points + points = (quote_if_necessary(src), quote_if_necessary(dst)) + self.obj_dict["points"] = points if obj_dict is None: # 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 else: self.obj_dict = obj_dict self.create_attribute_methods(EDGE_ATTRIBUTES) @@ -716,16 +729,14 @@ def __str__(self): def get_source(self): """Get the edges source node name.""" - return self.obj_dict['points'][0] + return self.obj_dict["points"][0] def get_destination(self): """Get the edge's destination node name.""" - return self.obj_dict['points'][1] + 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. @@ -739,28 +750,33 @@ def __eq__(self, edge): """ if not isinstance(edge, Edge): - raise Error('Can not compare and ' - 'edge to a non-edge object.') + raise Error("Can not compare an edge to a non-edge object.") - if self.get_parent_graph().get_top_graph_type() == 'graph': + 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 if not PY3: + def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: @@ -775,18 +791,21 @@ def parse_node_ref(self, node_str): return node_str - node_port_idx = node_str.rfind(':') + 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: 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 @@ -805,14 +824,14 @@ def to_string(self): 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' + 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.append("->") else: - edge.append('--') + edge.append("--") if isinstance(dst, frozendict): edge.append(Subgraph(obj_dict=dst).to_string()) @@ -823,22 +842,21 @@ def to_string(self): edge_attr = list() - for attr in sorted(self.obj_dict['attributes']): - value = self.obj_dict['attributes'][attr] - if value == '': + for attr in sorted(self.obj_dict["attributes"]): + value = self.obj_dict["attributes"][attr] + if value == "": value = '""' 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 = ', '.join(edge_attr) + edge_attr = ", ".join(edge_attr) if edge_attr: - edge.append(' [' + edge_attr + ']') + edge.append(" [" + edge_attr + "]") - return ' '.join(edge) + ';' + return " ".join(edge) + ";" class Graph(Common): @@ -877,9 +895,16 @@ class Graph(Common): 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 @@ -887,25 +912,28 @@ def __init__(self, graph_name='G', obj_dict=None, self.obj_dict = dict() - self.obj_dict['attributes'] = dict(attrs) + self.obj_dict["attributes"] = dict(attrs) - if graph_type not in ['graph', 'digraph']: - raise Error(( - 'Invalid type "{t}". ' - 'Accepted graph types are: ' - 'graph, digraph').format(t=graph_type)) + if graph_type not in ["graph", "digraph"]: + raise Error( + ( + 'Invalid type "{t}". ' + "Accepted graph types are: " + "graph, digraph" + ).format(t=graph_type) + ) - self.obj_dict['name'] = quote_if_necessary(graph_name) - self.obj_dict['type'] = 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["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.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) @@ -915,7 +943,7 @@ def __str__(self): return self.to_string() def get_graph_type(self): - return self.obj_dict['type'] + return self.obj_dict["type"] def get_top_graph_type(self): parent = self @@ -925,13 +953,13 @@ def get_top_graph_type(self): break parent = parent_ - return parent.obj_dict['type'] + 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') + graph_nodes = self.get_node("graph") if isinstance(graph_nodes, (list, tuple)): return [node.get_attributes() for node in graph_nodes] @@ -944,10 +972,10 @@ def set_node_defaults(self, **attrs): These attributes only apply to nodes added to the graph after calling this method. """ - self.add_node(Node('node', **attrs)) + self.add_node(Node("node", **attrs)) def get_node_defaults(self, **attrs): - graph_nodes = self.get_node('node') + graph_nodes = self.get_node("node") if isinstance(graph_nodes, (list, tuple)): return [node.get_attributes() for node in graph_nodes] @@ -955,10 +983,10 @@ def get_node_defaults(self, **attrs): return graph_nodes.get_attributes() def set_edge_defaults(self, **attrs): - self.add_node(Node('edge', **attrs)) + self.add_node(Node("edge", **attrs)) def get_edge_defaults(self, **attrs): - graph_nodes = self.get_node('edge') + graph_nodes = self.get_node("edge") if isinstance(graph_nodes, (list, tuple)): return [node.get_attributes() for node in graph_nodes] @@ -972,44 +1000,44 @@ def set_simplify(self, simplify): 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'] + 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 + self.obj_dict["type"] = graph_type def get_type(self): """Get the graph's type, 'graph' or 'digraph'.""" - return self.obj_dict['type'] + 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. @@ -1020,18 +1048,18 @@ def set_suppress_disconnected(self, val): 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'] + 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 + seq = self.obj_dict["current_child_sequence"] + self.obj_dict["current_child_sequence"] += 1 return seq def add_node(self, graph_node): @@ -1042,18 +1070,20 @@ def add_node(self, graph_node): """ if not isinstance(graph_node, Node): raise TypeError( - 'add_node() received ' + - 'a non node class object: ' + str(graph_node)) + "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()] = [ + self.obj_dict["nodes"][graph_node.get_name()] = [ graph_node.obj_dict ] graph_node.set_parent_graph(self.get_parent_graph()) else: - self.obj_dict['nodes'][graph_node.get_name()].append( + self.obj_dict["nodes"][graph_node.get_name()].append( graph_node.obj_dict ) @@ -1079,14 +1109,13 @@ def del_node(self, name, index=None): if isinstance(name, Node): name = name.get_name() - if name in self.obj_dict['nodes']: + 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] + 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] + del self.obj_dict["nodes"][name] return True return False @@ -1103,11 +1132,14 @@ def get_node(self, name): """ match = list() - if name in self.obj_dict['nodes']: + if name in self.obj_dict["nodes"]: match.extend( - [Node(obj_dict=obj_dict) - for obj_dict in self.obj_dict['nodes'][name]]) + [ + Node(obj_dict=obj_dict) + for obj_dict in self.obj_dict["nodes"][name] + ] + ) return match @@ -1123,12 +1155,9 @@ def get_node_list(self): """ node_objs = list() - for node in self.obj_dict['nodes']: - obj_dict_list = self.obj_dict['nodes'][node] - node_objs.extend([ - Node(obj_dict=obj_d) - for obj_d in obj_dict_list - ]) + for node in self.obj_dict["nodes"]: + obj_dict_list = self.obj_dict["nodes"][node] + node_objs.extend([Node(obj_dict=obj_d) for obj_d in obj_dict_list]) return node_objs @@ -1140,17 +1169,17 @@ def add_edge(self, graph_edge): """ if not isinstance(graph_edge, Edge): raise TypeError( - 'add_edge() received a non edge class object: ' + - str(graph_edge)) + "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 edge_points in self.obj_dict['edges']: - edge_list = self.obj_dict['edges'][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()) @@ -1184,13 +1213,14 @@ def del_edge(self, src_or_list, dst=None, index=None): if isinstance(dst, Node): dst = dst.get_name() - if (src, dst) in self.obj_dict['edges']: - if (index is not None and - index < len(self.obj_dict['edges'][(src, dst)])): - del self.obj_dict['edges'][(src, dst)][index] + 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)] + del self.obj_dict["edges"][(src, dst)] return True return False @@ -1214,19 +1244,21 @@ def get_edge(self, src_or_list, dst=None): match = list() - if edge_points in self.obj_dict['edges'] or ( - self.get_top_graph_type() == 'graph' and - edge_points_reverse in self.obj_dict['edges'] + 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( + 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)) + Edge( + edge_points[0], edge_points[1], obj_dict=edge_obj_dict + ) + ) return match @@ -1241,12 +1273,9 @@ def get_edge_list(self): """ edge_objs = list() - for edge in self.obj_dict['edges']: - obj_dict_list = self.obj_dict['edges'][edge] - edge_objs.extend([ - Edge(obj_dict=obj_d) - for obj_d in obj_dict_list - ]) + for edge in self.obj_dict["edges"]: + obj_dict_list = self.obj_dict["edges"][edge] + edge_objs.extend([Edge(obj_dict=obj_d) for obj_d in obj_dict_list]) return edge_objs @@ -1256,20 +1285,21 @@ def add_subgraph(self, sgraph): It takes a subgraph object as its only argument and returns None. """ - if (not isinstance(sgraph, Subgraph) and - not isinstance(sgraph, Cluster)): + if not isinstance(sgraph, Subgraph) and not isinstance( + sgraph, Cluster + ): raise TypeError( - 'add_subgraph() received a non subgraph class object:' + - str(sgraph)) + "add_subgraph() received a non subgraph class object:" + + str(sgraph) + ) - if sgraph.get_name() in self.obj_dict['subgraphs']: + if sgraph.get_name() in self.obj_dict["subgraphs"]: - sgraph_list = self.obj_dict['subgraphs'][sgraph.get_name()] + 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] + 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()) @@ -1286,9 +1316,9 @@ def get_subgraph(self, name): """ match = list() - if name in self.obj_dict['subgraphs']: + if name in self.obj_dict["subgraphs"]: - sgraphs_obj_dict = self.obj_dict['subgraphs'].get(name) + sgraphs_obj_dict = self.obj_dict["subgraphs"].get(name) for obj_dict_list in sgraphs_obj_dict: match.append(Subgraph(obj_dict=obj_dict_list)) @@ -1306,30 +1336,29 @@ def get_subgraph_list(self): """ sgraph_objs = list() - for sgraph in self.obj_dict['subgraphs']: - obj_dict_list = self.obj_dict['subgraphs'][sgraph] - sgraph_objs.extend([ - Subgraph(obj_dict=obj_d) - for obj_d in obj_dict_list - ]) + for sgraph in self.obj_dict["subgraphs"]: + obj_dict_list = self.obj_dict["subgraphs"][sgraph] + 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 + self.obj_dict["parent_graph"] = parent_graph - for k in self.obj_dict['nodes']: - obj_list = self.obj_dict['nodes'][k] + for k in self.obj_dict["nodes"]: + obj_list = self.obj_dict["nodes"][k] for obj in obj_list: - obj['parent_graph'] = parent_graph + obj["parent_graph"] = parent_graph - for k in self.obj_dict['edges']: - obj_list = self.obj_dict['edges'][k] + for k in self.obj_dict["edges"]: + obj_list = self.obj_dict["edges"][k] for obj in obj_list: - obj['parent_graph'] = parent_graph + obj["parent_graph"] = parent_graph - for k in self.obj_dict['subgraphs']: - obj_list = self.obj_dict['subgraphs'][k] + for k in self.obj_dict["subgraphs"]: + obj_list = self.obj_dict["subgraphs"][k] for obj in obj_list: Graph(obj_dict=obj).set_parent_graph(parent_graph) @@ -1341,94 +1370,95 @@ def to_string(self): """ graph = list() - if self.obj_dict.get('strict', None) is not None: + 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 ') + graph.append("strict ") - graph_type = self.obj_dict['type'] - if (graph_type == 'subgraph' and - not self.obj_dict.get('show_keyword', True)): - graph_type = '' - s = '{type} {name} {{\n'.format( - type=graph_type, - name=self.obj_dict['name']) + graph_type = self.obj_dict["type"] + if graph_type == "subgraph" and not self.obj_dict.get( + "show_keyword", True + ): + graph_type = "" + s = "{type} {name} {{\n".format( + type=graph_type, name=self.obj_dict["name"] + ) graph.append(s) - for attr in sorted(self.obj_dict['attributes']): + for attr in sorted(self.obj_dict["attributes"]): - if self.obj_dict['attributes'].get(attr, None) is not None: + if self.obj_dict["attributes"].get(attr, None) is not None: - val = self.obj_dict['attributes'].get(attr) - if val == '': + val = self.obj_dict["attributes"].get(attr) + if val == "": val = '""' if val is not None: - graph.append('%s=%s' % - (attr, quote_if_necessary(val))) + graph.append("%s=%s" % (attr, quote_if_necessary(val))) else: graph.append(attr) - graph.append(';\n') + graph.append(";\n") edges_done = set() edge_obj_dicts = list() - for k in self.obj_dict['edges']: - edge_obj_dicts.extend(self.obj_dict['edges'][k]) + for k in self.obj_dict["edges"]: + edge_obj_dicts.extend(self.obj_dict["edges"][k]) if edge_obj_dicts: - edge_src_set, edge_dst_set = list(zip( - *[obj['points'] for obj in edge_obj_dicts])) + edge_src_set, edge_dst_set = list( + zip(*[obj["points"] for obj in edge_obj_dicts]) + ) edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set) else: edge_src_set, edge_dst_set = set(), set() node_obj_dicts = list() - for k in self.obj_dict['nodes']: - node_obj_dicts.extend(self.obj_dict['nodes'][k]) + for k in self.obj_dict["nodes"]: + node_obj_dicts.extend(self.obj_dict["nodes"][k]) sgraph_obj_dicts = list() - for k in self.obj_dict['subgraphs']: - sgraph_obj_dicts.extend(self.obj_dict['subgraphs'][k]) + for k in self.obj_dict["subgraphs"]: + sgraph_obj_dicts.extend(self.obj_dict["subgraphs"][k]) obj_list = [ - (obj['sequence'], obj) + (obj["sequence"], obj) for obj in (edge_obj_dicts + node_obj_dicts + sgraph_obj_dicts) ] obj_list.sort(key=lambda x: x[0]) for idx, obj in obj_list: - if obj['type'] == 'node': + if obj["type"] == "node": node = Node(obj_dict=obj) - if self.obj_dict.get('suppress_disconnected', False): + 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): + if ( + node.get_name() not in edge_src_set + and node.get_name() not in edge_dst_set + ): continue - graph.append(node.to_string() + '\n') + graph.append(node.to_string() + "\n") - elif obj['type'] == 'edge': + elif obj["type"] == "edge": edge = Edge(obj_dict=obj) - if (self.obj_dict.get('simplify', False) and - edge in edges_done): + 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') + graph.append("}\n") - return ''.join(graph) + return "".join(graph) class Subgraph(Graph): @@ -1466,17 +1496,26 @@ class Subgraph(Graph): # 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, + self, + graph_name=graph_name, + obj_dict=obj_dict, suppress_disconnected=suppress_disconnected, - simplify=simplify, **attrs) + simplify=simplify, + **attrs + ) if obj_dict is None: - self.obj_dict['type'] = 'subgraph' + self.obj_dict["type"] = "subgraph" class Cluster(Graph): @@ -1511,18 +1550,27 @@ class Cluster(Graph): 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, + self, + graph_name=graph_name, + obj_dict=obj_dict, suppress_disconnected=suppress_disconnected, - simplify=simplify, **attrs) + simplify=simplify, + **attrs + ) if obj_dict is None: - self.obj_dict['type'] = 'subgraph' - self.obj_dict['name'] = quote_if_necessary('cluster_'+graph_name) + self.obj_dict["type"] = "subgraph" + self.obj_dict["name"] = quote_if_necessary("cluster_" + graph_name) self.create_attribute_methods(CLUSTER_ATTRIBUTES) @@ -1540,40 +1588,65 @@ def __init__(self, *argsl, **argsd): self.shape_files = list() self.formats = [ - 'canon', 'cmap', 'cmapx', - 'cmapx_np', 'dia', 'dot', - 'fig', 'gd', 'gd2', 'gif', - 'hpgl', 'imap', 'imap_np', 'ismap', - 'jpe', 'jpeg', 'jpg', 'mif', - 'mp', 'pcl', 'pdf', 'pic', 'plain', - 'plain-ext', 'png', 'ps', 'ps2', - 'svg', 'svgz', 'vml', 'vmlz', - 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib'] - - self.prog = 'dot' + "canon", + "cmap", + "cmapx", + "cmapx_np", + "dia", + "dot", + "fig", + "gd", + "gd2", + "gif", + "hpgl", + "imap", + "imap_np", + "ismap", + "jpe", + "jpeg", + "jpg", + "mif", + "mp", + "pcl", + "pdf", + "pic", + "plain", + "plain-ext", + "png", + "ps", + "ps2", + "svg", + "svgz", + "vml", + "vmlz", + "vrml", + "vtx", + "wbmp", + "xdot", + "xlib", + ] + + self.prog = "dot" # Automatically creates all # the methods enabling the creation # of output in any of the supported formats. for frmt in self.formats: - def new_method( - f=frmt, prog=self.prog, - encoding=None): + + def new_method(f=frmt, prog=self.prog, encoding=None): """Refer to docstring of method `create`.""" - return self.create( - format=f, prog=prog, encoding=encoding) - name = 'create_{fmt}'.format(fmt=frmt) + return self.create(format=f, prog=prog, encoding=encoding) + + name = "create_{fmt}".format(fmt=frmt) self.__setattr__(name, new_method) - for frmt in self.formats+['raw']: - def new_method( - path, f=frmt, prog=self.prog, - encoding=None): + for frmt in self.formats + ["raw"]: + + def new_method(path, f=frmt, prog=self.prog, encoding=None): """Refer to docstring of method `write.`""" - self.write( - path, format=f, prog=prog, - encoding=encoding) - name = 'write_{fmt}'.format(fmt=frmt) + self.write(path, format=f, prog=prog, encoding=encoding) + + name = "write_{fmt}".format(fmt=frmt) self.__setattr__(name, new_method) def __getstate__(self): @@ -1614,7 +1687,7 @@ def set_prog(self, prog): """ self.prog = prog - def write(self, path, prog=None, format='raw', encoding=None): + def write(self, path, prog=None, format="raw", encoding=None): """Writes a graph to a file. Given a filename 'path' it will open/create and truncate @@ -1641,19 +1714,19 @@ def write(self, path, prog=None, format='raw', encoding=None): """ if prog is None: prog = self.prog - if format == 'raw': + if format == "raw": s = self.to_string() if not PY3: s = unicode(s) - with io.open(path, mode='wt', encoding=encoding) as f: + with io.open(path, mode="wt", encoding=encoding) as f: f.write(s) else: s = self.create(prog, format, encoding=encoding) - with io.open(path, mode='wb') as f: + with io.open(path, mode="wb") as f: f.write(s) return True - def create(self, prog=None, format='ps', encoding=None): + def create(self, prog=None, format="ps", encoding=None): """Creates and returns a binary image for the graph. create will write the graph to a temporary dot file in the @@ -1730,16 +1803,16 @@ def create(self, prog=None, format='ps', encoding=None): # For each of the image files... for img in self.shape_files: # Get its data - f = open(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 = open(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() - arguments = ['-T{}'.format(format), ] + args + [tmp_name] + arguments = ["-T{}".format(format)] + args + [tmp_name] try: stdout_data, stderr_data, process = call_graphviz( @@ -1750,8 +1823,7 @@ def create(self, prog=None, format='ps', encoding=None): except OSError as e: if e.errno == errno.ENOENT: args = list(e.args) - args[1] = '"{prog}" not found in path.'.format( - prog=prog) + args[1] = '"{prog}" not found in path.'.format(prog=prog) raise OSError(*args) else: raise @@ -1765,7 +1837,7 @@ def create(self, prog=None, format='ps', encoding=None): if process.returncode != 0: message = ( '"{prog}" with args {arguments} returned code: {code}\n\n' - 'stdout, stderr:\n {out}\n{err}\n' + "stdout, stderr:\n {out}\n{err}\n" ).format( prog=prog, arguments=arguments, @@ -1775,12 +1847,12 @@ def create(self, prog=None, format='ps', encoding=None): ) print(message) - assert process.returncode == 0, ( - '"{prog}" with args {arguments} returned code: {code}'.format( - prog=prog, - arguments=arguments, - code=process.returncode, - ) - ) + assert ( + process.returncode == 0 + ), '"{prog}" with args {arguments} returned code: {code}'.format( + prog=prog, + arguments=arguments, + code=process.returncode, + ) return stdout_data diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..486bbe6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 79 +target-version = ['py27'] diff --git a/setup.py b/setup.py index 97da696..12d50c3 100644 --- a/setup.py +++ b/setup.py @@ -21,51 +21,59 @@ def get_long_description(): def get_version(): - pydot_py = os.path.join(CURRENT_DIR, 'pydot.py') - _version_re = re.compile(r'__version__\s+=\s+(?P.*)') - with codecs.open(pydot_py, 'r', encoding='utf8') as f: + pydot_py = os.path.join(CURRENT_DIR, "pydot.py") + _version_re = re.compile(r"__version__\s+=\s+(?P.*)") + with codecs.open(pydot_py, "r", encoding="utf8") as f: match = _version_re.search(f.read()) - version = match.group('version') if match is not None else '"unknown"' + version = match.group("version") if match is not None else '"unknown"' return str(ast.literal_eval(version)) setup( - name='pydot', + name="pydot", version=get_version(), description="Python interface to Graphviz's Dot", - author='Ero Carrera', - author_email='ero.carrera@gmail.com', - maintainer='Peter Nowee', - maintainer_email='peter@peternowee.com', - url='https://github.com/pydot/pydot', + author="Ero Carrera", + author_email="ero.carrera@gmail.com", + maintainer="Peter Nowee", + maintainer_email="peter@peternowee.com", + url="https://github.com/pydot/pydot", project_urls={ "Changelog": "https://github.com/pydot/pydot/blob/master/ChangeLog", "Bug Tracker": "https://github.com/pydot/pydot/issues", }, - license='MIT', - keywords='graphviz dot graphs visualization', - platforms=['any'], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + license="MIT", + keywords="graphviz dot graphs visualization", + platforms=["any"], + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Topic :: Scientific/Engineering :: Visualization', - 'Topic :: Software Development :: Libraries :: Python Modules'], + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: Libraries :: Python Modules", + ], long_description=get_long_description(), long_description_content_type="text/markdown", - py_modules=['pydot', 'dot_parser'], - install_requires=['pyparsing>=2.1.4'], - tests_require=['chardet']) + py_modules=["pydot", "dot_parser"], + install_requires=["pyparsing>=2.1.4"], + extras_require={ + "dev": [ + "chardet", + "black==21.5b2; python_version > '3.5'", + ], + }, + tests_require=["chardet"], +) diff --git a/test/pydot_unittest.py b/test/pydot_unittest.py index 4df26f8..40edb11 100644 --- a/test/pydot_unittest.py +++ b/test/pydot_unittest.py @@ -20,45 +20,43 @@ import unittest -TEST_PROGRAM = 'dot' -TESTS_DIR_1 = 'my_tests' -TESTS_DIR_2 = 'graphs' +TEST_PROGRAM = "dot" +TESTS_DIR_1 = "my_tests" +TESTS_DIR_2 = "graphs" class TestGraphAPI(unittest.TestCase): - def setUp(self): self._reset_graphs() def _reset_graphs(self): - self.graph_directed = pydot.Graph('testgraph', - graph_type='digraph') + self.graph_directed = pydot.Graph("testgraph", graph_type="digraph") def test_keep_graph_type(self): - g = pydot.Dot(graph_name='Test', graph_type='graph') - self.assertEqual(g.get_type(), 'graph') - g = pydot.Dot(graph_name='Test', graph_type='digraph') - self.assertEqual(g.get_type(), 'digraph') + g = pydot.Dot(graph_name="Test", graph_type="graph") + self.assertEqual(g.get_type(), "graph") + g = pydot.Dot(graph_name="Test", graph_type="digraph") + self.assertEqual(g.get_type(), "digraph") def test_add_style(self): - g = pydot.Dot(graph_name='Test', graph_type='graph') - node = pydot.Node('mynode') - node.add_style('abc') - self.assertEqual(node.get_style(), 'abc') - node.add_style('def') - self.assertEqual(node.get_style(), 'abc,def') - node.add_style('ghi') - self.assertEqual(node.get_style(), 'abc,def,ghi') + g = pydot.Dot(graph_name="Test", graph_type="graph") + node = pydot.Node("mynode") + node.add_style("abc") + self.assertEqual(node.get_style(), "abc") + node.add_style("def") + self.assertEqual(node.get_style(), "abc,def") + node.add_style("ghi") + self.assertEqual(node.get_style(), "abc,def,ghi") def test_create_simple_graph_with_node(self): g = pydot.Dot() - g.set_type('digraph') - node = pydot.Node('legend') - node.set("shape", 'box') + g.set_type("digraph") + node = pydot.Node("legend") + node.set("shape", "box") g.add_node(node) - node.set('label', 'mine') + node.set("label", "mine") s = g.to_string() - expected = 'digraph G {\nlegend [label=mine, shape=box];\n}\n' + expected = "digraph G {\nlegend [label=mine, shape=box];\n}\n" assert s == expected def test_attribute_with_implicit_value(self): @@ -67,7 +65,7 @@ def test_attribute_with_implicit_value(self): (g,) = graphs attrs = g.get_edges()[0].get_attributes() - self.assertEqual('decorate' in attrs, True) + self.assertEqual("decorate" in attrs, True) def test_subgraphs(self): g = pydot.Graph() @@ -78,19 +76,17 @@ def test_subgraphs(self): g.add_subgraph(s) - self.assertEqual(g.get_subgraphs()[0].get_name(), - s.get_name()) - self.assertEqual(g.get_subgraph_list()[0].get_name(), - s.get_name()) + self.assertEqual(g.get_subgraphs()[0].get_name(), s.get_name()) + self.assertEqual(g.get_subgraph_list()[0].get_name(), s.get_name()) def test_graph_pickling(self): g = pydot.Graph() s = pydot.Subgraph("foo") g.add_subgraph(s) - g.add_edge(pydot.Edge('A', 'B')) - g.add_edge(pydot.Edge('A', 'C')) - g.add_edge(pydot.Edge(('D', 'E'))) - g.add_node(pydot.Node('node!')) + g.add_edge(pydot.Edge("A", "B")) + g.add_edge(pydot.Edge("A", "C")) + g.add_edge(pydot.Edge(("D", "E"))) + g.add_node(pydot.Node("node!")) pickle.dumps(g) def test_unicode_ids(self): @@ -98,7 +94,7 @@ def test_unicode_ids(self): node2 = '"îôø®çßΩ"' g = pydot.Dot() - g.set_charset('latin1') + g.set_charset("latin1") g.add_node(pydot.Node(node1)) g.add_node(pydot.Node(node2)) g.add_edge(pydot.Edge(node1, node2)) @@ -120,50 +116,71 @@ def test_unicode_ids(self): def test_graph_simplify(self): # Fail example: pydot 1.0.2. GH pydot/pydot#92 OP patch 1. g = pydot.Graph() - g.add_edge(pydot.Edge('a', 'b')) - g.add_edge(pydot.Edge('a', 'b')) - g.add_edge(pydot.Edge('b', 'a')) - g.add_edge(pydot.Edge('b', 'a')) + g.add_edge(pydot.Edge("a", "b")) + g.add_edge(pydot.Edge("a", "b")) + g.add_edge(pydot.Edge("b", "a")) + g.add_edge(pydot.Edge("b", "a")) test_combinations = [ - ('graph', False, - 'graph G { a -- b; a -- b; b -- a; b -- a; }'), - ('graph', True, - 'graph G { a -- b; }'), - ('digraph', False, - 'digraph G { a -> b; a -> b; b -> a; b -> a; }'), - ('digraph', True, - 'digraph G { a -> b; b -> a; }')] - expected_concat = observed_concat = '' + ( + "graph", + False, + "graph G { a -- b; a -- b; b -- a; b -- a; }", + ), + ( + "graph", + True, + "graph G { a -- b; }", + ), + ( + "digraph", + False, + "digraph G { a -> b; a -> b; b -> a; b -> a; }", + ), + ( + "digraph", + True, + "digraph G { a -> b; b -> a; }", + ), + ] + expected_concat = observed_concat = "" for (graph_type, simplify, expected) in test_combinations: - expected_concat += 'graph_type %s, simplify %s: %s\n' % ( - graph_type, simplify, expected) + expected_concat += "graph_type %s, simplify %s: %s\n" % ( + graph_type, + simplify, + expected, + ) g.set_type(graph_type) g.set_simplify(simplify) try: - observed = ' '.join(g.to_string().split()) + observed = " ".join(g.to_string().split()) except (NameError, TypeError) as e: - observed = '%s: %s' % (type(e).__name__, e) - observed_concat += 'graph_type %s, simplify %s: %s\n' % ( - graph_type, simplify, observed) + observed = "%s: %s" % (type(e).__name__, e) + observed_concat += "graph_type %s, simplify %s: %s\n" % ( + graph_type, + simplify, + observed, + ) self.maxDiff = None self.assertMultiLineEqual(expected_concat, observed_concat) def test_graph_with_shapefiles(self): - shapefile_dir = os.path.join(test_dir, 'from-past-to-future') + shapefile_dir = os.path.join(test_dir, "from-past-to-future") # image files are omitted from sdist if not os.path.isdir(shapefile_dir): - warnings.warn('Skipping tests that involve images, ' - 'they can be found in the `git` repository.') + warnings.warn( + "Skipping tests that involve images, " + "they can be found in the `git` repository." + ) return - dot_file = os.path.join(shapefile_dir, 'from-past-to-future.dot') + dot_file = os.path.join(shapefile_dir, "from-past-to-future.dot") pngs = [ os.path.join(shapefile_dir, fname) for fname in os.listdir(shapefile_dir) - if fname.endswith('.png') + if fname.endswith(".png") ] - f = open(dot_file, 'rt') + f = open(dot_file, "rt") graph_data = f.read() f.close() @@ -171,26 +188,27 @@ def test_graph_with_shapefiles(self): (g,) = graphs g.set_shape_files(pngs) - jpe_data = g.create(format='jpe') + jpe_data = g.create(format="jpe") hexdigest = sha256(jpe_data).hexdigest() hexdigest_original = self._render_with_graphviz( - dot_file, encoding='ascii') + dot_file, encoding="ascii" + ) self.assertEqual(hexdigest, hexdigest_original) def test_multiple_graphs(self): - graph_data = 'graph A { a->b };\ngraph B {c->d}' + graph_data = "graph A { a->b };\ngraph B {c->d}" graphs = pydot.graph_from_dot_data(graph_data) n = len(graphs) assert n == 2, n names = [g.get_name() for g in graphs] - assert names == ['A', 'B'], names + assert names == ["A", "B"], names def _render_with_graphviz(self, filename, encoding): - with io.open(filename, 'rt', encoding=encoding) as stdin: + with io.open(filename, "rt", encoding=encoding) as stdin: stdout_data, stderr_data, process = pydot.call_graphviz( program=TEST_PROGRAM, - arguments=['-Tjpe', ], + arguments=["-Tjpe"], working_dir=os.path.dirname(filename), stdin=stdin, ) @@ -202,8 +220,9 @@ def _render_with_pydot(self, filename, encoding): c = pydot.graph_from_dot_file(filename, encoding=encoding) jpe_data = bytearray() for g in c: - jpe_data.extend(g.create(prog=TEST_PROGRAM, format='jpe', - encoding=encoding)) + jpe_data.extend( + g.create(prog=TEST_PROGRAM, format="jpe", encoding=encoding) + ) return sha256(jpe_data).hexdigest() def test_my_regression_tests(self): @@ -216,18 +235,17 @@ def test_graphviz_regression_tests(self): def _render_and_compare_dot_files(self, directory): # files that confuse `chardet` - encodings = { - 'Latin1.dot': 'latin-1'} + encodings = {"Latin1.dot": "latin-1"} dot_files = [ - fname for fname in os.listdir(directory) - if fname.endswith('.dot')] + fname for fname in os.listdir(directory) if fname.endswith(".dot") + ] for fname in dot_files: fpath = os.path.join(directory, fname) - with open(fpath, 'rb') as f: + with open(fpath, "rb") as f: s = f.read() estimate = chardet.detect(s) - encoding = encodings.get(fname, estimate['encoding']) - os.sys.stdout.write('#') + encoding = encodings.get(fname, estimate["encoding"]) + os.sys.stdout.write("#") os.sys.stdout.flush() pydot_sha = self._render_with_pydot(fpath, encoding) graphviz_sha = self._render_with_graphviz(fpath, encoding) @@ -236,7 +254,7 @@ def _render_and_compare_dot_files(self, directory): def test_numeric_node_id(self): self._reset_graphs() self.graph_directed.add_node(pydot.Node(1)) - self.assertEqual(self.graph_directed.get_nodes()[0].get_name(), '1') + self.assertEqual(self.graph_directed.get_nodes()[0].get_name(), "1") def test_quoted_node_id(self): self._reset_graphs() @@ -254,29 +272,24 @@ def test_quoted_node_id_to_string_no_attributes(self): def test_keyword_node_id(self): self._reset_graphs() - self.graph_directed.add_node(pydot.Node('node')) - self.assertEqual( - self.graph_directed.get_nodes()[0].get_name(), 'node' - ) + self.graph_directed.add_node(pydot.Node("node")) + self.assertEqual(self.graph_directed.get_nodes()[0].get_name(), "node") def test_keyword_node_id_to_string_no_attributes(self): self._reset_graphs() - self.graph_directed.add_node(pydot.Node('node')) - self.assertEqual( - self.graph_directed.get_nodes()[0].to_string(), '' - ) + self.graph_directed.add_node(pydot.Node("node")) + self.assertEqual(self.graph_directed.get_nodes()[0].to_string(), "") def test_keyword_node_id_to_string_with_attributes(self): self._reset_graphs() - self.graph_directed.add_node(pydot.Node('node', shape='box')) + self.graph_directed.add_node(pydot.Node("node", shape="box")) self.assertEqual( - self.graph_directed.get_nodes()[0].to_string(), - 'node [shape=box];' + self.graph_directed.get_nodes()[0].to_string(), "node [shape=box];" ) def test_names_of_a_thousand_nodes(self): self._reset_graphs() - names = {'node_%05d' % i for i in range(10**3)} + names = {"node_%05d" % i for i in range(10 ** 3)} for name in names: self.graph_directed.add_node(pydot.Node(name, label=name)) @@ -285,122 +298,133 @@ def test_names_of_a_thousand_nodes(self): ) def test_executable_not_found_exception(self): - graph = pydot.Dot('graphname', graph_type='digraph') - self.assertRaises(Exception, graph.create, prog='dothehe') + graph = pydot.Dot("graphname", graph_type="digraph") + self.assertRaises(Exception, graph.create, prog="dothehe") def test_graph_add_node_argument_type(self): self._reset_graphs() - self.assertRaises(TypeError, self.graph_directed.add_node, 1) - self.assertRaises(TypeError, self.graph_directed.add_node, 'a') + self.assertRaises(TypeError, self.graph_directed.add_node, 1) + self.assertRaises(TypeError, self.graph_directed.add_node, "a") def test_graph_add_edge_argument_type(self): self._reset_graphs() - self.assertRaises(TypeError, self.graph_directed.add_edge, 1) - self.assertRaises(TypeError, self.graph_directed.add_edge, 'a') + self.assertRaises(TypeError, self.graph_directed.add_edge, 1) + self.assertRaises(TypeError, self.graph_directed.add_edge, "a") def test_graph_add_subgraph_argument_type(self): self._reset_graphs() - self.assertRaises(TypeError, self.graph_directed.add_subgraph, 1) - self.assertRaises(TypeError, self.graph_directed.add_subgraph, 'a') + self.assertRaises(TypeError, self.graph_directed.add_subgraph, 1) + self.assertRaises(TypeError, self.graph_directed.add_subgraph, "a") def test_quoting(self): g = pydot.Dot() g.add_node(pydot.Node("test", label=string.printable)) - data = g.create(format='jpe') + data = g.create(format="jpe") self.assertEqual(len(data) > 0, True) def test_dot_args(self): g = pydot.Dot() - u = pydot.Node('a') + u = pydot.Node("a") g.add_node(u) - g.write_svg('test.svg', prog=['twopi', '-Goverlap=scale']) + g.write_svg("test.svg", prog=["twopi", "-Goverlap=scale"]) def test_edge_equality_basics_3_same_points_not_not_equal(self): # Fail example: pydot 1.4.1 on Python 2. g = pydot.Graph() - e1 = pydot.Edge('a', 'b') - e2 = pydot.Edge('a', 'b') + e1 = pydot.Edge("a", "b") + e2 = pydot.Edge("a", "b") g.add_edge(e1) g.add_edge(e2) self.assertFalse(e1 != e2) def test_edge_point_namestr(self): self._reset_graphs() - self.graph_directed.add_edge(pydot.Edge('a', 'b')) + self.graph_directed.add_edge(pydot.Edge("a", "b")) self.assertEqual( - self.graph_directed.get_edges()[0].to_string(), 'a -> b;') + self.graph_directed.get_edges()[0].to_string(), "a -> b;" + ) def test_edge_point_object_node(self): self._reset_graphs() self.graph_directed.add_edge( - pydot.Edge(pydot.Node('a'), pydot.Node('b')) + pydot.Edge(pydot.Node("a"), pydot.Node("b")) ) self.assertEqual( - self.graph_directed.get_edges()[0].to_string(), 'a -> b;') + self.graph_directed.get_edges()[0].to_string(), "a -> b;" + ) def test_edge_point_object_subgraph(self): self._reset_graphs() self.graph_directed.add_edge( - pydot.Edge(pydot.Subgraph('a'), pydot.Subgraph('b')) + pydot.Edge(pydot.Subgraph("a"), pydot.Subgraph("b")) ) self.assertEqual( - self.graph_directed.get_edges()[0].to_string(), 'a -> b;') + self.graph_directed.get_edges()[0].to_string(), "a -> b;" + ) def test_edge_point_object_cluster(self): self._reset_graphs() self.graph_directed.add_edge( - pydot.Edge(pydot.Cluster('a'), pydot.Cluster('b')) + pydot.Edge(pydot.Cluster("a"), pydot.Cluster("b")) ) self.assertEqual( self.graph_directed.get_edges()[0].to_string(), - 'cluster_a -> cluster_b;' + "cluster_a -> cluster_b;", ) def test_graph_from_adjacency_matrix(self): g = pydot.graph_from_adjacency_matrix( - [[0, 1, 0], [1, 0, 0], [0, 1, 1]], directed=True) - s = ' '.join(g.to_string().split()) - self.assertEqual(s, 'digraph G { 1 -> 2; 2 -> 1; 3 -> 2; 3 -> 3; }') + [[0, 1, 0], [1, 0, 0], [0, 1, 1]], directed=True + ) + s = " ".join(g.to_string().split()) + self.assertEqual(s, "digraph G { 1 -> 2; 2 -> 1; 3 -> 2; 3 -> 3; }") g = pydot.graph_from_adjacency_matrix( - [[0, 1, 0], [1, 0, 0], [0, 0, 1]], directed=False) - s = ' '.join(g.to_string().split()) - self.assertEqual(s, 'graph G { 1 -- 2; 3 -- 3; }') + [[0, 1, 0], [1, 0, 0], [0, 0, 1]], directed=False + ) + s = " ".join(g.to_string().split()) + self.assertEqual(s, "graph G { 1 -- 2; 3 -- 3; }") def test_graph_from_incidence_matrix(self): g = pydot.graph_from_incidence_matrix( - [[-1, 1, 0], [1, -1, 0], [0, 1, -1]], directed=True) - s = ' '.join(g.to_string().split()) - self.assertEqual(s, 'digraph G { 1 -> 2; 2 -> 1; 3 -> 2; }') + [[-1, 1, 0], [1, -1, 0], [0, 1, -1]], directed=True + ) + s = " ".join(g.to_string().split()) + self.assertEqual(s, "digraph G { 1 -> 2; 2 -> 1; 3 -> 2; }") g = pydot.graph_from_incidence_matrix( - [[1, 1, 0], [0, 1, 1]], directed=False) - s = ' '.join(g.to_string().split()) - self.assertEqual(s, 'graph G { 1 -- 2; 2 -- 3; }') + [[1, 1, 0], [0, 1, 1]], directed=False + ) + s = " ".join(g.to_string().split()) + self.assertEqual(s, "graph G { 1 -- 2; 2 -- 3; }") def check_path(): not_check = parse_args() if not_check: return - assert not os.path.isfile('setup.py'), ( - 'running out of source does not ' - 'test the installed `pydot`.') + assert not os.path.isfile( + "setup.py" + ), "running out of source does not test the installed `pydot`." def parse_args(): """Return arguments.""" parser = argparse.ArgumentParser() parser.add_argument( - '--no-check', action='store_true', - help=('do not require that no `setup.py` be present ' - 'in the current working directory.')) + "--no-check", + action="store_true", + help=( + "do not require that no `setup.py` be present " + "in the current working directory." + ), + ) args, unknown = parser.parse_known_args() # avoid confusing `unittest` sys.argv = [sys.argv[0]] + unknown return args.no_check -if __name__ == '__main__': +if __name__ == "__main__": check_path() test_dir = os.path.dirname(sys.argv[0]) - print('The tests are using `pydot` from: {pd}'.format(pd=pydot)) + print("The tests are using `pydot` from: {pd}".format(pd=pydot)) unittest.main(verbosity=2)