Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/readwrite/gml.py: 9%

421 statements  

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

1""" 

2Read graphs in GML format. 

3 

4"GML, the Graph Modelling Language, is our proposal for a portable 

5file format for graphs. GML's key features are portability, simple 

6syntax, extensibility and flexibility. A GML file consists of a 

7hierarchical key-value lists. Graphs can be annotated with arbitrary 

8data structures. The idea for a common file format was born at the 

9GD'95; this proposal is the outcome of many discussions. GML is the 

10standard file format in the Graphlet graph editor system. It has been 

11overtaken and adapted by several other systems for drawing graphs." 

12 

13GML files are stored using a 7-bit ASCII encoding with any extended 

14ASCII characters (iso8859-1) appearing as HTML character entities. 

15You will need to give some thought into how the exported data should 

16interact with different languages and even different Python versions. 

17Re-importing from gml is also a concern. 

18 

19Without specifying a `stringizer`/`destringizer`, the code is capable of 

20writing `int`/`float`/`str`/`dict`/`list` data as required by the GML  

21specification. For writing other data types, and for reading data other 

22than `str` you need to explicitly supply a `stringizer`/`destringizer`. 

23 

24For additional documentation on the GML file format, please see the 

25`GML website <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. 

26 

27Several example graphs in GML format may be found on Mark Newman's 

28`Network data page <http://www-personal.umich.edu/~mejn/netdata/>`_. 

29""" 

30import html.entities as htmlentitydefs 

31import re 

32import warnings 

33from ast import literal_eval 

34from collections import defaultdict 

35from enum import Enum 

36from io import StringIO 

37from typing import Any, NamedTuple 

38 

39import networkx as nx 

40from networkx.exception import NetworkXError 

41from networkx.utils import open_file 

42 

43__all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"] 

44 

45 

46def escape(text): 

47 """Use XML character references to escape characters. 

48 

49 Use XML character references for unprintable or non-ASCII 

50 characters, double quotes and ampersands in a string 

51 """ 

52 

53 def fixup(m): 

54 ch = m.group(0) 

55 return "&#" + str(ord(ch)) + ";" 

56 

57 text = re.sub('[^ -~]|[&"]', fixup, text) 

58 return text if isinstance(text, str) else str(text) 

59 

60 

61def unescape(text): 

62 """Replace XML character references with the referenced characters""" 

63 

64 def fixup(m): 

65 text = m.group(0) 

66 if text[1] == "#": 

67 # Character reference 

68 if text[2] == "x": 

69 code = int(text[3:-1], 16) 

70 else: 

71 code = int(text[2:-1]) 

72 else: 

73 # Named entity 

74 try: 

75 code = htmlentitydefs.name2codepoint[text[1:-1]] 

76 except KeyError: 

77 return text # leave unchanged 

78 try: 

79 return chr(code) 

80 except (ValueError, OverflowError): 

81 return text # leave unchanged 

82 

83 return re.sub("&(?:[0-9A-Za-z]+|#(?:[0-9]+|x[0-9A-Fa-f]+));", fixup, text) 

84 

85 

86def literal_destringizer(rep): 

87 """Convert a Python literal to the value it represents. 

88 

89 Parameters 

90 ---------- 

91 rep : string 

92 A Python literal. 

93 

94 Returns 

95 ------- 

96 value : object 

97 The value of the Python literal. 

98 

99 Raises 

100 ------ 

101 ValueError 

102 If `rep` is not a Python literal. 

103 """ 

104 if isinstance(rep, str): 

105 orig_rep = rep 

106 try: 

107 return literal_eval(rep) 

108 except SyntaxError as err: 

109 raise ValueError(f"{orig_rep!r} is not a valid Python literal") from err 

110 else: 

111 raise ValueError(f"{rep!r} is not a string") 

112 

113 

114@open_file(0, mode="rb") 

115@nx._dispatch(graphs=None) 

116def read_gml(path, label="label", destringizer=None): 

117 """Read graph in GML format from `path`. 

118 

119 Parameters 

120 ---------- 

121 path : filename or filehandle 

122 The filename or filehandle to read from. 

123 

124 label : string, optional 

125 If not None, the parsed nodes will be renamed according to node 

126 attributes indicated by `label`. Default value: 'label'. 

127 

128 destringizer : callable, optional 

129 A `destringizer` that recovers values stored as strings in GML. If it 

130 cannot convert a string to a value, a `ValueError` is raised. Default 

131 value : None. 

132 

133 Returns 

134 ------- 

135 G : NetworkX graph 

136 The parsed graph. 

137 

138 Raises 

139 ------ 

140 NetworkXError 

141 If the input cannot be parsed. 

142 

143 See Also 

144 -------- 

145 write_gml, parse_gml 

146 literal_destringizer 

147 

148 Notes 

149 ----- 

150 GML files are stored using a 7-bit ASCII encoding with any extended 

151 ASCII characters (iso8859-1) appearing as HTML character entities. 

152 Without specifying a `stringizer`/`destringizer`, the code is capable of 

153 writing `int`/`float`/`str`/`dict`/`list` data as required by the GML 

154 specification. For writing other data types, and for reading data other 

155 than `str` you need to explicitly supply a `stringizer`/`destringizer`. 

156 

157 For additional documentation on the GML file format, please see the 

158 `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. 

159 

160 See the module docstring :mod:`networkx.readwrite.gml` for more details. 

161 

162 Examples 

163 -------- 

164 >>> G = nx.path_graph(4) 

165 >>> nx.write_gml(G, "test.gml") 

166 

167 GML values are interpreted as strings by default: 

168 

169 >>> H = nx.read_gml("test.gml") 

170 >>> H.nodes 

171 NodeView(('0', '1', '2', '3')) 

172 

173 When a `destringizer` is provided, GML values are converted to the provided type. 

174 For example, integer nodes can be recovered as shown below: 

175 

176 >>> J = nx.read_gml("test.gml", destringizer=int) 

177 >>> J.nodes 

178 NodeView((0, 1, 2, 3)) 

179 

180 """ 

181 

182 def filter_lines(lines): 

183 for line in lines: 

184 try: 

185 line = line.decode("ascii") 

186 except UnicodeDecodeError as err: 

187 raise NetworkXError("input is not ASCII-encoded") from err 

188 if not isinstance(line, str): 

189 lines = str(lines) 

190 if line and line[-1] == "\n": 

191 line = line[:-1] 

192 yield line 

193 

194 G = parse_gml_lines(filter_lines(path), label, destringizer) 

195 return G 

196 

197 

198@nx._dispatch(graphs=None) 

199def parse_gml(lines, label="label", destringizer=None): 

200 """Parse GML graph from a string or iterable. 

201 

202 Parameters 

203 ---------- 

204 lines : string or iterable of strings 

205 Data in GML format. 

206 

207 label : string, optional 

208 If not None, the parsed nodes will be renamed according to node 

209 attributes indicated by `label`. Default value: 'label'. 

210 

211 destringizer : callable, optional 

212 A `destringizer` that recovers values stored as strings in GML. If it 

213 cannot convert a string to a value, a `ValueError` is raised. Default 

214 value : None. 

215 

216 Returns 

217 ------- 

218 G : NetworkX graph 

219 The parsed graph. 

220 

221 Raises 

222 ------ 

223 NetworkXError 

224 If the input cannot be parsed. 

225 

226 See Also 

227 -------- 

228 write_gml, read_gml 

229 

230 Notes 

231 ----- 

232 This stores nested GML attributes as dictionaries in the NetworkX graph, 

233 node, and edge attribute structures. 

234 

235 GML files are stored using a 7-bit ASCII encoding with any extended 

236 ASCII characters (iso8859-1) appearing as HTML character entities. 

237 Without specifying a `stringizer`/`destringizer`, the code is capable of 

238 writing `int`/`float`/`str`/`dict`/`list` data as required by the GML 

239 specification. For writing other data types, and for reading data other 

240 than `str` you need to explicitly supply a `stringizer`/`destringizer`. 

241 

242 For additional documentation on the GML file format, please see the 

243 `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. 

244 

245 See the module docstring :mod:`networkx.readwrite.gml` for more details. 

246 """ 

247 

248 def decode_line(line): 

249 if isinstance(line, bytes): 

250 try: 

251 line.decode("ascii") 

252 except UnicodeDecodeError as err: 

253 raise NetworkXError("input is not ASCII-encoded") from err 

254 if not isinstance(line, str): 

255 line = str(line) 

256 return line 

257 

258 def filter_lines(lines): 

259 if isinstance(lines, str): 

260 lines = decode_line(lines) 

261 lines = lines.splitlines() 

262 yield from lines 

263 else: 

264 for line in lines: 

265 line = decode_line(line) 

266 if line and line[-1] == "\n": 

267 line = line[:-1] 

268 if line.find("\n") != -1: 

269 raise NetworkXError("input line contains newline") 

270 yield line 

271 

272 G = parse_gml_lines(filter_lines(lines), label, destringizer) 

273 return G 

274 

275 

276class Pattern(Enum): 

277 """encodes the index of each token-matching pattern in `tokenize`.""" 

278 

279 KEYS = 0 

280 REALS = 1 

281 INTS = 2 

282 STRINGS = 3 

283 DICT_START = 4 

284 DICT_END = 5 

285 COMMENT_WHITESPACE = 6 

286 

287 

288class Token(NamedTuple): 

289 category: Pattern 

290 value: Any 

291 line: int 

292 position: int 

293 

294 

295LIST_START_VALUE = "_networkx_list_start" 

296 

297 

298def parse_gml_lines(lines, label, destringizer): 

299 """Parse GML `lines` into a graph.""" 

300 

301 def tokenize(): 

302 patterns = [ 

303 r"[A-Za-z][0-9A-Za-z_]*\b", # keys 

304 # reals 

305 r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*|INF)(?:[Ee][+-]?[0-9]+)?", 

306 r"[+-]?[0-9]+", # ints 

307 r'".*?"', # strings 

308 r"\[", # dict start 

309 r"\]", # dict end 

310 r"#.*$|\s+", # comments and whitespaces 

311 ] 

312 tokens = re.compile("|".join(f"({pattern})" for pattern in patterns)) 

313 lineno = 0 

314 multilines = [] # entries spread across multiple lines 

315 for line in lines: 

316 pos = 0 

317 

318 # deal with entries spread across multiple lines 

319 # 

320 # should we actually have to deal with escaped "s then do it here 

321 if multilines: 

322 multilines.append(line.strip()) 

323 if line[-1] == '"': # closing multiline entry 

324 # multiline entries will be joined by space. cannot 

325 # reintroduce newlines as this will break the tokenizer 

326 line = " ".join(multilines) 

327 multilines = [] 

328 else: # continued multiline entry 

329 lineno += 1 

330 continue 

331 else: 

332 if line.count('"') == 1: # opening multiline entry 

333 if line.strip()[0] != '"' and line.strip()[-1] != '"': 

334 # since we expect something like key "value", the " should not be found at ends 

335 # otherwise tokenizer will pick up the formatting mistake. 

336 multilines = [line.rstrip()] 

337 lineno += 1 

338 continue 

339 

340 length = len(line) 

341 

342 while pos < length: 

343 match = tokens.match(line, pos) 

344 if match is None: 

345 m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})" 

346 raise NetworkXError(m) 

347 for i in range(len(patterns)): 

348 group = match.group(i + 1) 

349 if group is not None: 

350 if i == 0: # keys 

351 value = group.rstrip() 

352 elif i == 1: # reals 

353 value = float(group) 

354 elif i == 2: # ints 

355 value = int(group) 

356 else: 

357 value = group 

358 if i != 6: # comments and whitespaces 

359 yield Token(Pattern(i), value, lineno + 1, pos + 1) 

360 pos += len(group) 

361 break 

362 lineno += 1 

363 yield Token(None, None, lineno + 1, 1) # EOF 

364 

365 def unexpected(curr_token, expected): 

366 category, value, lineno, pos = curr_token 

367 value = repr(value) if value is not None else "EOF" 

368 raise NetworkXError(f"expected {expected}, found {value} at ({lineno}, {pos})") 

369 

370 def consume(curr_token, category, expected): 

371 if curr_token.category == category: 

372 return next(tokens) 

373 unexpected(curr_token, expected) 

374 

375 def parse_kv(curr_token): 

376 dct = defaultdict(list) 

377 while curr_token.category == Pattern.KEYS: 

378 key = curr_token.value 

379 curr_token = next(tokens) 

380 category = curr_token.category 

381 if category == Pattern.REALS or category == Pattern.INTS: 

382 value = curr_token.value 

383 curr_token = next(tokens) 

384 elif category == Pattern.STRINGS: 

385 value = unescape(curr_token.value[1:-1]) 

386 if destringizer: 

387 try: 

388 value = destringizer(value) 

389 except ValueError: 

390 pass 

391 # Special handling for empty lists and tuples 

392 if value == "()": 

393 value = () 

394 if value == "[]": 

395 value = [] 

396 curr_token = next(tokens) 

397 elif category == Pattern.DICT_START: 

398 curr_token, value = parse_dict(curr_token) 

399 else: 

400 # Allow for string convertible id and label values 

401 if key in ("id", "label", "source", "target"): 

402 try: 

403 # String convert the token value 

404 value = unescape(str(curr_token.value)) 

405 if destringizer: 

406 try: 

407 value = destringizer(value) 

408 except ValueError: 

409 pass 

410 curr_token = next(tokens) 

411 except Exception: 

412 msg = ( 

413 "an int, float, string, '[' or string" 

414 + " convertible ASCII value for node id or label" 

415 ) 

416 unexpected(curr_token, msg) 

417 # Special handling for nan and infinity. Since the gml language 

418 # defines unquoted strings as keys, the numeric and string branches 

419 # are skipped and we end up in this special branch, so we need to 

420 # convert the current token value to a float for NAN and plain INF. 

421 # +/-INF are handled in the pattern for 'reals' in tokenize(). This 

422 # allows labels and values to be nan or infinity, but not keys. 

423 elif curr_token.value in {"NAN", "INF"}: 

424 value = float(curr_token.value) 

425 curr_token = next(tokens) 

426 else: # Otherwise error out 

427 unexpected(curr_token, "an int, float, string or '['") 

428 dct[key].append(value) 

429 

430 def clean_dict_value(value): 

431 if not isinstance(value, list): 

432 return value 

433 if len(value) == 1: 

434 return value[0] 

435 if value[0] == LIST_START_VALUE: 

436 return value[1:] 

437 return value 

438 

439 dct = {key: clean_dict_value(value) for key, value in dct.items()} 

440 return curr_token, dct 

441 

442 def parse_dict(curr_token): 

443 # dict start 

444 curr_token = consume(curr_token, Pattern.DICT_START, "'['") 

445 # dict contents 

446 curr_token, dct = parse_kv(curr_token) 

447 # dict end 

448 curr_token = consume(curr_token, Pattern.DICT_END, "']'") 

449 return curr_token, dct 

450 

451 def parse_graph(): 

452 curr_token, dct = parse_kv(next(tokens)) 

453 if curr_token.category is not None: # EOF 

454 unexpected(curr_token, "EOF") 

455 if "graph" not in dct: 

456 raise NetworkXError("input contains no graph") 

457 graph = dct["graph"] 

458 if isinstance(graph, list): 

459 raise NetworkXError("input contains more than one graph") 

460 return graph 

461 

462 tokens = tokenize() 

463 graph = parse_graph() 

464 

465 directed = graph.pop("directed", False) 

466 multigraph = graph.pop("multigraph", False) 

467 if not multigraph: 

468 G = nx.DiGraph() if directed else nx.Graph() 

469 else: 

470 G = nx.MultiDiGraph() if directed else nx.MultiGraph() 

471 graph_attr = {k: v for k, v in graph.items() if k not in ("node", "edge")} 

472 G.graph.update(graph_attr) 

473 

474 def pop_attr(dct, category, attr, i): 

475 try: 

476 return dct.pop(attr) 

477 except KeyError as err: 

478 raise NetworkXError(f"{category} #{i} has no {attr!r} attribute") from err 

479 

480 nodes = graph.get("node", []) 

481 mapping = {} 

482 node_labels = set() 

483 for i, node in enumerate(nodes if isinstance(nodes, list) else [nodes]): 

484 id = pop_attr(node, "node", "id", i) 

485 if id in G: 

486 raise NetworkXError(f"node id {id!r} is duplicated") 

487 if label is not None and label != "id": 

488 node_label = pop_attr(node, "node", label, i) 

489 if node_label in node_labels: 

490 raise NetworkXError(f"node label {node_label!r} is duplicated") 

491 node_labels.add(node_label) 

492 mapping[id] = node_label 

493 G.add_node(id, **node) 

494 

495 edges = graph.get("edge", []) 

496 for i, edge in enumerate(edges if isinstance(edges, list) else [edges]): 

497 source = pop_attr(edge, "edge", "source", i) 

498 target = pop_attr(edge, "edge", "target", i) 

499 if source not in G: 

500 raise NetworkXError(f"edge #{i} has undefined source {source!r}") 

501 if target not in G: 

502 raise NetworkXError(f"edge #{i} has undefined target {target!r}") 

503 if not multigraph: 

504 if not G.has_edge(source, target): 

505 G.add_edge(source, target, **edge) 

506 else: 

507 arrow = "->" if directed else "--" 

508 msg = f"edge #{i} ({source!r}{arrow}{target!r}) is duplicated" 

509 raise nx.NetworkXError(msg) 

510 else: 

511 key = edge.pop("key", None) 

512 if key is not None and G.has_edge(source, target, key): 

513 arrow = "->" if directed else "--" 

514 msg = f"edge #{i} ({source!r}{arrow}{target!r}, {key!r})" 

515 msg2 = 'Hint: If multigraph add "multigraph 1" to file header.' 

516 raise nx.NetworkXError(msg + " is duplicated\n" + msg2) 

517 G.add_edge(source, target, key, **edge) 

518 

519 if label is not None and label != "id": 

520 G = nx.relabel_nodes(G, mapping) 

521 return G 

522 

523 

524def literal_stringizer(value): 

525 """Convert a `value` to a Python literal in GML representation. 

526 

527 Parameters 

528 ---------- 

529 value : object 

530 The `value` to be converted to GML representation. 

531 

532 Returns 

533 ------- 

534 rep : string 

535 A double-quoted Python literal representing value. Unprintable 

536 characters are replaced by XML character references. 

537 

538 Raises 

539 ------ 

540 ValueError 

541 If `value` cannot be converted to GML. 

542 

543 Notes 

544 ----- 

545 The original value can be recovered using the 

546 :func:`networkx.readwrite.gml.literal_destringizer` function. 

547 """ 

548 

549 def stringize(value): 

550 if isinstance(value, (int, bool)) or value is None: 

551 if value is True: # GML uses 1/0 for boolean values. 

552 buf.write(str(1)) 

553 elif value is False: 

554 buf.write(str(0)) 

555 else: 

556 buf.write(str(value)) 

557 elif isinstance(value, str): 

558 text = repr(value) 

559 if text[0] != "u": 

560 try: 

561 value.encode("latin1") 

562 except UnicodeEncodeError: 

563 text = "u" + text 

564 buf.write(text) 

565 elif isinstance(value, (float, complex, str, bytes)): 

566 buf.write(repr(value)) 

567 elif isinstance(value, list): 

568 buf.write("[") 

569 first = True 

570 for item in value: 

571 if not first: 

572 buf.write(",") 

573 else: 

574 first = False 

575 stringize(item) 

576 buf.write("]") 

577 elif isinstance(value, tuple): 

578 if len(value) > 1: 

579 buf.write("(") 

580 first = True 

581 for item in value: 

582 if not first: 

583 buf.write(",") 

584 else: 

585 first = False 

586 stringize(item) 

587 buf.write(")") 

588 elif value: 

589 buf.write("(") 

590 stringize(value[0]) 

591 buf.write(",)") 

592 else: 

593 buf.write("()") 

594 elif isinstance(value, dict): 

595 buf.write("{") 

596 first = True 

597 for key, value in value.items(): 

598 if not first: 

599 buf.write(",") 

600 else: 

601 first = False 

602 stringize(key) 

603 buf.write(":") 

604 stringize(value) 

605 buf.write("}") 

606 elif isinstance(value, set): 

607 buf.write("{") 

608 first = True 

609 for item in value: 

610 if not first: 

611 buf.write(",") 

612 else: 

613 first = False 

614 stringize(item) 

615 buf.write("}") 

616 else: 

617 msg = f"{value!r} cannot be converted into a Python literal" 

618 raise ValueError(msg) 

619 

620 buf = StringIO() 

621 stringize(value) 

622 return buf.getvalue() 

623 

624 

625def generate_gml(G, stringizer=None): 

626 r"""Generate a single entry of the graph `G` in GML format. 

627 

628 Parameters 

629 ---------- 

630 G : NetworkX graph 

631 The graph to be converted to GML. 

632 

633 stringizer : callable, optional 

634 A `stringizer` which converts non-int/non-float/non-dict values into 

635 strings. If it cannot convert a value into a string, it should raise a 

636 `ValueError` to indicate that. Default value: None. 

637 

638 Returns 

639 ------- 

640 lines: generator of strings 

641 Lines of GML data. Newlines are not appended. 

642 

643 Raises 

644 ------ 

645 NetworkXError 

646 If `stringizer` cannot convert a value into a string, or the value to 

647 convert is not a string while `stringizer` is None. 

648 

649 See Also 

650 -------- 

651 literal_stringizer 

652 

653 Notes 

654 ----- 

655 Graph attributes named 'directed', 'multigraph', 'node' or 

656 'edge', node attributes named 'id' or 'label', edge attributes 

657 named 'source' or 'target' (or 'key' if `G` is a multigraph) 

658 are ignored because these attribute names are used to encode the graph 

659 structure. 

660 

661 GML files are stored using a 7-bit ASCII encoding with any extended 

662 ASCII characters (iso8859-1) appearing as HTML character entities. 

663 Without specifying a `stringizer`/`destringizer`, the code is capable of 

664 writing `int`/`float`/`str`/`dict`/`list` data as required by the GML 

665 specification. For writing other data types, and for reading data other 

666 than `str` you need to explicitly supply a `stringizer`/`destringizer`. 

667 

668 For additional documentation on the GML file format, please see the 

669 `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. 

670 

671 See the module docstring :mod:`networkx.readwrite.gml` for more details. 

672 

673 Examples 

674 -------- 

675 >>> G = nx.Graph() 

676 >>> G.add_node("1") 

677 >>> print("\n".join(nx.generate_gml(G))) 

678 graph [ 

679 node [ 

680 id 0 

681 label "1" 

682 ] 

683 ] 

684 >>> G = nx.MultiGraph([("a", "b"), ("a", "b")]) 

685 >>> print("\n".join(nx.generate_gml(G))) 

686 graph [ 

687 multigraph 1 

688 node [ 

689 id 0 

690 label "a" 

691 ] 

692 node [ 

693 id 1 

694 label "b" 

695 ] 

696 edge [ 

697 source 0 

698 target 1 

699 key 0 

700 ] 

701 edge [ 

702 source 0 

703 target 1 

704 key 1 

705 ] 

706 ] 

707 """ 

708 valid_keys = re.compile("^[A-Za-z][0-9A-Za-z_]*$") 

709 

710 def stringize(key, value, ignored_keys, indent, in_list=False): 

711 if not isinstance(key, str): 

712 raise NetworkXError(f"{key!r} is not a string") 

713 if not valid_keys.match(key): 

714 raise NetworkXError(f"{key!r} is not a valid key") 

715 if not isinstance(key, str): 

716 key = str(key) 

717 if key not in ignored_keys: 

718 if isinstance(value, (int, bool)): 

719 if key == "label": 

720 yield indent + key + ' "' + str(value) + '"' 

721 elif value is True: 

722 # python bool is an instance of int 

723 yield indent + key + " 1" 

724 elif value is False: 

725 yield indent + key + " 0" 

726 # GML only supports signed 32-bit integers 

727 elif value < -(2**31) or value >= 2**31: 

728 yield indent + key + ' "' + str(value) + '"' 

729 else: 

730 yield indent + key + " " + str(value) 

731 elif isinstance(value, float): 

732 text = repr(value).upper() 

733 # GML matches INF to keys, so prepend + to INF. Use repr(float(*)) 

734 # instead of string literal to future proof against changes to repr. 

735 if text == repr(float("inf")).upper(): 

736 text = "+" + text 

737 else: 

738 # GML requires that a real literal contain a decimal point, but 

739 # repr may not output a decimal point when the mantissa is 

740 # integral and hence needs fixing. 

741 epos = text.rfind("E") 

742 if epos != -1 and text.find(".", 0, epos) == -1: 

743 text = text[:epos] + "." + text[epos:] 

744 if key == "label": 

745 yield indent + key + ' "' + text + '"' 

746 else: 

747 yield indent + key + " " + text 

748 elif isinstance(value, dict): 

749 yield indent + key + " [" 

750 next_indent = indent + " " 

751 for key, value in value.items(): 

752 yield from stringize(key, value, (), next_indent) 

753 yield indent + "]" 

754 elif isinstance(value, tuple) and key == "label": 

755 yield indent + key + f" \"({','.join(repr(v) for v in value)})\"" 

756 elif isinstance(value, (list, tuple)) and key != "label" and not in_list: 

757 if len(value) == 0: 

758 yield indent + key + " " + f'"{value!r}"' 

759 if len(value) == 1: 

760 yield indent + key + " " + f'"{LIST_START_VALUE}"' 

761 for val in value: 

762 yield from stringize(key, val, (), indent, True) 

763 else: 

764 if stringizer: 

765 try: 

766 value = stringizer(value) 

767 except ValueError as err: 

768 raise NetworkXError( 

769 f"{value!r} cannot be converted into a string" 

770 ) from err 

771 if not isinstance(value, str): 

772 raise NetworkXError(f"{value!r} is not a string") 

773 yield indent + key + ' "' + escape(value) + '"' 

774 

775 multigraph = G.is_multigraph() 

776 yield "graph [" 

777 

778 # Output graph attributes 

779 if G.is_directed(): 

780 yield " directed 1" 

781 if multigraph: 

782 yield " multigraph 1" 

783 ignored_keys = {"directed", "multigraph", "node", "edge"} 

784 for attr, value in G.graph.items(): 

785 yield from stringize(attr, value, ignored_keys, " ") 

786 

787 # Output node data 

788 node_id = dict(zip(G, range(len(G)))) 

789 ignored_keys = {"id", "label"} 

790 for node, attrs in G.nodes.items(): 

791 yield " node [" 

792 yield " id " + str(node_id[node]) 

793 yield from stringize("label", node, (), " ") 

794 for attr, value in attrs.items(): 

795 yield from stringize(attr, value, ignored_keys, " ") 

796 yield " ]" 

797 

798 # Output edge data 

799 ignored_keys = {"source", "target"} 

800 kwargs = {"data": True} 

801 if multigraph: 

802 ignored_keys.add("key") 

803 kwargs["keys"] = True 

804 for e in G.edges(**kwargs): 

805 yield " edge [" 

806 yield " source " + str(node_id[e[0]]) 

807 yield " target " + str(node_id[e[1]]) 

808 if multigraph: 

809 yield from stringize("key", e[2], (), " ") 

810 for attr, value in e[-1].items(): 

811 yield from stringize(attr, value, ignored_keys, " ") 

812 yield " ]" 

813 yield "]" 

814 

815 

816@open_file(1, mode="wb") 

817def write_gml(G, path, stringizer=None): 

818 """Write a graph `G` in GML format to the file or file handle `path`. 

819 

820 Parameters 

821 ---------- 

822 G : NetworkX graph 

823 The graph to be converted to GML. 

824 

825 path : filename or filehandle 

826 The filename or filehandle to write. Files whose names end with .gz or 

827 .bz2 will be compressed. 

828 

829 stringizer : callable, optional 

830 A `stringizer` which converts non-int/non-float/non-dict values into 

831 strings. If it cannot convert a value into a string, it should raise a 

832 `ValueError` to indicate that. Default value: None. 

833 

834 Raises 

835 ------ 

836 NetworkXError 

837 If `stringizer` cannot convert a value into a string, or the value to 

838 convert is not a string while `stringizer` is None. 

839 

840 See Also 

841 -------- 

842 read_gml, generate_gml 

843 literal_stringizer 

844 

845 Notes 

846 ----- 

847 Graph attributes named 'directed', 'multigraph', 'node' or 

848 'edge', node attributes named 'id' or 'label', edge attributes 

849 named 'source' or 'target' (or 'key' if `G` is a multigraph) 

850 are ignored because these attribute names are used to encode the graph 

851 structure. 

852 

853 GML files are stored using a 7-bit ASCII encoding with any extended 

854 ASCII characters (iso8859-1) appearing as HTML character entities. 

855 Without specifying a `stringizer`/`destringizer`, the code is capable of 

856 writing `int`/`float`/`str`/`dict`/`list` data as required by the GML 

857 specification. For writing other data types, and for reading data other 

858 than `str` you need to explicitly supply a `stringizer`/`destringizer`. 

859 

860 Note that while we allow non-standard GML to be read from a file, we make 

861 sure to write GML format. In particular, underscores are not allowed in 

862 attribute names. 

863 For additional documentation on the GML file format, please see the 

864 `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_. 

865 

866 See the module docstring :mod:`networkx.readwrite.gml` for more details. 

867 

868 Examples 

869 -------- 

870 >>> G = nx.path_graph(4) 

871 >>> nx.write_gml(G, "test.gml") 

872 

873 Filenames ending in .gz or .bz2 will be compressed. 

874 

875 >>> nx.write_gml(G, "test.gml.gz") 

876 """ 

877 for line in generate_gml(G, stringizer): 

878 path.write((line + "\n").encode("ascii"))