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

133 statements  

1"""Create DOT code with method-calls.""" 

2 

3import contextlib 

4import typing 

5 

6from . import _tools 

7from . import base 

8from . import quoting 

9 

10__all__ = ['GraphSyntax', 'DigraphSyntax', 'Dot'] 

11 

12 

13def comment(line: str) -> str: 

14 """Return comment header line.""" 

15 return f'// {line}\n' 

16 

17 

18def graph_head(name: str) -> str: 

19 """Return DOT graph head line.""" 

20 return f'graph {name}{{\n' 

21 

22 

23def digraph_head(name: str) -> str: 

24 """Return DOT digraph head line.""" 

25 return f'digraph {name}{{\n' 

26 

27 

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' 

31 

32 

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' 

36 

37 

38class GraphSyntax: 

39 """DOT graph head and edge syntax.""" 

40 

41 _head = staticmethod(graph_head) 

42 

43 _edge = staticmethod(graph_edge) 

44 

45 

46class DigraphSyntax: 

47 """DOT digraph head and edge syntax.""" 

48 

49 _head = staticmethod(digraph_head) 

50 

51 _edge = staticmethod(digraph_edge) 

52 

53 

54def subgraph(name: str) -> str: 

55 """Return DOT subgraph head line.""" 

56 return f'subgraph {name}{{\n' 

57 

58 

59def subgraph_plain(name: str) -> str: 

60 """Return plain DOT subgraph head line.""" 

61 return f'{name}{{\n' 

62 

63 

64def node(left: str, right: str) -> str: 

65 """Return DOT node statement line.""" 

66 return f'\t{left}{right}\n' 

67 

68 

69class Dot(quoting.Quote, base.Base): 

70 """Assemble DOT source code.""" 

71 

72 directed: bool 

73 

74 _comment = staticmethod(comment) 

75 

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') 

80 

81 @classmethod 

82 def _head_strict(cls, name: str) -> str: 

83 """Return DOT strict head line.""" 

84 return f'strict {cls._head(name)}' 

85 

86 _tail = '}\n' 

87 

88 _subgraph = staticmethod(subgraph) 

89 

90 _subgraph_plain = staticmethod(subgraph_plain) 

91 

92 _node = _attr = staticmethod(node) 

93 

94 @classmethod 

95 def _attr_plain(cls, left: str) -> str: 

96 return cls._attr(left, '') 

97 

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') 

102 

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='') 

107 

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) 

114 

115 self.name = name 

116 """str: DOT source identifier for the ``graph`` or ``digraph`` statement.""" 

117 

118 self.comment = comment 

119 """str: DOT source comment for the first source line.""" 

120 

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.""" 

123 

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.""" 

126 

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.""" 

129 

130 self.body = list(body) if body is not None else [] 

131 """~typing.List[str]: Verbatim DOT source lines including final newline.""" 

132 

133 self.strict = strict 

134 """bool: Rendering should merge multi-edges.""" 

135 

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) 

145 

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. 

149 

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() 

157 

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). 

161 

162 Yields: Line ending with a newline (``'\n'``). 

163 """ 

164 if self.comment: 

165 yield self._comment(self.comment) 

166 

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 '') 

174 

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)) 

179 

180 yield from self.body 

181 

182 yield self._tail 

183 

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. 

190 

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). 

195 

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) 

206 

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. 

213 

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). 

221 

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>`. 

227 

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) 

239 

240 def edges(self, tail_head_iter) -> None: 

241 """Create a bunch of edges. 

242 

243 Args: 

244 tail_head_iter: Iterable of ``(tail_name, head_name)`` pairs 

245 (format:``node[:port[:compass]]``). 

246 

247 

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] 

258 

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. 

264 

265 Args: 

266 kw: Attributes target 

267 (``None`` or ``'graph'``, ``'node'``, ``'edge'``). 

268 attrs: Attributes to be set (must be strings, may be empty). 

269 

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) 

283 

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. 

296 

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). 

311 

312 See the :ref:`usage examples in the User Guide <subgraphs-clusters>`. 

313 

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. 

317 

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) 

330 

331 @contextlib.contextmanager 

332 def subgraph_contextmanager(*, parent): 

333 """Return subgraph and add to parent on exit.""" 

334 yield subgraph 

335 parent.subgraph(subgraph) 

336 

337 return subgraph_contextmanager(parent=self) 

338 

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()') 

342 

343 if graph.directed != self.directed: 

344 raise ValueError(f'{self!r} cannot add subgraph of different kind:' 

345 f' {graph!r}') 

346 

347 self.body += [f'\t{line}' for line in graph.__iter__(subgraph=True)]