Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/graphviz/dot.py: 60%

126 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:43 +0000

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

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

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

185 def node(self, name: str, 

186 label: typing.Optional[str] = None, 

187 _attributes=None, **attrs) -> None: 

188 """Create a node. 

189 

190 Args: 

191 name: Unique identifier for the node inside the source. 

192 label: Caption to be displayed (defaults to the node ``name``). 

193 attrs: Any additional node attributes (must be strings). 

194 """ 

195 name = self._quote(name) 

196 attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes) 

197 line = self._node(name, attr_list) 

198 self.body.append(line) 

199 

200 @_tools.deprecate_positional_args(supported_number=4) 

201 def edge(self, tail_name: str, head_name: str, 

202 label: typing.Optional[str] = None, 

203 _attributes=None, **attrs) -> None: 

204 """Create an edge between two nodes. 

205 

206 Args: 

207 tail_name: Start node identifier 

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

209 head_name: End node identifier 

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

211 label: Caption to be displayed near the edge. 

212 attrs: Any additional edge attributes (must be strings). 

213 

214 Note: 

215 The ``tail_name`` and ``head_name`` strings are separated 

216 by (optional) colon(s) into ``node`` name, ``port`` name, 

217 and ``compass`` (e.g. ``sw``). 

218 See :ref:`details in the User Guide <node-ports-compass>`. 

219 """ 

220 tail_name = self._quote_edge(tail_name) 

221 head_name = self._quote_edge(head_name) 

222 attr_list = self._attr_list(label, kwargs=attrs, attributes=_attributes) 

223 line = self._edge(tail=tail_name, head=head_name, attr=attr_list) 

224 self.body.append(line) 

225 

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

227 """Create a bunch of edges. 

228 

229 Args: 

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

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

232 

233 

234 Note: 

235 The ``tail_name`` and ``head_name`` strings are separated 

236 by (optional) colon(s) into ``node`` name, ``port`` name, 

237 and ``compass`` (e.g. ``sw``). 

238 See :ref:`details in the User Guide <node-ports-compass>`. 

239 """ 

240 edge = self._edge_plain 

241 quote = self._quote_edge 

242 self.body += [edge(tail=quote(t), head=quote(h)) 

243 for t, h in tail_head_iter] 

244 

245 @_tools.deprecate_positional_args(supported_number=2) 

246 def attr(self, kw: typing.Optional[str] = None, 

247 _attributes=None, **attrs) -> None: 

248 """Add a general or graph/node/edge attribute statement. 

249 

250 Args: 

251 kw: Attributes target 

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

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

254 

255 See the :ref:`usage examples in the User Guide <attributes>`. 

256 """ 

257 if kw is not None and kw.lower() not in ('graph', 'node', 'edge'): 

258 raise ValueError('attr statement must target graph, node, or edge:' 

259 f' {kw!r}') 

260 if attrs or _attributes: 

261 if kw is None: 

262 a_list = self._a_list(None, kwargs=attrs, attributes=_attributes) 

263 line = self._attr_plain(a_list) 

264 else: 

265 attr_list = self._attr_list(None, kwargs=attrs, attributes=_attributes) 

266 line = self._attr(kw, attr_list) 

267 self.body.append(line) 

268 

269 @_tools.deprecate_positional_args(supported_number=2) 

270 def subgraph(self, graph=None, 

271 name: typing.Optional[str] = None, 

272 comment: typing.Optional[str] = None, 

273 graph_attr=None, node_attr=None, edge_attr=None, 

274 body=None): 

275 """Add the current content of the given sole ``graph`` argument 

276 as subgraph or return a context manager 

277 returning a new graph instance 

278 created with the given (``name``, ``comment``, etc.) arguments 

279 whose content is added as subgraph 

280 when leaving the context manager's ``with``-block. 

281 

282 Args: 

283 graph: An instance of the same kind 

284 (:class:`.Graph`, :class:`.Digraph`) as the current graph 

285 (sole argument in non-with-block use). 

286 name: Subgraph name (``with``-block use). 

287 comment: Subgraph comment (``with``-block use). 

288 graph_attr: Subgraph-level attribute-value mapping 

289 (``with``-block use). 

290 node_attr: Node-level attribute-value mapping 

291 (``with``-block use). 

292 edge_attr: Edge-level attribute-value mapping 

293 (``with``-block use). 

294 body: Verbatim lines to add to the subgraph ``body`` 

295 (``with``-block use). 

296 

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

298 

299 When used as a context manager, the returned new graph instance 

300 uses ``strict=None`` and the parent graph's values 

301 for ``directory``, ``format``, ``engine``, and ``encoding`` by default. 

302 

303 Note: 

304 If the ``name`` of the subgraph begins with 

305 ``'cluster'`` (all lowercase) 

306 the layout engine will treat it as a special cluster subgraph. 

307 """ 

308 if graph is None: 

309 kwargs = self._copy_kwargs() 

310 kwargs.pop('filename', None) 

311 kwargs.update(name=name, comment=comment, 

312 graph_attr=graph_attr, node_attr=node_attr, edge_attr=edge_attr, 

313 body=body, strict=None) 

314 subgraph = self.__class__(**kwargs) 

315 

316 @contextlib.contextmanager 

317 def subgraph_contextmanager(*, parent): 

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

319 yield subgraph 

320 parent.subgraph(subgraph) 

321 

322 return subgraph_contextmanager(parent=self) 

323 

324 args = [name, comment, graph_attr, node_attr, edge_attr, body] 

325 if not all(a is None for a in args): 

326 raise ValueError('graph must be sole argument of subgraph()') 

327 

328 if graph.directed != self.directed: 

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

330 f' {graph!r}') 

331 

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