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