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."""
3from collections.abc import Iterable, Iterator, Mapping
4import contextlib
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 @property
73 def directed(self) -> bool: # pragma: no cover
74 raise NotImplementedError('must be implemented by concrete subclasses')
76 _comment = staticmethod(comment)
78 @staticmethod
79 def _head(name: str) -> str: # pragma: no cover
80 """Return DOT head line."""
81 raise NotImplementedError('must be implemented by concrete subclasses')
83 @classmethod
84 def _head_strict(cls, name: str) -> str:
85 """Return DOT strict head line."""
86 return f'strict {cls._head(name)}'
88 _tail = '}\n'
90 _subgraph = staticmethod(subgraph)
92 _subgraph_plain = staticmethod(subgraph_plain)
94 _node = _attr = staticmethod(node)
96 @classmethod
97 def _attr_plain(cls, left: str) -> str:
98 return cls._attr(left, '')
100 @staticmethod
101 def _edge(*, tail: str, head: str, attr: str) -> str: # pragma: no cover
102 """Return DOT edge statement line."""
103 raise NotImplementedError('must be implemented by concrete subclasses')
105 @classmethod
106 def _edge_plain(cls, *, tail: str, head: str) -> str:
107 """Return plain DOT edge statement line."""
108 return cls._edge(tail=tail, head=head, attr='')
110 def __init__(self, *,
111 name: str | None = None,
112 comment: str | None = None,
113 graph_attr: Mapping[str, str] | None = None,
114 node_attr: Mapping[str, str] | None = None,
115 edge_attr: Mapping[str, str] | None = None,
116 body: Iterable[str] | None = None,
117 strict: bool = False, **kwargs) -> None:
118 super().__init__(**kwargs)
120 self.name: str | None = name
121 """DOT source identifier for the ``graph`` or ``digraph`` statement."""
123 self.comment: str | None = comment
124 """DOT source comment for the first source line."""
126 self.graph_attr: dict[str, str] = dict(graph_attr) if graph_attr is not None else {}
127 """Attribute-value pairs applying to the graph."""
129 self.node_attr: dict[str, str] = dict(node_attr) if node_attr is not None else {}
130 """Attribute-value pairs applying to all nodes."""
132 self.edge_attr: dict[str, str] = dict(edge_attr) if edge_attr is not None else {}
133 """Attribute-value pairs applying to all edges."""
135 self.body: list[str] = list(body) if body is not None else []
136 """Verbatim DOT source lines including final newline."""
138 self.strict: bool = strict
139 """Rendering should merge multi-edges."""
141 def _copy_kwargs(self, **kwargs):
142 """Return the kwargs to create a copy of the instance."""
143 return super()._copy_kwargs(name=self.name,
144 comment=self.comment,
145 graph_attr=dict(self.graph_attr),
146 node_attr=dict(self.node_attr),
147 edge_attr=dict(self.edge_attr),
148 body=list(self.body),
149 strict=self.strict)
151 @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self')
152 def clear(self, keep_attrs: bool = False) -> None:
153 """Reset content to an empty body, clear graph/node/egde_attr mappings.
155 Args:
156 keep_attrs (bool): preserve graph/node/egde_attr mappings
157 """
158 if not keep_attrs:
159 for a in (self.graph_attr, self.node_attr, self.edge_attr):
160 a.clear()
161 self.body.clear()
163 @_tools.deprecate_positional_args(supported_number=0, ignore_arg='self')
164 def __iter__(self, subgraph: bool = False) -> Iterator[str]:
165 r"""Yield the DOT source code line by line (as graph or subgraph).
167 Yields: Line ending with a newline (``'\n'``).
168 """
169 if self.comment:
170 yield self._comment(self.comment)
172 if subgraph:
173 if self.strict:
174 raise ValueError('subgraphs cannot be strict')
175 head = self._subgraph if self.name else self._subgraph_plain
176 else:
177 head = self._head_strict if self.strict else self._head
178 yield head(self._quote(self.name) + ' ' if self.name else '')
180 for kw in ('graph', 'node', 'edge'):
181 attrs = getattr(self, f'{kw}_attr')
182 if attrs:
183 yield self._attr(kw, self._attr_list(None, kwargs=attrs))
185 yield from self.body
187 yield self._tail
189 @_tools.deprecate_positional_args(supported_number=2, ignore_arg='self',
190 category=DeprecationWarning)
191 def node(self, name: str,
192 label: str | None = None,
193 _attributes=None, **attrs: str) -> None:
194 """Create a node.
196 Args:
197 name: Unique identifier for the node inside the source.
198 label: Caption to be displayed (defaults to the node ``name``).
199 attrs: Any additional node attributes (must be strings).
201 Attention:
202 When rendering ``label``, backslash-escapes
203 and strings of the form ``<...>`` have a special meaning.
204 See the sections :ref:`backslash-escapes` and
205 :ref:`quoting-and-html-like-labels` in the user guide for details.
206 """
207 name = self._quote(name)
208 attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
209 line = self._node(name, attr_list)
210 self.body.append(line)
212 @_tools.deprecate_positional_args(supported_number=3, ignore_arg='self',
213 category=DeprecationWarning)
214 def edge(self, tail_name: str, head_name: str,
215 label: str | None = None,
216 _attributes=None, **attrs: str) -> None:
217 """Create an edge between two nodes.
219 Args:
220 tail_name: Start node identifier
221 (format: ``node[:port[:compass]]``).
222 head_name: End node identifier
223 (format: ``node[:port[:compass]]``).
224 label: Caption to be displayed near the edge.
225 attrs: Any additional edge attributes (must be strings).
227 Note:
228 The ``tail_name`` and ``head_name`` strings are separated
229 by (optional) colon(s) into ``node`` name, ``port`` name,
230 and ``compass`` (e.g. ``sw``).
231 See :ref:`details in the User Guide <node-ports-compass>`.
233 Attention:
234 When rendering ``label``, backslash-escapes
235 and strings of the form ``<...>`` have a special meaning.
236 See the sections :ref:`backslash-escapes` and
237 :ref:`quoting-and-html-like-labels` in the user guide for details.
238 """
239 tail_name = self._quote_edge(tail_name)
240 head_name = self._quote_edge(head_name)
241 attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes)
242 line = self._edge(tail=tail_name, head=head_name, attr=attr_list)
243 self.body.append(line)
245 def edges(self, tail_head_iter: Iterable[tuple[str, str]]) -> None:
246 """Create a bunch of edges.
248 Args:
249 tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs
250 (format:``node[:port[:compass]]``).
253 Note:
254 The ``tail_name`` and ``head_name`` strings are separated
255 by (optional) colon(s) into ``node`` name, ``port`` name,
256 and ``compass`` (e.g. ``sw``).
257 See :ref:`details in the User Guide <node-ports-compass>`.
258 """
259 edge = self._edge_plain
260 quote = self._quote_edge
261 self.body += [edge(tail=quote(t), head=quote(h))
262 for t, h in tail_head_iter]
264 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self',
265 category=DeprecationWarning)
266 def attr(self, kw: str | None = None,
267 _attributes=None, **attrs: str) -> None:
268 """Add a general or graph/node/edge attribute statement.
270 Args:
271 kw: Attributes target
272 (``None`` or ``'graph'``, ``'node'``, ``'edge'``).
273 attrs: Attributes to be set (must be strings, may be empty).
275 See the :ref:`usage examples in the User Guide <attributes>`.
276 """
277 if kw is not None and kw.lower() not in ('graph', 'node', 'edge'):
278 raise ValueError('attr statement must target graph, node, or edge:'
279 f' {kw!r}')
280 if attrs or _attributes:
281 if kw is None:
282 a_list = self._a_list(None, kwargs=attrs, attributes=_attributes)
283 line = self._attr_plain(a_list)
284 else:
285 attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes)
286 line = self._attr(kw, attr_list)
287 self.body.append(line)
289 @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self')
290 def subgraph(self, graph=None,
291 name: str | None = None,
292 comment: str | None = None,
293 graph_attr: Mapping[str, str] | None = None,
294 node_attr: Mapping[str, str] | None = None,
295 edge_attr: Mapping[str, str] | None = None,
296 body=None):
297 """Add the current content of the given sole ``graph`` argument
298 as subgraph or return a context manager
299 returning a new graph instance
300 created with the given (``name``, ``comment``, etc.) arguments
301 whose content is added as subgraph
302 when leaving the context manager's ``with``-block.
304 Args:
305 graph: An instance of the same kind
306 (:class:`.Graph`, :class:`.Digraph`) as the current graph
307 (sole argument in non-with-block use).
308 name: Subgraph name (``with``-block use).
309 comment: Subgraph comment (``with``-block use).
310 graph_attr: Subgraph-level attribute-value mapping
311 (``with``-block use).
312 node_attr: Node-level attribute-value mapping
313 (``with``-block use).
314 edge_attr: Edge-level attribute-value mapping
315 (``with``-block use).
316 body: Verbatim lines to add to the subgraph ``body``
317 (``with``-block use).
319 See the :ref:`usage examples in the User Guide <subgraphs-clusters>`.
321 When used as a context manager, the returned new graph instance
322 uses ``strict=None`` and the parent graph's values
323 for ``directory``, ``format``, ``engine``, and ``encoding`` by default.
325 Note:
326 If the ``name`` of the subgraph begins with
327 ``'cluster'`` (all lowercase)
328 the layout engine will treat it as a special cluster subgraph.
329 """
330 if graph is None:
331 kwargs = self._copy_kwargs()
332 kwargs.pop('filename', None)
333 kwargs.update(name=name, comment=comment,
334 graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr,
335 body=body, strict=None)
336 subgraph = self.__class__(**kwargs)
338 @contextlib.contextmanager
339 def subgraph_contextmanager(*, parent):
340 """Return subgraph and add to parent on exit."""
341 yield subgraph
342 parent.subgraph(subgraph)
344 return subgraph_contextmanager(parent=self)
346 args = [name, comment, graph_attr, node_attr, edge_attr, body]
347 if not all(a is None for a in args):
348 raise ValueError('graph must be sole argument of subgraph()')
350 if graph.directed != self.directed:
351 raise ValueError(f'{self!r} cannot add subgraph of different kind:'
352 f' {graph!r}')
354 self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)]