Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/graphviz/dot.py: 62%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Create DOT code with method-calls."""
3import contextlib
4import typing
6from . import _tools
7from . import base
8from . import quoting
10__all__ = ['GraphSyntax', 'DigraphSyntax', 'Dot']
13def comment(line: str) -> str:
14 """Return comment header line."""
15 return f'// {line}\n'
18def graph_head(name: str) -> str:
19 """Return DOT graph head line."""
20 return f'graph {name}{{\n'
23def digraph_head(name: str) -> str:
24 """Return DOT digraph head line."""
25 return f'digraph {name}{{\n'
28def graph_edge(*, tail: str, head: str, attr: str) -> str:
29 """Return DOT graph edge statement line."""
30 return f'\t{tail} -- {head}{attr}\n'
33def digraph_edge(*, tail: str, head: str, attr: str) -> str:
34 """Return DOT digraph edge statement line."""
35 return f'\t{tail} -> {head}{attr}\n'
38class GraphSyntax:
39 """DOT graph head and edge syntax."""
41 _head = staticmethod(graph_head)
43 _edge = staticmethod(graph_edge)
46class DigraphSyntax:
47 """DOT digraph head and edge syntax."""
49 _head = staticmethod(digraph_head)
51 _edge = staticmethod(digraph_edge)
54def subgraph(name: str) -> str:
55 """Return DOT subgraph head line."""
56 return f'subgraph {name}{{\n'
59def subgraph_plain(name: str) -> str:
60 """Return plain DOT subgraph head line."""
61 return f'{name}{{\n'
64def node(left: str, right: str) -> str:
65 """Return DOT node statement line."""
66 return f'\t{left}{right}\n'
69class Dot(quoting.Quote, base.Base):
70 """Assemble DOT source code."""
72 directed: bool
74 _comment = staticmethod(comment)
76 @staticmethod
77 def _head(name: str) -> str: # pragma: no cover
78 """Return DOT head line."""
79 raise NotImplementedError('must be implemented by concrete subclasses')
81 @classmethod
82 def _head_strict(cls, name: str) -> str:
83 """Return DOT strict head line."""
84 return f'strict {cls._head(name)}'
86 _tail = '}\n'
88 _subgraph = staticmethod(subgraph)
90 _subgraph_plain = staticmethod(subgraph_plain)
92 _node = _attr = staticmethod(node)
94 @classmethod
95 def _attr_plain(cls, left: str) -> str:
96 return cls._attr(left, '')
98 @staticmethod
99 def _edge(*, tail: str, head: str, attr: str) -> str: # pragma: no cover
100 """Return DOT edge statement line."""
101 raise NotImplementedError('must be implemented by concrete subclasses')
103 @classmethod
104 def _edge_plain(cls, *, tail: str, head: str) -> str:
105 """Return plain DOT edge statement line."""
106 return cls._edge(tail=tail, head=head, attr='')
108 def __init__(self, *,
109 name: typing.Optional[str] = None,
110 comment: typing.Optional[str] = None,
111 graph_attr=None, node_attr=None, edge_attr=None, body=None,
112 strict: bool = False, **kwargs) -> None:
113 super().__init__(**kwargs)
115 self.name = name
116 """str: DOT source identifier for the ``graph`` or ``digraph`` statement."""
118 self.comment = comment
119 """str: DOT source comment for the first source line."""
121 self.graph_attr = dict(graph_attr) if graph_attr is not None else {}
122 """~typing.Dict[str, str]: Attribute-value pairs applying to the graph."""
124 self.node_attr = dict(node_attr) if node_attr is not None else {}
125 """~typing.Dict[str, str]: Attribute-value pairs applying to all nodes."""
127 self.edge_attr = dict(edge_attr) if edge_attr is not None else {}
128 """~typing.Dict[str, str]: Attribute-value pairs applying to all edges."""
130 self.body = list(body) if body is not None else []
131 """~typing.List[str]: Verbatim DOT source lines including final newline."""
133 self.strict = strict
134 """bool: Rendering should merge multi-edges."""
136 def _copy_kwargs(self, **kwargs):
137 """Return the kwargs to create a copy of the instance."""
138 return super()._copy_kwargs(name=self.name,
139 comment=self.comment,
140 graph_attr=dict(self.graph_attr),
141 node_attr=dict(self.node_attr),
142 edge_attr=dict(self.edge_attr),
143 body=list(self.body),
144 strict=self.strict)
146 @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self')
147 def clear(self, keep_attrs: bool = False) -> None:
148 """Reset content to an empty body, clear graph/node/egde_attr mappings.
150 Args:
151 keep_attrs (bool): preserve graph/node/egde_attr mappings
152 """
153 if not keep_attrs:
154 for a in (self.graph_attr, self.node_attr, self.edge_attr):
155 a.clear()
156 self.body.clear()
158 @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self')
159 def __iter__(self, subgraph: bool = False) -> typing.Iterator[str]:
160 r"""Yield the DOT source code line by line (as graph or subgraph).
162 Yields: Line ending with a newline (``'\n'``).
163 """
164 if self.comment:
165 yield self._comment(self.comment)
167 if subgraph:
168 if self.strict:
169 raise ValueError('subgraphs cannot be strict')
170 head = self._subgraph if self.name else self._subgraph_plain
171 else:
172 head = self._head_strict if self.strict else self._head
173 yield head(self._quote(self.name) + ' ' if self.name else '')
175 for kw in ('graph', 'node', 'edge'):
176 attrs = getattr(self, f'{kw}_attr')
177 if attrs:
178 yield self._attr(kw, self._attr_list(None, kwargs=attrs))
180 yield from self.body
182 yield self._tail
184 @_tools.deprecate_positional_args(supported_number=2, ignore_arg='self',
185 category=DeprecationWarning)
186 def node(self, name: str,
187 label: typing.Optional[str] = None,
188 _attributes=None, **attrs) -> None:
189 """Create a node.
191 Args:
192 name: Unique identifier for the node inside the source.
193 label: Caption to be displayed (defaults to the node ``name``).
194 attrs: Any additional node attributes (must be strings).
196 Attention:
197 When rendering ``label``, backslash-escapes
198 and strings of the form ``<...>`` have a special meaning.
199 See the sections :ref:`backslash-escapes` and
200 :ref:`quoting-and-html-like-labels` in the user guide for details.
201 """
202 name = self._quote(name)
203 attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
204 line = self._node(name, attr_list)
205 self.body.append(line)
207 @_tools.deprecate_positional_args(supported_number=3, ignore_arg='self',
208 category=DeprecationWarning)
209 def edge(self, tail_name: str, head_name: str,
210 label: typing.Optional[str] = None,
211 _attributes=None, **attrs) -> None:
212 """Create an edge between two nodes.
214 Args:
215 tail_name: Start node identifier
216 (format: ``node[:port[:compass]]``).
217 head_name: End node identifier
218 (format: ``node[:port[:compass]]``).
219 label: Caption to be displayed near the edge.
220 attrs: Any additional edge attributes (must be strings).
222 Note:
223 The ``tail_name`` and ``head_name`` strings are separated
224 by (optional) colon(s) into ``node`` name, ``port`` name,
225 and ``compass`` (e.g. ``sw``).
226 See :ref:`details in the User Guide <node-ports-compass>`.
228 Attention:
229 When rendering ``label``, backslash-escapes
230 and strings of the form ``<...>`` have a special meaning.
231 See the sections :ref:`backslash-escapes` and
232 :ref:`quoting-and-html-like-labels` in the user guide for details.
233 """
234 tail_name = self._quote_edge(tail_name)
235 head_name = self._quote_edge(head_name)
236 attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
237 line = self._edge(tail=tail_name, head=head_name, attr=attr_list)
238 self.body.append(line)
240 def edges(self, tail_head_iter) -> None:
241 """Create a bunch of edges.
243 Args:
244 tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs
245 (format:``node[:port[:compass]]``).
248 Note:
249 The ``tail_name`` and ``head_name`` strings are separated
250 by (optional) colon(s) into ``node`` name, ``port`` name,
251 and ``compass`` (e.g. ``sw``).
252 See :ref:`details in the User Guide <node-ports-compass>`.
253 """
254 edge = self._edge_plain
255 quote = self._quote_edge
256 self.body += [edge(tail=quote(t), head=quote(h))
257 for t, h in tail_head_iter]
259 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self',
260 category=DeprecationWarning)
261 def attr(self, kw: typing.Optional[str] = None,
262 _attributes=None, **attrs) -> None:
263 """Add a general or graph/node/edge attribute statement.
265 Args:
266 kw: Attributes target
267 (``None`` or ``'graph'``, ``'node'``, ``'edge'``).
268 attrs: Attributes to be set (must be strings, may be empty).
270 See the :ref:`usage examples in the User Guide <attributes>`.
271 """
272 if kw is not None and kw.lower() not in ('graph', 'node', 'edge'):
273 raise ValueError('attr statement must target graph, node, or edge:'
274 f' {kw!r}')
275 if attrs or _attributes:
276 if kw is None:
277 a_list = self._a_list(None, kwargs=attrs, attributes=_attributes)
278 line = self._attr_plain(a_list)
279 else:
280 attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes)
281 line = self._attr(kw, attr_list)
282 self.body.append(line)
284 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self')
285 def subgraph(self, graph=None,
286 name: typing.Optional[str] = None,
287 comment: typing.Optional[str] = None,
288 graph_attr=None, node_attr=None, edge_attr=None,
289 body=None):
290 """Add the current content of the given sole ``graph`` argument
291 as subgraph or return a context manager
292 returning a new graph instance
293 created with the given (``name``, ``comment``, etc.) arguments
294 whose content is added as subgraph
295 when leaving the context manager's ``with``-block.
297 Args:
298 graph: An instance of the same kind
299 (:class:`.Graph`, :class:`.Digraph`) as the current graph
300 (sole argument in non-with-block use).
301 name: Subgraph name (``with``-block use).
302 comment: Subgraph comment (``with``-block use).
303 graph_attr: Subgraph-level attribute-value mapping
304 (``with``-block use).
305 node_attr: Node-level attribute-value mapping
306 (``with``-block use).
307 edge_attr: Edge-level attribute-value mapping
308 (``with``-block use).
309 body: Verbatim lines to add to the subgraph ``body``
310 (``with``-block use).
312 See the :ref:`usage examples in the User Guide <subgraphs-clusters>`.
314 When used as a context manager, the returned new graph instance
315 uses ``strict=None`` and the parent graph's values
316 for ``directory``, ``format``, ``engine``, and ``encoding`` by default.
318 Note:
319 If the ``name`` of the subgraph begins with
320 ``'cluster'`` (all lowercase)
321 the layout engine will treat it as a special cluster subgraph.
322 """
323 if graph is None:
324 kwargs = self._copy_kwargs()
325 kwargs.pop('filename', None)
326 kwargs.update(name=name, comment=comment,
327 graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr,
328 body=body, strict=None)
329 subgraph = self.__class__(**kwargs)
331 @contextlib.contextmanager
332 def subgraph_contextmanager(*, parent):
333 """Return subgraph and add to parent on exit."""
334 yield subgraph
335 parent.subgraph(subgraph)
337 return subgraph_contextmanager(parent=self)
339 args = [name, comment, graph_attr, node_attr, edge_attr, body]
340 if not all(a is None for a in args):
341 raise ValueError('graph must be sole argument of subgraph()')
343 if graph.directed != self.directed:
344 raise ValueError(f'{self!r} cannot add subgraph of different kind:'
345 f' {graph!r}')
347 self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)]