comparison env/lib/python3.9/site-packages/networkx/readwrite/gml.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 """
2 Read graphs in GML format.
3
4 "GML, the Graph Modelling Language, is our proposal for a portable
5 file format for graphs. GML's key features are portability, simple
6 syntax, extensibility and flexibility. A GML file consists of a
7 hierarchical key-value lists. Graphs can be annotated with arbitrary
8 data structures. The idea for a common file format was born at the
9 GD'95; this proposal is the outcome of many discussions. GML is the
10 standard file format in the Graphlet graph editor system. It has been
11 overtaken and adapted by several other systems for drawing graphs."
12
13 GML files are stored using a 7-bit ASCII encoding with any extended
14 ASCII characters (iso8859-1) appearing as HTML character entities.
15 You will need to give some thought into how the exported data should
16 interact with different languages and even different Python versions.
17 Re-importing from gml is also a concern.
18
19 Without specifying a `stringizer`/`destringizer`, the code is capable of
20 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML
21 specification. For other data types, you need to explicitly supply a
22 `stringizer`/`destringizer`.
23
24 For additional documentation on the GML file format, please see the
25 `GML website <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_.
26
27 Several 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 from io import StringIO
31 from ast import literal_eval
32 from collections import defaultdict
33 from enum import Enum
34 from typing import Any, NamedTuple
35 import networkx as nx
36 from networkx.exception import NetworkXError
37 from networkx.utils import open_file
38
39 import warnings
40 import re
41 import html.entities as htmlentitydefs
42
43 __all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"]
44
45
46 def 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
61 def 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
86 def 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 msg = "literal_destringizer is deprecated and will be removed in 3.0."
105 warnings.warn(msg, DeprecationWarning)
106 if isinstance(rep, str):
107 orig_rep = rep
108 try:
109 return literal_eval(rep)
110 except SyntaxError as e:
111 raise ValueError(f"{orig_rep!r} is not a valid Python literal") from e
112 else:
113 raise ValueError(f"{rep!r} is not a string")
114
115
116 @open_file(0, mode="rb")
117 def read_gml(path, label="label", destringizer=None):
118 """Read graph in GML format from `path`.
119
120 Parameters
121 ----------
122 path : filename or filehandle
123 The filename or filehandle to read from.
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
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 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML
154 specification. For other data types, you need to explicitly supply a
155 `stringizer`/`destringizer`.
156
157 For additional documentation on the GML file format, please see the
158 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_.
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 >>> H = nx.read_gml("test.gml")
167 """
168
169 def filter_lines(lines):
170 for line in lines:
171 try:
172 line = line.decode("ascii")
173 except UnicodeDecodeError as e:
174 raise NetworkXError("input is not ASCII-encoded") from e
175 if not isinstance(line, str):
176 lines = str(lines)
177 if line and line[-1] == "\n":
178 line = line[:-1]
179 yield line
180
181 G = parse_gml_lines(filter_lines(path), label, destringizer)
182 return G
183
184
185 def parse_gml(lines, label="label", destringizer=None):
186 """Parse GML graph from a string or iterable.
187
188 Parameters
189 ----------
190 lines : string or iterable of strings
191 Data in GML format.
192
193 label : string, optional
194 If not None, the parsed nodes will be renamed according to node
195 attributes indicated by `label`. Default value: 'label'.
196
197 destringizer : callable, optional
198 A `destringizer` that recovers values stored as strings in GML. If it
199 cannot convert a string to a value, a `ValueError` is raised. Default
200 value : None.
201
202 Returns
203 -------
204 G : NetworkX graph
205 The parsed graph.
206
207 Raises
208 ------
209 NetworkXError
210 If the input cannot be parsed.
211
212 See Also
213 --------
214 write_gml, read_gml
215
216 Notes
217 -----
218 This stores nested GML attributes as dictionaries in the NetworkX graph,
219 node, and edge attribute structures.
220
221 GML files are stored using a 7-bit ASCII encoding with any extended
222 ASCII characters (iso8859-1) appearing as HTML character entities.
223 Without specifying a `stringizer`/`destringizer`, the code is capable of
224 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML
225 specification. For other data types, you need to explicitly supply a
226 `stringizer`/`destringizer`.
227
228 For additional documentation on the GML file format, please see the
229 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_.
230
231 See the module docstring :mod:`networkx.readwrite.gml` for more details.
232 """
233
234 def decode_line(line):
235 if isinstance(line, bytes):
236 try:
237 line.decode("ascii")
238 except UnicodeDecodeError as e:
239 raise NetworkXError("input is not ASCII-encoded") from e
240 if not isinstance(line, str):
241 line = str(line)
242 return line
243
244 def filter_lines(lines):
245 if isinstance(lines, str):
246 lines = decode_line(lines)
247 lines = lines.splitlines()
248 yield from lines
249 else:
250 for line in lines:
251 line = decode_line(line)
252 if line and line[-1] == "\n":
253 line = line[:-1]
254 if line.find("\n") != -1:
255 raise NetworkXError("input line contains newline")
256 yield line
257
258 G = parse_gml_lines(filter_lines(lines), label, destringizer)
259 return G
260
261
262 class Pattern(Enum):
263 """ encodes the index of each token-matching pattern in `tokenize`. """
264
265 KEYS = 0
266 REALS = 1
267 INTS = 2
268 STRINGS = 3
269 DICT_START = 4
270 DICT_END = 5
271 COMMENT_WHITESPACE = 6
272
273
274 class Token(NamedTuple):
275 category: Pattern
276 value: Any
277 line: int
278 position: int
279
280
281 LIST_START_VALUE = "_networkx_list_start"
282
283
284 def parse_gml_lines(lines, label, destringizer):
285 """Parse GML `lines` into a graph.
286 """
287
288 def tokenize():
289 patterns = [
290 r"[A-Za-z][0-9A-Za-z_]*\b", # keys
291 # reals
292 r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*)(?:[Ee][+-]?[0-9]+)?",
293 r"[+-]?[0-9]+", # ints
294 r'".*?"', # strings
295 r"\[", # dict start
296 r"\]", # dict end
297 r"#.*$|\s+", # comments and whitespaces
298 ]
299 tokens = re.compile("|".join(f"({pattern})" for pattern in patterns))
300 lineno = 0
301 for line in lines:
302 length = len(line)
303 pos = 0
304 while pos < length:
305 match = tokens.match(line, pos)
306 if match is None:
307 m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})"
308 raise NetworkXError(m)
309 for i in range(len(patterns)):
310 group = match.group(i + 1)
311 if group is not None:
312 if i == 0: # keys
313 value = group.rstrip()
314 elif i == 1: # reals
315 value = float(group)
316 elif i == 2: # ints
317 value = int(group)
318 else:
319 value = group
320 if i != 6: # comments and whitespaces
321 yield Token(Pattern(i), value, lineno + 1, pos + 1)
322 pos += len(group)
323 break
324 lineno += 1
325 yield Token(None, None, lineno + 1, 1) # EOF
326
327 def unexpected(curr_token, expected):
328 category, value, lineno, pos = curr_token
329 value = repr(value) if value is not None else "EOF"
330 raise NetworkXError(f"expected {expected}, found {value} at ({lineno}, {pos})")
331
332 def consume(curr_token, category, expected):
333 if curr_token.category == category:
334 return next(tokens)
335 unexpected(curr_token, expected)
336
337 def parse_kv(curr_token):
338 dct = defaultdict(list)
339 while curr_token.category == Pattern.KEYS:
340 key = curr_token.value
341 curr_token = next(tokens)
342 category = curr_token.category
343 if category == Pattern.REALS or category == Pattern.INTS:
344 value = curr_token.value
345 curr_token = next(tokens)
346 elif category == Pattern.STRINGS:
347 value = unescape(curr_token.value[1:-1])
348 if destringizer:
349 try:
350 value = destringizer(value)
351 except ValueError:
352 pass
353 curr_token = next(tokens)
354 elif category == Pattern.DICT_START:
355 curr_token, value = parse_dict(curr_token)
356 else:
357 # Allow for string convertible id and label values
358 if key in ("id", "label", "source", "target"):
359 try:
360 # String convert the token value
361 value = unescape(str(curr_token.value))
362 if destringizer:
363 try:
364 value = destringizer(value)
365 except ValueError:
366 pass
367 curr_token = next(tokens)
368 except Exception:
369 msg = (
370 "an int, float, string, '[' or string"
371 + " convertable ASCII value for node id or label"
372 )
373 unexpected(curr_token, msg)
374 else: # Otherwise error out
375 unexpected(curr_token, "an int, float, string or '['")
376 dct[key].append(value)
377
378 def clean_dict_value(value):
379 if not isinstance(value, list):
380 return value
381 if len(value) == 1:
382 return value[0]
383 if value[0] == LIST_START_VALUE:
384 return value[1:]
385 return value
386
387 dct = {key: clean_dict_value(value) for key, value in dct.items()}
388 return curr_token, dct
389
390 def parse_dict(curr_token):
391 # dict start
392 curr_token = consume(curr_token, Pattern.DICT_START, "'['")
393 # dict contents
394 curr_token, dct = parse_kv(curr_token)
395 # dict end
396 curr_token = consume(curr_token, Pattern.DICT_END, "']'")
397 return curr_token, dct
398
399 def parse_graph():
400 curr_token, dct = parse_kv(next(tokens))
401 if curr_token.category is not None: # EOF
402 unexpected(curr_token, "EOF")
403 if "graph" not in dct:
404 raise NetworkXError("input contains no graph")
405 graph = dct["graph"]
406 if isinstance(graph, list):
407 raise NetworkXError("input contains more than one graph")
408 return graph
409
410 tokens = tokenize()
411 graph = parse_graph()
412
413 directed = graph.pop("directed", False)
414 multigraph = graph.pop("multigraph", False)
415 if not multigraph:
416 G = nx.DiGraph() if directed else nx.Graph()
417 else:
418 G = nx.MultiDiGraph() if directed else nx.MultiGraph()
419 graph_attr = {k: v for k, v in graph.items() if k not in ("node", "edge")}
420 G.graph.update(graph_attr)
421
422 def pop_attr(dct, category, attr, i):
423 try:
424 return dct.pop(attr)
425 except KeyError as e:
426 raise NetworkXError(f"{category} #{i} has no '{attr}' attribute") from e
427
428 nodes = graph.get("node", [])
429 mapping = {}
430 node_labels = set()
431 for i, node in enumerate(nodes if isinstance(nodes, list) else [nodes]):
432 id = pop_attr(node, "node", "id", i)
433 if id in G:
434 raise NetworkXError(f"node id {id!r} is duplicated")
435 if label is not None and label != "id":
436 node_label = pop_attr(node, "node", label, i)
437 if node_label in node_labels:
438 raise NetworkXError(f"node label {node_label!r} is duplicated")
439 node_labels.add(node_label)
440 mapping[id] = node_label
441 G.add_node(id, **node)
442
443 edges = graph.get("edge", [])
444 for i, edge in enumerate(edges if isinstance(edges, list) else [edges]):
445 source = pop_attr(edge, "edge", "source", i)
446 target = pop_attr(edge, "edge", "target", i)
447 if source not in G:
448 raise NetworkXError(f"edge #{i} has undefined source {source!r}")
449 if target not in G:
450 raise NetworkXError(f"edge #{i} has undefined target {target!r}")
451 if not multigraph:
452 if not G.has_edge(source, target):
453 G.add_edge(source, target, **edge)
454 else:
455 arrow = "->" if directed else "--"
456 msg = f"edge #{i} ({source!r}{arrow}{target!r}) is duplicated"
457 raise nx.NetworkXError(msg)
458 else:
459 key = edge.pop("key", None)
460 if key is not None and G.has_edge(source, target, key):
461 arrow = "->" if directed else "--"
462 msg = f"edge #{i} ({source!r}{arrow}{target!r}, {key!r})"
463 msg2 = 'Hint: If multigraph add "multigraph 1" to file header.'
464 raise nx.NetworkXError(msg + " is duplicated\n" + msg2)
465 G.add_edge(source, target, key, **edge)
466
467 if label is not None and label != "id":
468 G = nx.relabel_nodes(G, mapping)
469 return G
470
471
472 def literal_stringizer(value):
473 """Convert a `value` to a Python literal in GML representation.
474
475 Parameters
476 ----------
477 value : object
478 The `value` to be converted to GML representation.
479
480 Returns
481 -------
482 rep : string
483 A double-quoted Python literal representing value. Unprintable
484 characters are replaced by XML character references.
485
486 Raises
487 ------
488 ValueError
489 If `value` cannot be converted to GML.
490
491 Notes
492 -----
493 `literal_stringizer` is largely the same as `repr` in terms of
494 functionality but attempts prefix `unicode` and `bytes` literals with
495 `u` and `b` to provide better interoperability of data generated by
496 Python 2 and Python 3.
497
498 The original value can be recovered using the
499 :func:`networkx.readwrite.gml.literal_destringizer` function.
500 """
501 msg = "literal_stringizer is deprecated and will be removed in 3.0."
502 warnings.warn(msg, DeprecationWarning)
503
504 def stringize(value):
505 if isinstance(value, (int, bool)) or value is None:
506 if value is True: # GML uses 1/0 for boolean values.
507 buf.write(str(1))
508 elif value is False:
509 buf.write(str(0))
510 else:
511 buf.write(str(value))
512 elif isinstance(value, str):
513 text = repr(value)
514 if text[0] != "u":
515 try:
516 value.encode("latin1")
517 except UnicodeEncodeError:
518 text = "u" + text
519 buf.write(text)
520 elif isinstance(value, (float, complex, str, bytes)):
521 buf.write(repr(value))
522 elif isinstance(value, list):
523 buf.write("[")
524 first = True
525 for item in value:
526 if not first:
527 buf.write(",")
528 else:
529 first = False
530 stringize(item)
531 buf.write("]")
532 elif isinstance(value, tuple):
533 if len(value) > 1:
534 buf.write("(")
535 first = True
536 for item in value:
537 if not first:
538 buf.write(",")
539 else:
540 first = False
541 stringize(item)
542 buf.write(")")
543 elif value:
544 buf.write("(")
545 stringize(value[0])
546 buf.write(",)")
547 else:
548 buf.write("()")
549 elif isinstance(value, dict):
550 buf.write("{")
551 first = True
552 for key, value in value.items():
553 if not first:
554 buf.write(",")
555 else:
556 first = False
557 stringize(key)
558 buf.write(":")
559 stringize(value)
560 buf.write("}")
561 elif isinstance(value, set):
562 buf.write("{")
563 first = True
564 for item in value:
565 if not first:
566 buf.write(",")
567 else:
568 first = False
569 stringize(item)
570 buf.write("}")
571 else:
572 msg = "{value!r} cannot be converted into a Python literal"
573 raise ValueError(msg)
574
575 buf = StringIO()
576 stringize(value)
577 return buf.getvalue()
578
579
580 def generate_gml(G, stringizer=None):
581 r"""Generate a single entry of the graph `G` in GML format.
582
583 Parameters
584 ----------
585 G : NetworkX graph
586 The graph to be converted to GML.
587
588 stringizer : callable, optional
589 A `stringizer` which converts non-int/non-float/non-dict values into
590 strings. If it cannot convert a value into a string, it should raise a
591 `ValueError` to indicate that. Default value: None.
592
593 Returns
594 -------
595 lines: generator of strings
596 Lines of GML data. Newlines are not appended.
597
598 Raises
599 ------
600 NetworkXError
601 If `stringizer` cannot convert a value into a string, or the value to
602 convert is not a string while `stringizer` is None.
603
604 Notes
605 -----
606 Graph attributes named 'directed', 'multigraph', 'node' or
607 'edge', node attributes named 'id' or 'label', edge attributes
608 named 'source' or 'target' (or 'key' if `G` is a multigraph)
609 are ignored because these attribute names are used to encode the graph
610 structure.
611
612 GML files are stored using a 7-bit ASCII encoding with any extended
613 ASCII characters (iso8859-1) appearing as HTML character entities.
614 Without specifying a `stringizer`/`destringizer`, the code is capable of
615 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML
616 specification. For other data types, you need to explicitly supply a
617 `stringizer`/`destringizer`.
618
619 For additional documentation on the GML file format, please see the
620 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_.
621
622 See the module docstring :mod:`networkx.readwrite.gml` for more details.
623
624 Examples
625 --------
626 >>> G = nx.Graph()
627 >>> G.add_node("1")
628 >>> print("\n".join(nx.generate_gml(G)))
629 graph [
630 node [
631 id 0
632 label "1"
633 ]
634 ]
635 >>> G = nx.OrderedMultiGraph([("a", "b"), ("a", "b")])
636 >>> print("\n".join(nx.generate_gml(G)))
637 graph [
638 multigraph 1
639 node [
640 id 0
641 label "a"
642 ]
643 node [
644 id 1
645 label "b"
646 ]
647 edge [
648 source 0
649 target 1
650 key 0
651 ]
652 edge [
653 source 0
654 target 1
655 key 1
656 ]
657 ]
658 """
659 valid_keys = re.compile("^[A-Za-z][0-9A-Za-z_]*$")
660
661 def stringize(key, value, ignored_keys, indent, in_list=False):
662 if not isinstance(key, str):
663 raise NetworkXError(f"{key!r} is not a string")
664 if not valid_keys.match(key):
665 raise NetworkXError(f"{key!r} is not a valid key")
666 if not isinstance(key, str):
667 key = str(key)
668 if key not in ignored_keys:
669 if isinstance(value, (int, bool)):
670 if key == "label":
671 yield indent + key + ' "' + str(value) + '"'
672 elif value is True:
673 # python bool is an instance of int
674 yield indent + key + " 1"
675 elif value is False:
676 yield indent + key + " 0"
677 # GML only supports signed 32-bit integers
678 elif value < -(2 ** 31) or value >= 2 ** 31:
679 yield indent + key + ' "' + str(value) + '"'
680 else:
681 yield indent + key + " " + str(value)
682 elif isinstance(value, float):
683 text = repr(value).upper()
684 # GML requires that a real literal contain a decimal point, but
685 # repr may not output a decimal point when the mantissa is
686 # integral and hence needs fixing.
687 epos = text.rfind("E")
688 if epos != -1 and text.find(".", 0, epos) == -1:
689 text = text[:epos] + "." + text[epos:]
690 if key == "label":
691 yield indent + key + ' "' + text + '"'
692 else:
693 yield indent + key + " " + text
694 elif isinstance(value, dict):
695 yield indent + key + " ["
696 next_indent = indent + " "
697 for key, value in value.items():
698 yield from stringize(key, value, (), next_indent)
699 yield indent + "]"
700 elif (
701 isinstance(value, (list, tuple))
702 and key != "label"
703 and value
704 and not in_list
705 ):
706 if len(value) == 1:
707 yield indent + key + " " + f'"{LIST_START_VALUE}"'
708 for val in value:
709 yield from stringize(key, val, (), indent, True)
710 else:
711 if stringizer:
712 try:
713 value = stringizer(value)
714 except ValueError as e:
715 raise NetworkXError(
716 f"{value!r} cannot be converted into a string"
717 ) from e
718 if not isinstance(value, str):
719 raise NetworkXError(f"{value!r} is not a string")
720 yield indent + key + ' "' + escape(value) + '"'
721
722 multigraph = G.is_multigraph()
723 yield "graph ["
724
725 # Output graph attributes
726 if G.is_directed():
727 yield " directed 1"
728 if multigraph:
729 yield " multigraph 1"
730 ignored_keys = {"directed", "multigraph", "node", "edge"}
731 for attr, value in G.graph.items():
732 yield from stringize(attr, value, ignored_keys, " ")
733
734 # Output node data
735 node_id = dict(zip(G, range(len(G))))
736 ignored_keys = {"id", "label"}
737 for node, attrs in G.nodes.items():
738 yield " node ["
739 yield " id " + str(node_id[node])
740 yield from stringize("label", node, (), " ")
741 for attr, value in attrs.items():
742 yield from stringize(attr, value, ignored_keys, " ")
743 yield " ]"
744
745 # Output edge data
746 ignored_keys = {"source", "target"}
747 kwargs = {"data": True}
748 if multigraph:
749 ignored_keys.add("key")
750 kwargs["keys"] = True
751 for e in G.edges(**kwargs):
752 yield " edge ["
753 yield " source " + str(node_id[e[0]])
754 yield " target " + str(node_id[e[1]])
755 if multigraph:
756 yield from stringize("key", e[2], (), " ")
757 for attr, value in e[-1].items():
758 yield from stringize(attr, value, ignored_keys, " ")
759 yield " ]"
760 yield "]"
761
762
763 @open_file(1, mode="wb")
764 def write_gml(G, path, stringizer=None):
765 """Write a graph `G` in GML format to the file or file handle `path`.
766
767 Parameters
768 ----------
769 G : NetworkX graph
770 The graph to be converted to GML.
771
772 path : filename or filehandle
773 The filename or filehandle to write. Files whose names end with .gz or
774 .bz2 will be compressed.
775
776 stringizer : callable, optional
777 A `stringizer` which converts non-int/non-float/non-dict values into
778 strings. If it cannot convert a value into a string, it should raise a
779 `ValueError` to indicate that. Default value: None.
780
781 Raises
782 ------
783 NetworkXError
784 If `stringizer` cannot convert a value into a string, or the value to
785 convert is not a string while `stringizer` is None.
786
787 See Also
788 --------
789 read_gml, generate_gml
790
791 Notes
792 -----
793 Graph attributes named 'directed', 'multigraph', 'node' or
794 'edge', node attributes named 'id' or 'label', edge attributes
795 named 'source' or 'target' (or 'key' if `G` is a multigraph)
796 are ignored because these attribute names are used to encode the graph
797 structure.
798
799 GML files are stored using a 7-bit ASCII encoding with any extended
800 ASCII characters (iso8859-1) appearing as HTML character entities.
801 Without specifying a `stringizer`/`destringizer`, the code is capable of
802 handling `int`/`float`/`str`/`dict`/`list` data as required by the GML
803 specification. For other data types, you need to explicitly supply a
804 `stringizer`/`destringizer`.
805
806 Note that while we allow non-standard GML to be read from a file, we make
807 sure to write GML format. In particular, underscores are not allowed in
808 attribute names.
809 For additional documentation on the GML file format, please see the
810 `GML url <http://www.infosun.fim.uni-passau.de/Graphlet/GML/gml-tr.html>`_.
811
812 See the module docstring :mod:`networkx.readwrite.gml` for more details.
813
814 Examples
815 --------
816 >>> G = nx.path_graph(4)
817 >>> nx.write_gml(G, "test.gml")
818
819 Filenames ending in .gz or .bz2 will be compressed.
820
821 >>> nx.write_gml(G, "test.gml.gz")
822 """
823 for line in generate_gml(G, stringizer):
824 path.write((line + "\n").encode("ascii"))