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

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

421 statements  

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

30 

31import html.entities as htmlentitydefs 

32import re 

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._dispatchable(graphs=None, returns_graph=True) 

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

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

118 

119 Parameters 

120 ---------- 

121 path : file or string 

122 Filename or file handle to read. 

123 Filenames ending in .gz or .bz2 will be decompressed. 

124 

125 label : string, optional 

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

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

128 

129 destringizer : callable, optional 

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

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

132 value : None. 

133 

134 Returns 

135 ------- 

136 G : NetworkX graph 

137 The parsed graph. 

138 

139 Raises 

140 ------ 

141 NetworkXError 

142 If the input cannot be parsed. 

143 

144 See Also 

145 -------- 

146 write_gml, parse_gml 

147 literal_destringizer 

148 

149 Notes 

150 ----- 

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

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

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

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

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

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

157 

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

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

160 

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

162 

163 Examples 

164 -------- 

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

166 >>> nx.write_gml(G, "test_path4.gml") 

167 

168 GML values are interpreted as strings by default: 

169 

170 >>> H = nx.read_gml("test_path4.gml") 

171 >>> H.nodes 

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

173 

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

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

176 

177 >>> J = nx.read_gml("test_path4.gml", destringizer=int) 

178 >>> J.nodes 

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

180 

181 """ 

182 

183 def filter_lines(lines): 

184 for line in lines: 

185 try: 

186 line = line.decode("ascii") 

187 except UnicodeDecodeError as err: 

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

189 if not isinstance(line, str): 

190 lines = str(lines) 

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

192 line = line[:-1] 

193 yield line 

194 

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

196 return G 

197 

198 

199@nx._dispatchable(graphs=None, returns_graph=True) 

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

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

202 

203 Parameters 

204 ---------- 

205 lines : string or iterable of strings 

206 Data in GML format. 

207 

208 label : string, optional 

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

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

211 

212 destringizer : callable, optional 

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

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

215 value : None. 

216 

217 Returns 

218 ------- 

219 G : NetworkX graph 

220 The parsed graph. 

221 

222 Raises 

223 ------ 

224 NetworkXError 

225 If the input cannot be parsed. 

226 

227 See Also 

228 -------- 

229 write_gml, read_gml 

230 

231 Notes 

232 ----- 

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

234 node, and edge attribute structures. 

235 

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

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

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

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

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

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

242 

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

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

245 

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

247 """ 

248 

249 def decode_line(line): 

250 if isinstance(line, bytes): 

251 try: 

252 line.decode("ascii") 

253 except UnicodeDecodeError as err: 

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

255 if not isinstance(line, str): 

256 line = str(line) 

257 return line 

258 

259 def filter_lines(lines): 

260 if isinstance(lines, str): 

261 lines = decode_line(lines) 

262 lines = lines.splitlines() 

263 yield from lines 

264 else: 

265 for line in lines: 

266 line = decode_line(line) 

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

268 line = line[:-1] 

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

270 raise NetworkXError("input line contains newline") 

271 yield line 

272 

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

274 return G 

275 

276 

277class Pattern(Enum): 

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

279 

280 KEYS = 0 

281 REALS = 1 

282 INTS = 2 

283 STRINGS = 3 

284 DICT_START = 4 

285 DICT_END = 5 

286 COMMENT_WHITESPACE = 6 

287 

288 

289class Token(NamedTuple): 

290 category: Pattern 

291 value: Any 

292 line: int 

293 position: int 

294 

295 

296LIST_START_VALUE = "_networkx_list_start" 

297 

298 

299def parse_gml_lines(lines, label, destringizer): 

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

301 

302 def tokenize(): 

303 patterns = [ 

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

305 # reals 

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

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

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

309 r"\[", # dict start 

310 r"\]", # dict end 

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

312 ] 

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

314 lineno = 0 

315 multilines = [] # entries spread across multiple lines 

316 for line in lines: 

317 pos = 0 

318 

319 # deal with entries spread across multiple lines 

320 # 

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

322 if multilines: 

323 multilines.append(line.strip()) 

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

325 # multiline entries will be joined by space. cannot 

326 # reintroduce newlines as this will break the tokenizer 

327 line = " ".join(multilines) 

328 multilines = [] 

329 else: # continued multiline entry 

330 lineno += 1 

331 continue 

332 else: 

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

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

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

336 # otherwise tokenizer will pick up the formatting mistake. 

337 multilines = [line.rstrip()] 

338 lineno += 1 

339 continue 

340 

341 length = len(line) 

342 

343 while pos < length: 

344 match = tokens.match(line, pos) 

345 if match is None: 

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

347 raise NetworkXError(m) 

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

349 group = match.group(i + 1) 

350 if group is not None: 

351 if i == 0: # keys 

352 value = group.rstrip() 

353 elif i == 1: # reals 

354 value = float(group) 

355 elif i == 2: # ints 

356 value = int(group) 

357 else: 

358 value = group 

359 if i != 6: # comments and whitespaces 

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

361 pos += len(group) 

362 break 

363 lineno += 1 

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

365 

366 def unexpected(curr_token, expected): 

367 category, value, lineno, pos = curr_token 

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

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

370 

371 def consume(curr_token, category, expected): 

372 if curr_token.category == category: 

373 return next(tokens) 

374 unexpected(curr_token, expected) 

375 

376 def parse_kv(curr_token): 

377 dct = defaultdict(list) 

378 while curr_token.category == Pattern.KEYS: 

379 key = curr_token.value 

380 curr_token = next(tokens) 

381 category = curr_token.category 

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

383 value = curr_token.value 

384 curr_token = next(tokens) 

385 elif category == Pattern.STRINGS: 

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

387 if destringizer: 

388 try: 

389 value = destringizer(value) 

390 except ValueError: 

391 pass 

392 # Special handling for empty lists and tuples 

393 if value == "()": 

394 value = () 

395 if value == "[]": 

396 value = [] 

397 curr_token = next(tokens) 

398 elif category == Pattern.DICT_START: 

399 curr_token, value = parse_dict(curr_token) 

400 else: 

401 # Allow for string convertible id and label values 

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

403 try: 

404 # String convert the token value 

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

406 if destringizer: 

407 try: 

408 value = destringizer(value) 

409 except ValueError: 

410 pass 

411 curr_token = next(tokens) 

412 except Exception: 

413 msg = ( 

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

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

416 ) 

417 unexpected(curr_token, msg) 

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

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

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

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

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

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

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

425 value = float(curr_token.value) 

426 curr_token = next(tokens) 

427 else: # Otherwise error out 

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

429 dct[key].append(value) 

430 

431 def clean_dict_value(value): 

432 if not isinstance(value, list): 

433 return value 

434 if len(value) == 1: 

435 return value[0] 

436 if value[0] == LIST_START_VALUE: 

437 return value[1:] 

438 return value 

439 

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

441 return curr_token, dct 

442 

443 def parse_dict(curr_token): 

444 # dict start 

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

446 # dict contents 

447 curr_token, dct = parse_kv(curr_token) 

448 # dict end 

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

450 return curr_token, dct 

451 

452 def parse_graph(): 

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

454 if curr_token.category is not None: # EOF 

455 unexpected(curr_token, "EOF") 

456 if "graph" not in dct: 

457 raise NetworkXError("input contains no graph") 

458 graph = dct["graph"] 

459 if isinstance(graph, list): 

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

461 return graph 

462 

463 tokens = tokenize() 

464 graph = parse_graph() 

465 

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

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

468 if not multigraph: 

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

470 else: 

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

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

473 G.graph.update(graph_attr) 

474 

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

476 try: 

477 return dct.pop(attr) 

478 except KeyError as err: 

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

480 

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

482 mapping = {} 

483 node_labels = set() 

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

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

486 if id in G: 

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

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

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

490 if node_label in node_labels: 

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

492 node_labels.add(node_label) 

493 mapping[id] = node_label 

494 G.add_node(id, **node) 

495 

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

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

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

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

500 if source not in G: 

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

502 if target not in G: 

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

504 if not multigraph: 

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

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

507 else: 

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

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

510 raise nx.NetworkXError(msg) 

511 else: 

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

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

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

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

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

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

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

519 

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

521 G = nx.relabel_nodes(G, mapping) 

522 return G 

523 

524 

525def literal_stringizer(value): 

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

527 

528 Parameters 

529 ---------- 

530 value : object 

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

532 

533 Returns 

534 ------- 

535 rep : string 

536 A double-quoted Python literal representing value. Unprintable 

537 characters are replaced by XML character references. 

538 

539 Raises 

540 ------ 

541 ValueError 

542 If `value` cannot be converted to GML. 

543 

544 Notes 

545 ----- 

546 The original value can be recovered using the 

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

548 """ 

549 

550 def stringize(value): 

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

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

553 buf.write(str(1)) 

554 elif value is False: 

555 buf.write(str(0)) 

556 else: 

557 buf.write(str(value)) 

558 elif isinstance(value, str): 

559 text = repr(value) 

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

561 try: 

562 value.encode("latin1") 

563 except UnicodeEncodeError: 

564 text = "u" + text 

565 buf.write(text) 

566 elif isinstance(value, float | complex | str | bytes): 

567 buf.write(repr(value)) 

568 elif isinstance(value, list): 

569 buf.write("[") 

570 first = True 

571 for item in value: 

572 if not first: 

573 buf.write(",") 

574 else: 

575 first = False 

576 stringize(item) 

577 buf.write("]") 

578 elif isinstance(value, tuple): 

579 if len(value) > 1: 

580 buf.write("(") 

581 first = True 

582 for item in value: 

583 if not first: 

584 buf.write(",") 

585 else: 

586 first = False 

587 stringize(item) 

588 buf.write(")") 

589 elif value: 

590 buf.write("(") 

591 stringize(value[0]) 

592 buf.write(",)") 

593 else: 

594 buf.write("()") 

595 elif isinstance(value, dict): 

596 buf.write("{") 

597 first = True 

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

599 if not first: 

600 buf.write(",") 

601 else: 

602 first = False 

603 stringize(key) 

604 buf.write(":") 

605 stringize(value) 

606 buf.write("}") 

607 elif isinstance(value, set): 

608 buf.write("{") 

609 first = True 

610 for item in value: 

611 if not first: 

612 buf.write(",") 

613 else: 

614 first = False 

615 stringize(item) 

616 buf.write("}") 

617 else: 

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

619 raise ValueError(msg) 

620 

621 buf = StringIO() 

622 stringize(value) 

623 return buf.getvalue() 

624 

625 

626def generate_gml(G, stringizer=None): 

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

628 

629 Parameters 

630 ---------- 

631 G : NetworkX graph 

632 The graph to be converted to GML. 

633 

634 stringizer : callable, optional 

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

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

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

638 

639 Returns 

640 ------- 

641 lines: generator of strings 

642 Lines of GML data. Newlines are not appended. 

643 

644 Raises 

645 ------ 

646 NetworkXError 

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

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

649 

650 See Also 

651 -------- 

652 literal_stringizer 

653 

654 Notes 

655 ----- 

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

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

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

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

660 structure. 

661 

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

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

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

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

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

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

668 

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

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

671 

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

673 

674 Examples 

675 -------- 

676 >>> G = nx.Graph() 

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

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

679 graph [ 

680 node [ 

681 id 0 

682 label "1" 

683 ] 

684 ] 

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

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

687 graph [ 

688 multigraph 1 

689 node [ 

690 id 0 

691 label "a" 

692 ] 

693 node [ 

694 id 1 

695 label "b" 

696 ] 

697 edge [ 

698 source 0 

699 target 1 

700 key 0 

701 ] 

702 edge [ 

703 source 0 

704 target 1 

705 key 1 

706 ] 

707 ] 

708 """ 

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

710 

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

712 if not isinstance(key, str): 

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

714 if not valid_keys.match(key): 

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

716 if not isinstance(key, str): 

717 key = str(key) 

718 if key not in ignored_keys: 

719 if isinstance(value, int | bool): 

720 if key == "label": 

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

722 elif value is True: 

723 # python bool is an instance of int 

724 yield indent + key + " 1" 

725 elif value is False: 

726 yield indent + key + " 0" 

727 # GML only supports signed 32-bit integers 

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

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

730 else: 

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

732 elif isinstance(value, float): 

733 text = repr(value).upper() 

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

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

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

737 text = "+" + text 

738 else: 

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

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

741 # integral and hence needs fixing. 

742 epos = text.rfind("E") 

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

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

745 if key == "label": 

746 yield indent + key + ' "' + text + '"' 

747 else: 

748 yield indent + key + " " + text 

749 elif isinstance(value, dict): 

750 yield indent + key + " [" 

751 next_indent = indent + " " 

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

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

754 yield indent + "]" 

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

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

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

758 if len(value) == 0: 

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

760 if len(value) == 1: 

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

762 for val in value: 

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

764 else: 

765 if stringizer: 

766 try: 

767 value = stringizer(value) 

768 except ValueError as err: 

769 raise NetworkXError( 

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

771 ) from err 

772 if not isinstance(value, str): 

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

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

775 

776 multigraph = G.is_multigraph() 

777 yield "graph [" 

778 

779 # Output graph attributes 

780 if G.is_directed(): 

781 yield " directed 1" 

782 if multigraph: 

783 yield " multigraph 1" 

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

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

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

787 

788 # Output node data 

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

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

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

792 yield " node [" 

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

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

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

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

797 yield " ]" 

798 

799 # Output edge data 

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

801 kwargs = {"data": True} 

802 if multigraph: 

803 ignored_keys.add("key") 

804 kwargs["keys"] = True 

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

806 yield " edge [" 

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

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

809 if multigraph: 

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

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

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

813 yield " ]" 

814 yield "]" 

815 

816 

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

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

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

820 

821 Parameters 

822 ---------- 

823 G : NetworkX graph 

824 The graph to be converted to GML. 

825 

826 path : string or file 

827 Filename or file handle to write to. 

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

829 

830 stringizer : callable, optional 

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

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

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

834 

835 Raises 

836 ------ 

837 NetworkXError 

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

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

840 

841 See Also 

842 -------- 

843 read_gml, generate_gml 

844 literal_stringizer 

845 

846 Notes 

847 ----- 

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

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

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

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

852 structure. 

853 

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

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

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

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

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

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

860 

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

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

863 attribute names. 

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

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

866 

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

868 

869 Examples 

870 -------- 

871 >>> G = nx.path_graph(5) 

872 >>> nx.write_gml(G, "test_path5.gml") 

873 

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

875 

876 >>> nx.write_gml(G, "test_path5.gml.gz") 

877 """ 

878 for line in generate_gml(G, stringizer): 

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