File size: 13,047 Bytes
d015b2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
"""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)]