Source code for anytree.exporter.dotexporter

import codecs
import itertools
import logging
import re
from os import path, remove
from subprocess import check_call
from tempfile import NamedTemporaryFile

import six

from anytree import PreOrderIter

_RE_ESC = re.compile(r'["\\]')


[docs]class DotExporter: """ Dot Language Exporter. Args: node (Node): start node. Keyword Args: graph: DOT graph type. name: DOT graph name. options: list of options added to the graph. indent (int): number of spaces for indent. nodenamefunc: Function to extract node name from `node` object. The function shall accept one `node` object as argument and return the name of it. nodeattrfunc: Function to decorate a node with attributes. The function shall accept one `node` object as argument and return the attributes. edgeattrfunc: Function to decorate a edge with attributes. The function shall accept two `node` objects as argument. The first the node and the second the child and return the attributes. edgetypefunc: Function to which gives the edge type. The function shall accept two `node` objects as argument. The first the node and the second the child and return the edge (i.e. '->'). filter_: Function to filter nodes to include in export. The function shall accept one `node` object as argument and return True if it should be included, or False if it should not be included. stop: stop iteration at `node` if `stop` function returns `True` for `node`. maxlevel (int): Limit export to this number of levels. >>> from anytree import Node >>> root = Node("root") >>> s0 = Node("sub0", parent=root, edge=2) >>> s0b = Node("sub0B", parent=s0, foo=4, edge=109) >>> s0a = Node("sub0A", parent=s0, edge="") >>> s1 = Node("sub1", parent=root, edge="") >>> s1a = Node("sub1A", parent=s1, edge=7) >>> s1b = Node("sub1B", parent=s1, edge=8) >>> s1c = Node("sub1C", parent=s1, edge=22) >>> s1ca = Node("sub1Ca", parent=s1c, edge=42) .. note:: If the node names are not unqiue, see :any:`UniqueDotExporter`. A directed graph: >>> from anytree.exporter import DotExporter >>> for line in DotExporter(root): ... print(line) digraph tree { "root"; "sub0"; "sub0B"; "sub0A"; "sub1"; "sub1A"; "sub1B"; "sub1C"; "sub1Ca"; "root" -> "sub0"; "root" -> "sub1"; "sub0" -> "sub0B"; "sub0" -> "sub0A"; "sub1" -> "sub1A"; "sub1" -> "sub1B"; "sub1" -> "sub1C"; "sub1C" -> "sub1Ca"; } The resulting graph: .. image:: ../static/dotexporter0.png An undirected graph: >>> def nodenamefunc(node): ... return '%s:%s' % (node.name, node.depth) >>> def edgeattrfunc(node, child): ... return 'label="%s:%s"' % (node.name, child.name) >>> def edgetypefunc(node, child): ... return '--' >>> from anytree.exporter import DotExporter >>> for line in DotExporter(root, graph="graph", ... nodenamefunc=nodenamefunc, ... nodeattrfunc=lambda node: "shape=box", ... edgeattrfunc=edgeattrfunc, ... edgetypefunc=edgetypefunc): ... print(line) graph tree { "root:0" [shape=box]; "sub0:1" [shape=box]; "sub0B:2" [shape=box]; "sub0A:2" [shape=box]; "sub1:1" [shape=box]; "sub1A:2" [shape=box]; "sub1B:2" [shape=box]; "sub1C:2" [shape=box]; "sub1Ca:3" [shape=box]; "root:0" -- "sub0:1" [label="root:sub0"]; "root:0" -- "sub1:1" [label="root:sub1"]; "sub0:1" -- "sub0B:2" [label="sub0:sub0B"]; "sub0:1" -- "sub0A:2" [label="sub0:sub0A"]; "sub1:1" -- "sub1A:2" [label="sub1:sub1A"]; "sub1:1" -- "sub1B:2" [label="sub1:sub1B"]; "sub1:1" -- "sub1C:2" [label="sub1:sub1C"]; "sub1C:2" -- "sub1Ca:3" [label="sub1C:sub1Ca"]; } The resulting graph: .. image:: ../static/dotexporter1.png To export custom node implementations or :any:`AnyNode`, please provide a proper `nodenamefunc`: >>> from anytree import AnyNode >>> root = AnyNode(id="root") >>> s0 = AnyNode(id="sub0", parent=root) >>> s0b = AnyNode(id="s0b", parent=s0) >>> s0a = AnyNode(id="s0a", parent=s0) >>> from anytree.exporter import DotExporter >>> for line in DotExporter(root, nodenamefunc=lambda n: n.id): ... print(line) digraph tree { "root"; "sub0"; "s0b"; "s0a"; "root" -> "sub0"; "sub0" -> "s0b"; "sub0" -> "s0a"; } """ def __init__( self, node, graph="digraph", name="tree", options=None, indent=4, nodenamefunc=None, nodeattrfunc=None, edgeattrfunc=None, edgetypefunc=None, filter_=None, maxlevel=None, stop=None, ): self.node = node self.graph = graph self.name = name self.options = options self.indent = indent self.nodenamefunc = nodenamefunc self.nodeattrfunc = nodeattrfunc self.edgeattrfunc = edgeattrfunc self.edgetypefunc = edgetypefunc self.filter_ = filter_ self.maxlevel = maxlevel self.stop = stop def __iter__(self): # prepare indent = " " * self.indent nodenamefunc = self.nodenamefunc or self._default_nodenamefunc nodeattrfunc = self.nodeattrfunc or self._default_nodeattrfunc edgeattrfunc = self.edgeattrfunc or self._default_edgeattrfunc edgetypefunc = self.edgetypefunc or self._default_edgetypefunc filter_ = self.filter_ or self._default_filter return self.__iter(indent, nodenamefunc, nodeattrfunc, edgeattrfunc, edgetypefunc, filter_) @staticmethod def _default_nodenamefunc(node): return node.name @staticmethod def _default_nodeattrfunc(node): # pylint: disable=unused-argument return None @staticmethod def _default_edgeattrfunc(node, child): # pylint: disable=unused-argument return None @staticmethod def _default_edgetypefunc(node, child): # pylint: disable=unused-argument return "->" @staticmethod def _default_filter(node): # pylint: disable=unused-argument return True def __iter(self, indent, nodenamefunc, nodeattrfunc, edgeattrfunc, edgetypefunc, filter_): yield "{self.graph} {self.name} {{".format(self=self) for option in self.__iter_options(indent): yield option for node in self.__iter_nodes(indent, nodenamefunc, nodeattrfunc, filter_): yield node for edge in self.__iter_edges(indent, nodenamefunc, edgeattrfunc, edgetypefunc, filter_): yield edge yield "}" def __iter_options(self, indent): options = self.options if options: for option in options: yield "%s%s" % (indent, option) def __iter_nodes(self, indent, nodenamefunc, nodeattrfunc, filter_): for node in PreOrderIter(self.node, filter_=filter_, stop=self.stop, maxlevel=self.maxlevel): nodename = nodenamefunc(node) nodeattr = nodeattrfunc(node) nodeattr = " [%s]" % nodeattr if nodeattr is not None else "" yield '%s"%s"%s;' % (indent, DotExporter.esc(nodename), nodeattr) def __iter_edges(self, indent, nodenamefunc, edgeattrfunc, edgetypefunc, filter_): maxlevel = self.maxlevel - 1 if self.maxlevel else None for node in PreOrderIter(self.node, filter_=filter_, stop=self.stop, maxlevel=maxlevel): nodename = nodenamefunc(node) for child in node.children: if not filter_(child): continue childname = nodenamefunc(child) edgeattr = edgeattrfunc(node, child) edgetype = edgetypefunc(node, child) edgeattr = " [%s]" % edgeattr if edgeattr is not None else "" yield '%s"%s" %s "%s"%s;' % ( indent, DotExporter.esc(nodename), edgetype, DotExporter.esc(childname), edgeattr, )
[docs] def to_dotfile(self, filename): """ Write graph to `filename`. >>> from anytree import Node >>> root = Node("root") >>> s0 = Node("sub0", parent=root) >>> s0b = Node("sub0B", parent=s0) >>> s0a = Node("sub0A", parent=s0) >>> s1 = Node("sub1", parent=root) >>> s1a = Node("sub1A", parent=s1) >>> s1b = Node("sub1B", parent=s1) >>> s1c = Node("sub1C", parent=s1) >>> s1ca = Node("sub1Ca", parent=s1c) >>> from anytree.exporter import DotExporter >>> DotExporter(root).to_dotfile("tree.dot") The generated file should be handed over to the `dot` tool from the http://www.graphviz.org/ package:: $ dot tree.dot -T png -o tree.png """ with codecs.open(filename, "w", "utf-8") as file: for line in self: file.write("%s\n" % line)
[docs] def to_picture(self, filename): """ Write graph to a temporary file and invoke `dot`. The output file type is automatically detected from the file suffix. *`graphviz` needs to be installed, before usage of this method.* """ fileformat = path.splitext(filename)[1][1:] with NamedTemporaryFile("wb", delete=False) as dotfile: dotfilename = dotfile.name for line in self: dotfile.write(("%s\n" % line).encode("utf-8")) dotfile.flush() cmd = ["dot", dotfilename, "-T", fileformat, "-o", filename] check_call(cmd) try: remove(dotfilename) # pylint: disable=broad-exception-caught except Exception: # pragma: no cover logging.getLogger(__name__).warning("Could not remove temporary file %s", dotfilename)
[docs] @staticmethod def esc(value): """Escape Strings.""" return _RE_ESC.sub(lambda m: r"\%s" % m.group(0), six.text_type(value))
[docs]class UniqueDotExporter(DotExporter): """ Unqiue Dot Language Exporter. Handle trees with random or conflicting node names gracefully. Args: node (Node): start node. Keyword Args: graph: DOT graph type. name: DOT graph name. options: list of options added to the graph. indent (int): number of spaces for indent. nodenamefunc: Function to extract node name from `node` object. The function shall accept one `node` object as argument and return the name of it. nodeattrfunc: Function to decorate a node with attributes. The function shall accept one `node` object as argument and return the attributes. edgeattrfunc: Function to decorate a edge with attributes. The function shall accept two `node` objects as argument. The first the node and the second the child and return the attributes. edgetypefunc: Function to which gives the edge type. The function shall accept two `node` objects as argument. The first the node and the second the child and return the edge (i.e. '->'). filter_: Function to filter nodes to include in export. The function shall accept one `node` object as argument and return True if it should be included, or False if it should not be included. stop: stop iteration at `node` if `stop` function returns `True` for `node`. maxlevel (int): Limit export to this number of levels. >>> from anytree import Node >>> root = Node("root") >>> s0 = Node("sub0", parent=root) >>> s0b = Node("s0", parent=s0) >>> s0a = Node("s0", parent=s0) >>> s1 = Node("sub1", parent=root) >>> s1a = Node("s1", parent=s1) >>> s1b = Node("s1", parent=s1) >>> s1c = Node("s1", parent=s1) >>> s1ca = Node("sub1Ca", parent=s1c) >>> from anytree.exporter import UniqueDotExporter >>> for line in UniqueDotExporter(root): ... print(line) digraph tree { "0x0" [label="root"]; "0x1" [label="sub0"]; "0x2" [label="s0"]; "0x3" [label="s0"]; "0x4" [label="sub1"]; "0x5" [label="s1"]; "0x6" [label="s1"]; "0x7" [label="s1"]; "0x8" [label="sub1Ca"]; "0x0" -> "0x1"; "0x0" -> "0x4"; "0x1" -> "0x2"; "0x1" -> "0x3"; "0x4" -> "0x5"; "0x4" -> "0x6"; "0x4" -> "0x7"; "0x7" -> "0x8"; } The resulting graph: .. image:: ../static/uniquedotexporter2.png To export custom node implementations or :any:`AnyNode`, please provide a proper `nodeattrfunc`: >>> from anytree import AnyNode >>> root = AnyNode(id="root") >>> s0 = AnyNode(id="sub0", parent=root) >>> s0b = AnyNode(id="s0", parent=s0) >>> s0a = AnyNode(id="s0", parent=s0) >>> from anytree.exporter import UniqueDotExporter >>> for line in UniqueDotExporter(root, nodeattrfunc=lambda n: 'label="%s"' % (n.id)): ... print(line) digraph tree { "0x0" [label="root"]; "0x1" [label="sub0"]; "0x2" [label="s0"]; "0x3" [label="s0"]; "0x0" -> "0x1"; "0x1" -> "0x2"; "0x1" -> "0x3"; } """ def __init__( self, node, graph="digraph", name="tree", options=None, indent=4, nodenamefunc=None, nodeattrfunc=None, edgeattrfunc=None, edgetypefunc=None, filter_=None, stop=None, maxlevel=None, ): super(UniqueDotExporter, self).__init__( node, graph=graph, name=name, options=options, indent=indent, nodenamefunc=nodenamefunc, nodeattrfunc=nodeattrfunc, edgeattrfunc=edgeattrfunc, edgetypefunc=edgetypefunc, filter_=filter_, stop=stop, maxlevel=maxlevel, ) self.__node_ids = {} self.__node_counter = itertools.count() # pylint: disable=arguments-differ def _default_nodenamefunc(self, node): node_id = id(node) try: num = self.__node_ids[node_id] except KeyError: num = self.__node_ids[node_id] = next(self.__node_counter) return hex(num) @staticmethod def _default_nodeattrfunc(node): return 'label="%s"' % (node.name,)