Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/drawing/nx_pydot.py: 10%

159 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

1""" 

2***** 

3Pydot 

4***** 

5 

6Import and export NetworkX graphs in Graphviz dot format using pydot. 

7 

8Either this module or nx_agraph can be used to interface with graphviz. 

9 

10Examples 

11-------- 

12>>> G = nx.complete_graph(5) 

13>>> PG = nx.nx_pydot.to_pydot(G) 

14>>> H = nx.nx_pydot.from_pydot(PG) 

15 

16See Also 

17-------- 

18 - pydot: https://github.com/erocarrera/pydot 

19 - Graphviz: https://www.graphviz.org 

20 - DOT Language: http://www.graphviz.org/doc/info/lang.html 

21""" 

22import warnings 

23from locale import getpreferredencoding 

24 

25import networkx as nx 

26from networkx.utils import open_file 

27 

28__all__ = [ 

29 "write_dot", 

30 "read_dot", 

31 "graphviz_layout", 

32 "pydot_layout", 

33 "to_pydot", 

34 "from_pydot", 

35] 

36 

37 

38@open_file(1, mode="w") 

39def write_dot(G, path): 

40 """Write NetworkX graph G to Graphviz dot format on path. 

41 

42 Path can be a string or a file handle. 

43 """ 

44 msg = ( 

45 "nx.nx_pydot.write_dot depends on the pydot package, which has " 

46 "known issues and is not actively maintained. Consider using " 

47 "nx.nx_agraph.write_dot instead.\n\n" 

48 "See https://github.com/networkx/networkx/issues/5723" 

49 ) 

50 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

51 P = to_pydot(G) 

52 path.write(P.to_string()) 

53 return 

54 

55 

56@open_file(0, mode="r") 

57@nx._dispatch(name="pydot_read_dot", graphs=None) 

58def read_dot(path): 

59 """Returns a NetworkX :class:`MultiGraph` or :class:`MultiDiGraph` from the 

60 dot file with the passed path. 

61 

62 If this file contains multiple graphs, only the first such graph is 

63 returned. All graphs _except_ the first are silently ignored. 

64 

65 Parameters 

66 ---------- 

67 path : str or file 

68 Filename or file handle. 

69 

70 Returns 

71 ------- 

72 G : MultiGraph or MultiDiGraph 

73 A :class:`MultiGraph` or :class:`MultiDiGraph`. 

74 

75 Notes 

76 ----- 

77 Use `G = nx.Graph(nx.nx_pydot.read_dot(path))` to return a :class:`Graph` instead of a 

78 :class:`MultiGraph`. 

79 """ 

80 import pydot 

81 

82 msg = ( 

83 "nx.nx_pydot.read_dot depends on the pydot package, which has " 

84 "known issues and is not actively maintained. Consider using " 

85 "nx.nx_agraph.read_dot instead.\n\n" 

86 "See https://github.com/networkx/networkx/issues/5723" 

87 ) 

88 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

89 

90 data = path.read() 

91 

92 # List of one or more "pydot.Dot" instances deserialized from this file. 

93 P_list = pydot.graph_from_dot_data(data) 

94 

95 # Convert only the first such instance into a NetworkX graph. 

96 return from_pydot(P_list[0]) 

97 

98 

99@nx._dispatch(graphs=None) 

100def from_pydot(P): 

101 """Returns a NetworkX graph from a Pydot graph. 

102 

103 Parameters 

104 ---------- 

105 P : Pydot graph 

106 A graph created with Pydot 

107 

108 Returns 

109 ------- 

110 G : NetworkX multigraph 

111 A MultiGraph or MultiDiGraph. 

112 

113 Examples 

114 -------- 

115 >>> K5 = nx.complete_graph(5) 

116 >>> A = nx.nx_pydot.to_pydot(K5) 

117 >>> G = nx.nx_pydot.from_pydot(A) # return MultiGraph 

118 

119 # make a Graph instead of MultiGraph 

120 >>> G = nx.Graph(nx.nx_pydot.from_pydot(A)) 

121 

122 """ 

123 msg = ( 

124 "nx.nx_pydot.from_pydot depends on the pydot package, which has " 

125 "known issues and is not actively maintained.\n\n" 

126 "See https://github.com/networkx/networkx/issues/5723" 

127 ) 

128 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

129 

130 if P.get_strict(None): # pydot bug: get_strict() shouldn't take argument 

131 multiedges = False 

132 else: 

133 multiedges = True 

134 

135 if P.get_type() == "graph": # undirected 

136 if multiedges: 

137 N = nx.MultiGraph() 

138 else: 

139 N = nx.Graph() 

140 else: 

141 if multiedges: 

142 N = nx.MultiDiGraph() 

143 else: 

144 N = nx.DiGraph() 

145 

146 # assign defaults 

147 name = P.get_name().strip('"') 

148 if name != "": 

149 N.name = name 

150 

151 # add nodes, attributes to N.node_attr 

152 for p in P.get_node_list(): 

153 n = p.get_name().strip('"') 

154 if n in ("node", "graph", "edge"): 

155 continue 

156 N.add_node(n, **p.get_attributes()) 

157 

158 # add edges 

159 for e in P.get_edge_list(): 

160 u = e.get_source() 

161 v = e.get_destination() 

162 attr = e.get_attributes() 

163 s = [] 

164 d = [] 

165 

166 if isinstance(u, str): 

167 s.append(u.strip('"')) 

168 else: 

169 for unodes in u["nodes"]: 

170 s.append(unodes.strip('"')) 

171 

172 if isinstance(v, str): 

173 d.append(v.strip('"')) 

174 else: 

175 for vnodes in v["nodes"]: 

176 d.append(vnodes.strip('"')) 

177 

178 for source_node in s: 

179 for destination_node in d: 

180 N.add_edge(source_node, destination_node, **attr) 

181 

182 # add default attributes for graph, nodes, edges 

183 pattr = P.get_attributes() 

184 if pattr: 

185 N.graph["graph"] = pattr 

186 try: 

187 N.graph["node"] = P.get_node_defaults()[0] 

188 except (IndexError, TypeError): 

189 pass # N.graph['node']={} 

190 try: 

191 N.graph["edge"] = P.get_edge_defaults()[0] 

192 except (IndexError, TypeError): 

193 pass # N.graph['edge']={} 

194 return N 

195 

196 

197def _check_colon_quotes(s): 

198 # A quick helper function to check if a string has a colon in it 

199 # and if it is quoted properly with double quotes. 

200 # refer https://github.com/pydot/pydot/issues/258 

201 return ":" in s and (s[0] != '"' or s[-1] != '"') 

202 

203 

204def to_pydot(N): 

205 """Returns a pydot graph from a NetworkX graph N. 

206 

207 Parameters 

208 ---------- 

209 N : NetworkX graph 

210 A graph created with NetworkX 

211 

212 Examples 

213 -------- 

214 >>> K5 = nx.complete_graph(5) 

215 >>> P = nx.nx_pydot.to_pydot(K5) 

216 

217 Notes 

218 ----- 

219 

220 """ 

221 import pydot 

222 

223 msg = ( 

224 "nx.nx_pydot.to_pydot depends on the pydot package, which has " 

225 "known issues and is not actively maintained.\n\n" 

226 "See https://github.com/networkx/networkx/issues/5723" 

227 ) 

228 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

229 

230 # set Graphviz graph type 

231 if N.is_directed(): 

232 graph_type = "digraph" 

233 else: 

234 graph_type = "graph" 

235 strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph() 

236 

237 name = N.name 

238 graph_defaults = N.graph.get("graph", {}) 

239 if name == "": 

240 P = pydot.Dot("", graph_type=graph_type, strict=strict, **graph_defaults) 

241 else: 

242 P = pydot.Dot( 

243 f'"{name}"', graph_type=graph_type, strict=strict, **graph_defaults 

244 ) 

245 try: 

246 P.set_node_defaults(**N.graph["node"]) 

247 except KeyError: 

248 pass 

249 try: 

250 P.set_edge_defaults(**N.graph["edge"]) 

251 except KeyError: 

252 pass 

253 

254 for n, nodedata in N.nodes(data=True): 

255 str_nodedata = {str(k): str(v) for k, v in nodedata.items()} 

256 # Explicitly catch nodes with ":" in node names or nodedata. 

257 n = str(n) 

258 raise_error = _check_colon_quotes(n) or ( 

259 any( 

260 (_check_colon_quotes(k) or _check_colon_quotes(v)) 

261 for k, v in str_nodedata.items() 

262 ) 

263 ) 

264 if raise_error: 

265 raise ValueError( 

266 f'Node names and attributes should not contain ":" unless they are quoted with "".\ 

267 For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ 

268 Please refer https://github.com/pydot/pydot/issues/258' 

269 ) 

270 p = pydot.Node(n, **str_nodedata) 

271 P.add_node(p) 

272 

273 if N.is_multigraph(): 

274 for u, v, key, edgedata in N.edges(data=True, keys=True): 

275 str_edgedata = {str(k): str(v) for k, v in edgedata.items() if k != "key"} 

276 u, v = str(u), str(v) 

277 raise_error = ( 

278 _check_colon_quotes(u) 

279 or _check_colon_quotes(v) 

280 or ( 

281 any( 

282 (_check_colon_quotes(k) or _check_colon_quotes(val)) 

283 for k, val in str_edgedata.items() 

284 ) 

285 ) 

286 ) 

287 if raise_error: 

288 raise ValueError( 

289 f'Node names and attributes should not contain ":" unless they are quoted with "".\ 

290 For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ 

291 Please refer https://github.com/pydot/pydot/issues/258' 

292 ) 

293 edge = pydot.Edge(u, v, key=str(key), **str_edgedata) 

294 P.add_edge(edge) 

295 

296 else: 

297 for u, v, edgedata in N.edges(data=True): 

298 str_edgedata = {str(k): str(v) for k, v in edgedata.items()} 

299 u, v = str(u), str(v) 

300 raise_error = ( 

301 _check_colon_quotes(u) 

302 or _check_colon_quotes(v) 

303 or ( 

304 any( 

305 (_check_colon_quotes(k) or _check_colon_quotes(val)) 

306 for k, val in str_edgedata.items() 

307 ) 

308 ) 

309 ) 

310 if raise_error: 

311 raise ValueError( 

312 f'Node names and attributes should not contain ":" unless they are quoted with "".\ 

313 For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ 

314 Please refer https://github.com/pydot/pydot/issues/258' 

315 ) 

316 edge = pydot.Edge(u, v, **str_edgedata) 

317 P.add_edge(edge) 

318 return P 

319 

320 

321def graphviz_layout(G, prog="neato", root=None): 

322 """Create node positions using Pydot and Graphviz. 

323 

324 Returns a dictionary of positions keyed by node. 

325 

326 Parameters 

327 ---------- 

328 G : NetworkX Graph 

329 The graph for which the layout is computed. 

330 prog : string (default: 'neato') 

331 The name of the GraphViz program to use for layout. 

332 Options depend on GraphViz version but may include: 

333 'dot', 'twopi', 'fdp', 'sfdp', 'circo' 

334 root : Node from G or None (default: None) 

335 The node of G from which to start some layout algorithms. 

336 

337 Returns 

338 ------- 

339 Dictionary of (x, y) positions keyed by node. 

340 

341 Examples 

342 -------- 

343 >>> G = nx.complete_graph(4) 

344 >>> pos = nx.nx_pydot.graphviz_layout(G) 

345 >>> pos = nx.nx_pydot.graphviz_layout(G, prog="dot") 

346 

347 Notes 

348 ----- 

349 This is a wrapper for pydot_layout. 

350 """ 

351 msg = ( 

352 "nx.nx_pydot.graphviz_layout depends on the pydot package, which has " 

353 "known issues and is not actively maintained. Consider using " 

354 "nx.nx_agraph.graphviz_layout instead.\n\n" 

355 "See https://github.com/networkx/networkx/issues/5723" 

356 ) 

357 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

358 

359 return pydot_layout(G=G, prog=prog, root=root) 

360 

361 

362def pydot_layout(G, prog="neato", root=None): 

363 """Create node positions using :mod:`pydot` and Graphviz. 

364 

365 Parameters 

366 ---------- 

367 G : Graph 

368 NetworkX graph to be laid out. 

369 prog : string (default: 'neato') 

370 Name of the GraphViz command to use for layout. 

371 Options depend on GraphViz version but may include: 

372 'dot', 'twopi', 'fdp', 'sfdp', 'circo' 

373 root : Node from G or None (default: None) 

374 The node of G from which to start some layout algorithms. 

375 

376 Returns 

377 ------- 

378 dict 

379 Dictionary of positions keyed by node. 

380 

381 Examples 

382 -------- 

383 >>> G = nx.complete_graph(4) 

384 >>> pos = nx.nx_pydot.pydot_layout(G) 

385 >>> pos = nx.nx_pydot.pydot_layout(G, prog="dot") 

386 

387 Notes 

388 ----- 

389 If you use complex node objects, they may have the same string 

390 representation and GraphViz could treat them as the same node. 

391 The layout may assign both nodes a single location. See Issue #1568 

392 If this occurs in your case, consider relabeling the nodes just 

393 for the layout computation using something similar to:: 

394 

395 H = nx.convert_node_labels_to_integers(G, label_attribute='node_label') 

396 H_layout = nx.nx_pydot.pydot_layout(G, prog='dot') 

397 G_layout = {H.nodes[n]['node_label']: p for n, p in H_layout.items()} 

398 

399 """ 

400 import pydot 

401 

402 msg = ( 

403 "nx.nx_pydot.pydot_layout depends on the pydot package, which has " 

404 "known issues and is not actively maintained.\n\n" 

405 "See https://github.com/networkx/networkx/issues/5723" 

406 ) 

407 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

408 P = to_pydot(G) 

409 if root is not None: 

410 P.set("root", str(root)) 

411 

412 # List of low-level bytes comprising a string in the dot language converted 

413 # from the passed graph with the passed external GraphViz command. 

414 D_bytes = P.create_dot(prog=prog) 

415 

416 # Unique string decoded from these bytes with the preferred locale encoding 

417 D = str(D_bytes, encoding=getpreferredencoding()) 

418 

419 if D == "": # no data returned 

420 print(f"Graphviz layout with {prog} failed") 

421 print() 

422 print("To debug what happened try:") 

423 print("P = nx.nx_pydot.to_pydot(G)") 

424 print('P.write_dot("file.dot")') 

425 print(f"And then run {prog} on file.dot") 

426 return 

427 

428 # List of one or more "pydot.Dot" instances deserialized from this string. 

429 Q_list = pydot.graph_from_dot_data(D) 

430 assert len(Q_list) == 1 

431 

432 # The first and only such instance, as guaranteed by the above assertion. 

433 Q = Q_list[0] 

434 

435 node_pos = {} 

436 for n in G.nodes(): 

437 str_n = str(n) 

438 # Explicitly catch nodes with ":" in node names or nodedata. 

439 if _check_colon_quotes(str_n): 

440 raise ValueError( 

441 f'Node names and node attributes should not contain ":" unless they are quoted with "".\ 

442 For example the string \'attribute:data1\' should be written as \'"attribute:data1"\'.\ 

443 Please refer https://github.com/pydot/pydot/issues/258' 

444 ) 

445 pydot_node = pydot.Node(str_n).get_name() 

446 node = Q.get_node(pydot_node) 

447 

448 if isinstance(node, list): 

449 node = node[0] 

450 pos = node.get_pos()[1:-1] # strip leading and trailing double quotes 

451 if pos is not None: 

452 xx, yy = pos.split(",") 

453 node_pos[n] = (float(xx), float(yy)) 

454 return node_pos