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
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-20 07:00 +0000
1"""
2Read graphs in GML format.
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."
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.
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`.
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>`_.
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
39import networkx as nx
40from networkx.exception import NetworkXError
41from networkx.utils import open_file
43__all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"]
46def escape(text):
47 """Use XML character references to escape characters.
49 Use XML character references for unprintable or non-ASCII
50 characters, double quotes and ampersands in a string
51 """
53 def fixup(m):
54 ch = m.group(0)
55 return "&#" + str(ord(ch)) + ";"
57 text = re.sub('[^ -~]|[&"]', fixup, text)
58 return text if isinstance(text, str) else str(text)
61def unescape(text):
62 """Replace XML character references with the referenced characters"""
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
83 return re.sub("&(?:[0-9A-Za-z]+|#(?:[0-9]+|x[0-9A-Fa-f]+));", fixup, text)
86def literal_destringizer(rep):
87 """Convert a Python literal to the value it represents.
89 Parameters
90 ----------
91 rep : string
92 A Python literal.
94 Returns
95 -------
96 value : object
97 The value of the Python literal.
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")
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`.
119 Parameters
120 ----------
121 path : filename or filehandle
122 The filename or filehandle to read from.
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'.
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.
133 Returns
134 -------
135 G : NetworkX graph
136 The parsed graph.
138 Raises
139 ------
140 NetworkXError
141 If the input cannot be parsed.
143 See Also
144 --------
145 write_gml, parse_gml
146 literal_destringizer
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`.
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>`_.
160 See the module docstring :mod:`networkx.readwrite.gml` for more details.
162 Examples
163 --------
164 >>> G = nx.path_graph(4)
165 >>> nx.write_gml(G, "test.gml")
167 GML values are interpreted as strings by default:
169 >>> H = nx.read_gml("test.gml")
170 >>> H.nodes
171 NodeView(('0', '1', '2', '3'))
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:
176 >>> J = nx.read_gml("test.gml", destringizer=int)
177 >>> J.nodes
178 NodeView((0, 1, 2, 3))
180 """
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
194 G = parse_gml_lines(filter_lines(path), label, destringizer)
195 return G
198@nx._dispatch(graphs=None)
199def parse_gml(lines, label="label", destringizer=None):
200 """Parse GML graph from a string or iterable.
202 Parameters
203 ----------
204 lines : string or iterable of strings
205 Data in GML format.
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'.
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.
216 Returns
217 -------
218 G : NetworkX graph
219 The parsed graph.
221 Raises
222 ------
223 NetworkXError
224 If the input cannot be parsed.
226 See Also
227 --------
228 write_gml, read_gml
230 Notes
231 -----
232 This stores nested GML attributes as dictionaries in the NetworkX graph,
233 node, and edge attribute structures.
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`.
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>`_.
245 See the module docstring :mod:`networkx.readwrite.gml` for more details.
246 """
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
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
272 G = parse_gml_lines(filter_lines(lines), label, destringizer)
273 return G
276class Pattern(Enum):
277 """encodes the index of each token-matching pattern in `tokenize`."""
279 KEYS = 0
280 REALS = 1
281 INTS = 2
282 STRINGS = 3
283 DICT_START = 4
284 DICT_END = 5
285 COMMENT_WHITESPACE = 6
288class Token(NamedTuple):
289 category: Pattern
290 value: Any
291 line: int
292 position: int
295LIST_START_VALUE = "_networkx_list_start"
298def parse_gml_lines(lines, label, destringizer):
299 """Parse GML `lines` into a graph."""
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
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
340 length = len(line)
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
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})")
370 def consume(curr_token, category, expected):
371 if curr_token.category == category:
372 return next(tokens)
373 unexpected(curr_token, expected)
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)
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
439 dct = {key: clean_dict_value(value) for key, value in dct.items()}
440 return curr_token, dct
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
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
462 tokens = tokenize()
463 graph = parse_graph()
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)
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
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)
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)
519 if label is not None and label != "id":
520 G = nx.relabel_nodes(G, mapping)
521 return G
524def literal_stringizer(value):
525 """Convert a `value` to a Python literal in GML representation.
527 Parameters
528 ----------
529 value : object
530 The `value` to be converted to GML representation.
532 Returns
533 -------
534 rep : string
535 A double-quoted Python literal representing value. Unprintable
536 characters are replaced by XML character references.
538 Raises
539 ------
540 ValueError
541 If `value` cannot be converted to GML.
543 Notes
544 -----
545 The original value can be recovered using the
546 :func:`networkx.readwrite.gml.literal_destringizer` function.
547 """
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)
620 buf = StringIO()
621 stringize(value)
622 return buf.getvalue()
625def generate_gml(G, stringizer=None):
626 r"""Generate a single entry of the graph `G` in GML format.
628 Parameters
629 ----------
630 G : NetworkX graph
631 The graph to be converted to GML.
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.
638 Returns
639 -------
640 lines: generator of strings
641 Lines of GML data. Newlines are not appended.
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.
649 See Also
650 --------
651 literal_stringizer
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.
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`.
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>`_.
671 See the module docstring :mod:`networkx.readwrite.gml` for more details.
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_]*$")
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) + '"'
775 multigraph = G.is_multigraph()
776 yield "graph ["
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, " ")
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 " ]"
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 "]"
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`.
820 Parameters
821 ----------
822 G : NetworkX graph
823 The graph to be converted to GML.
825 path : filename or filehandle
826 The filename or filehandle to write. Files whose names end with .gz or
827 .bz2 will be compressed.
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.
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.
840 See Also
841 --------
842 read_gml, generate_gml
843 literal_stringizer
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.
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`.
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>`_.
866 See the module docstring :mod:`networkx.readwrite.gml` for more details.
868 Examples
869 --------
870 >>> G = nx.path_graph(4)
871 >>> nx.write_gml(G, "test.gml")
873 Filenames ending in .gz or .bz2 will be compressed.
875 >>> nx.write_gml(G, "test.gml.gz")
876 """
877 for line in generate_gml(G, stringizer):
878 path.write((line + "\n").encode("ascii"))