Niharmahesh's picture
Upload folder using huggingface_hub
d015b2a verified
"""Create DOT code with method-calls."""
import contextlib
import typing
from . import _tools
from . import base
from . import quoting
__all__ = ['GraphSyntax', 'DigraphSyntax', 'Dot']
def comment(line: str) -> str:
"""Return comment header line."""
return f'// {line}\n'
def graph_head(name: str) -> str:
"""Return DOT graph head line."""
return f'graph {name}{{\n'
def digraph_head(name: str) -> str:
"""Return DOT digraph head line."""
return f'digraph {name}{{\n'
def graph_edge(*, tail: str, head: str, attr: str) -> str:
"""Return DOT graph edge statement line."""
return f'\t{tail} -- {head}{attr}\n'
def digraph_edge(*, tail: str, head: str, attr: str) -> str:
"""Return DOT digraph edge statement line."""
return f'\t{tail} -> {head}{attr}\n'
class GraphSyntax:
"""DOT graph head and edge syntax."""
_head = staticmethod(graph_head)
_edge = staticmethod(graph_edge)
class DigraphSyntax:
"""DOT digraph head and edge syntax."""
_head = staticmethod(digraph_head)
_edge = staticmethod(digraph_edge)
def subgraph(name: str) -> str:
"""Return DOT subgraph head line."""
return f'subgraph {name}{{\n'
def subgraph_plain(name: str) -> str:
"""Return plain DOT subgraph head line."""
return f'{name}{{\n'
def node(left: str, right: str) -> str:
"""Return DOT node statement line."""
return f'\t{left}{right}\n'
class Dot(quoting.Quote, base.Base):
"""Assemble DOT source code."""
directed: bool
_comment = staticmethod(comment)
@staticmethod
def _head(name: str) -> str: # pragma: no cover
"""Return DOT head line."""
raise NotImplementedError('must be implemented by concrete subclasses')
@classmethod
def _head_strict(cls, name: str) -> str:
"""Return DOT strict head line."""
return f'strict {cls._head(name)}'
_tail = '}\n'
_subgraph = staticmethod(subgraph)
_subgraph_plain = staticmethod(subgraph_plain)
_node = _attr = staticmethod(node)
@classmethod
def _attr_plain(cls, left: str) -> str:
return cls._attr(left, '')
@staticmethod
def _edge(*, tail: str, head: str, attr: str) -> str: # pragma: no cover
"""Return DOT edge statement line."""
raise NotImplementedError('must be implemented by concrete subclasses')
@classmethod
def _edge_plain(cls, *, tail: str, head: str) -> str:
"""Return plain DOT edge statement line."""
return cls._edge(tail=tail, head=head, attr='')
def __init__(self, *,
name: typing.Optional[str] = None,
comment: typing.Optional[str] = None,
graph_attr=None, node_attr=None, edge_attr=None, body=None,
strict: bool = False, **kwargs) -> None:
super().__init__(**kwargs)
self.name = name
"""str: DOT source identifier for the ``graph`` or ``digraph`` statement."""
self.comment = comment
"""str: DOT source comment for the first source line."""
self.graph_attr = dict(graph_attr) if graph_attr is not None else {}
"""~typing.Dict[str, str]: Attribute-value pairs applying to the graph."""
self.node_attr = dict(node_attr) if node_attr is not None else {}
"""~typing.Dict[str, str]: Attribute-value pairs applying to all nodes."""
self.edge_attr = dict(edge_attr) if edge_attr is not None else {}
"""~typing.Dict[str, str]: Attribute-value pairs applying to all edges."""
self.body = list(body) if body is not None else []
"""~typing.List[str]: Verbatim DOT source lines including final newline."""
self.strict = strict
"""bool: Rendering should merge multi-edges."""
def _copy_kwargs(self, **kwargs):
"""Return the kwargs to create a copy of the instance."""
return super()._copy_kwargs(name=self.name,
comment=self.comment,
graph_attr=dict(self.graph_attr),
node_attr=dict(self.node_attr),
edge_attr=dict(self.edge_attr),
body=list(self.body),
strict=self.strict)
@_tools.deprecate_positional_args(supported_number=1)
def clear(self, keep_attrs: bool = False) -> None:
"""Reset content to an empty body, clear graph/node/egde_attr mappings.
Args:
keep_attrs (bool): preserve graph/node/egde_attr mappings
"""
if not keep_attrs:
for a in (self.graph_attr, self.node_attr, self.edge_attr):
a.clear()
self.body.clear()
@_tools.deprecate_positional_args(supported_number=1)
def __iter__(self, subgraph: bool = False) -> typing.Iterator[str]:
r"""Yield the DOT source code line by line (as graph or subgraph).
Yields: Line ending with a newline (``'\n'``).
"""
if self.comment:
yield self._comment(self.comment)
if subgraph:
if self.strict:
raise ValueError('subgraphs cannot be strict')
head = self._subgraph if self.name else self._subgraph_plain
else:
head = self._head_strict if self.strict else self._head
yield head(self._quote(self.name) + ' ' if self.name else '')
for kw in ('graph', 'node', 'edge'):
attrs = getattr(self, f'{kw}_attr')
if attrs:
yield self._attr(kw, self._attr_list(None, kwargs=attrs))
yield from self.body
yield self._tail
@_tools.deprecate_positional_args(supported_number=3)
def node(self, name: str,
label: typing.Optional[str] = None,
_attributes=None, **attrs) -> None:
"""Create a node.
Args:
name: Unique identifier for the node inside the source.
label: Caption to be displayed (defaults to the node ``name``).
attrs: Any additional node attributes (must be strings).
Attention:
When rendering ``label``, backslash-escapes
and strings of the form ``<...>`` have a special meaning.
See the sections :ref:`backslash-escapes` and
:ref:`quoting-and-html-like-labels` in the user guide for details.
"""
name = self._quote(name)
attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
line = self._node(name, attr_list)
self.body.append(line)
@_tools.deprecate_positional_args(supported_number=4)
def edge(self, tail_name: str, head_name: str,
label: typing.Optional[str] = None,
_attributes=None, **attrs) -> None:
"""Create an edge between two nodes.
Args:
tail_name: Start node identifier
(format: ``node[:port[:compass]]``).
head_name: End node identifier
(format: ``node[:port[:compass]]``).
label: Caption to be displayed near the edge.
attrs: Any additional edge attributes (must be strings).
Note:
The ``tail_name`` and ``head_name`` strings are separated
by (optional) colon(s) into ``node`` name, ``port`` name,
and ``compass`` (e.g. ``sw``).
See :ref:`details in the User Guide <node-ports-compass>`.
Attention:
When rendering ``label``, backslash-escapes
and strings of the form ``<...>`` have a special meaning.
See the sections :ref:`backslash-escapes` and
:ref:`quoting-and-html-like-labels` in the user guide for details.
"""
tail_name = self._quote_edge(tail_name)
head_name = self._quote_edge(head_name)
attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
line = self._edge(tail=tail_name, head=head_name, attr=attr_list)
self.body.append(line)
def edges(self, tail_head_iter) -> None:
"""Create a bunch of edges.
Args:
tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs
(format:``node[:port[:compass]]``).
Note:
The ``tail_name`` and ``head_name`` strings are separated
by (optional) colon(s) into ``node`` name, ``port`` name,
and ``compass`` (e.g. ``sw``).
See :ref:`details in the User Guide <node-ports-compass>`.
"""
edge = self._edge_plain
quote = self._quote_edge
self.body += [edge(tail=quote(t), head=quote(h))
for t, h in tail_head_iter]
@_tools.deprecate_positional_args(supported_number=2)
def attr(self, kw: typing.Optional[str] = None,
_attributes=None, **attrs) -> None:
"""Add a general or graph/node/edge attribute statement.
Args:
kw: Attributes target
(``None`` or ``'graph'``, ``'node'``, ``'edge'``).
attrs: Attributes to be set (must be strings, may be empty).
See the :ref:`usage examples in the User Guide <attributes>`.
"""
if kw is not None and kw.lower() not in ('graph', 'node', 'edge'):
raise ValueError('attr statement must target graph, node, or edge:'
f' {kw!r}')
if attrs or _attributes:
if kw is None:
a_list = self._a_list(None, kwargs=attrs, attributes=_attributes)
line = self._attr_plain(a_list)
else:
attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes)
line = self._attr(kw, attr_list)
self.body.append(line)
@_tools.deprecate_positional_args(supported_number=2)
def subgraph(self, graph=None,
name: typing.Optional[str] = None,
comment: typing.Optional[str] = None,
graph_attr=None, node_attr=None, edge_attr=None,
body=None):
"""Add the current content of the given sole ``graph`` argument
as subgraph or return a context manager
returning a new graph instance
created with the given (``name``, ``comment``, etc.) arguments
whose content is added as subgraph
when leaving the context manager's ``with``-block.
Args:
graph: An instance of the same kind
(:class:`.Graph`, :class:`.Digraph`) as the current graph
(sole argument in non-with-block use).
name: Subgraph name (``with``-block use).
comment: Subgraph comment (``with``-block use).
graph_attr: Subgraph-level attribute-value mapping
(``with``-block use).
node_attr: Node-level attribute-value mapping
(``with``-block use).
edge_attr: Edge-level attribute-value mapping
(``with``-block use).
body: Verbatim lines to add to the subgraph ``body``
(``with``-block use).
See the :ref:`usage examples in the User Guide <subgraphs-clusters>`.
When used as a context manager, the returned new graph instance
uses ``strict=None`` and the parent graph's values
for ``directory``, ``format``, ``engine``, and ``encoding`` by default.
Note:
If the ``name`` of the subgraph begins with
``'cluster'`` (all lowercase)
the layout engine will treat it as a special cluster subgraph.
"""
if graph is None:
kwargs = self._copy_kwargs()
kwargs.pop('filename', None)
kwargs.update(name=name, comment=comment,
graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr,
body=body, strict=None)
subgraph = self.__class__(**kwargs)
@contextlib.contextmanager
def subgraph_contextmanager(*, parent):
"""Return subgraph and add to parent on exit."""
yield subgraph
parent.subgraph(subgraph)
return subgraph_contextmanager(parent=self)
args = [name, comment, graph_attr, node_attr, edge_attr, body]
if not all(a is None for a in args):
raise ValueError('graph must be sole argument of subgraph()')
if graph.directed != self.directed:
raise ValueError(f'{self!r} cannot add subgraph of different kind:'
f' {graph!r}')
self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)]