aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py879
1 files changed, 879 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py
new file mode 100644
index 00000000..891d7096
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py
@@ -0,0 +1,879 @@
+"""
+Read graphs in GML format.
+
+"GML, the Graph Modelling Language, is our proposal for a portable
+file format for graphs. GML's key features are portability, simple
+syntax, extensibility and flexibility. A GML file consists of a
+hierarchical key-value lists. Graphs can be annotated with arbitrary
+data structures. The idea for a common file format was born at the
+GD'95; this proposal is the outcome of many discussions. GML is the
+standard file format in the Graphlet graph editor system. It has been
+overtaken and adapted by several other systems for drawing graphs."
+
+GML files are stored using a 7-bit ASCII encoding with any extended
+ASCII characters (iso8859-1) appearing as HTML character entities.
+You will need to give some thought into how the exported data should
+interact with different languages and even different Python versions.
+Re-importing from gml is also a concern.
+
+Without specifying a `stringizer`/`destringizer`, the code is capable of
+writing `int`/`float`/`str`/`dict`/`list` data as required by the GML
+specification. For writing other data types, and for reading data other
+than `str` you need to explicitly supply a `stringizer`/`destringizer`.
+
+For additional documentation on the GML file format, please see the
+`GML website <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_.
+
+Several example graphs in GML format may be found on Mark Newman's
+`Network data page <http://www-personal.umich.edu/~mejn/netdata/>`_.
+"""
+
+import html.entities as htmlentitydefs
+import re
+import warnings
+from ast import literal_eval
+from collections import defaultdict
+from enum import Enum
+from io import StringIO
+from typing import Any, NamedTuple
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.utils import open_file
+
+__all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"]
+
+
+def escape(text):
+ """Use XML character references to escape characters.
+
+ Use XML character references for unprintable or non-ASCII
+ characters, double quotes and ampersands in a string
+ """
+
+ def fixup(m):
+ ch = m.group(0)
+ return "&#" + str(ord(ch)) + ";"
+
+ text = re.sub('[^ -~]|[&"]', fixup, text)
+ return text if isinstance(text, str) else str(text)
+
+
+def unescape(text):
+ """Replace XML character references with the referenced characters"""
+
+ def fixup(m):
+ text = m.group(0)
+ if text[1] == "#":
+ # Character reference
+ if text[2] == "x":
+ code = int(text[3:-1], 16)
+ else:
+ code = int(text[2:-1])
+ else:
+ # Named entity
+ try:
+ code = htmlentitydefs.name2codepoint[text[1:-1]]
+ except KeyError:
+ return text # leave unchanged
+ try:
+ return chr(code)
+ except (ValueError, OverflowError):
+ return text # leave unchanged
+
+ return re.sub("&(?:[0-9A-Za-z]+|#(?:[0-9]+|x[0-9A-Fa-f]+));", fixup, text)
+
+
+def literal_destringizer(rep):
+ """Convert a Python literal to the value it represents.
+
+ Parameters
+ ----------
+ rep : string
+ A Python literal.
+
+ Returns
+ -------
+ value : object
+ The value of the Python literal.
+
+ Raises
+ ------
+ ValueError
+ If `rep` is not a Python literal.
+ """
+ if isinstance(rep, str):
+ orig_rep = rep
+ try:
+ return literal_eval(rep)
+ except SyntaxError as err:
+ raise ValueError(f"{orig_rep!r} is not a valid Python literal") from err
+ else:
+ raise ValueError(f"{rep!r} is not a string")
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_gml(path, label="label", destringizer=None):
+ """Read graph in GML format from `path`.
+
+ Parameters
+ ----------
+ path : filename or filehandle
+ The filename or filehandle to read from.
+
+ label : string, optional
+ If not None, the parsed nodes will be renamed according to node
+ attributes indicated by `label`. Default value: 'label'.
+
+ destringizer : callable, optional
+ A `destringizer` that recovers values stored as strings in GML. If it
+ cannot convert a string to a value, a `ValueError` is raised. Default
+ value : None.
+
+ Returns
+ -------
+ G : NetworkX graph
+ The parsed graph.
+
+ Raises
+ ------
+ NetworkXError
+ If the input cannot be parsed.
+
+ See Also
+ --------
+ write_gml, parse_gml
+ literal_destringizer
+
+ Notes
+ -----
+ GML files are stored using a 7-bit ASCII encoding with any extended
+ ASCII characters (iso8859-1) appearing as HTML character entities.
+ Without specifying a `stringizer`/`destringizer`, the code is capable of
+ writing `int`/`float`/`str`/`dict`/`list` data as required by the GML
+ specification. For writing other data types, and for reading data other
+ than `str` you need to explicitly supply a `stringizer`/`destringizer`.
+
+ For additional documentation on the GML file format, please see the
+ `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_.
+
+ See the module docstring :mod:`networkx.readwrite.gml` for more details.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_gml(G, "test.gml")
+
+ GML values are interpreted as strings by default:
+
+ >>> H = nx.read_gml("test.gml")
+ >>> H.nodes
+ NodeView(('0', '1', '2', '3'))
+
+ When a `destringizer` is provided, GML values are converted to the provided type.
+ For example, integer nodes can be recovered as shown below:
+
+ >>> J = nx.read_gml("test.gml", destringizer=int)
+ >>> J.nodes
+ NodeView((0, 1, 2, 3))
+
+ """
+
+ def filter_lines(lines):
+ for line in lines:
+ try:
+ line = line.decode("ascii")
+ except UnicodeDecodeError as err:
+ raise NetworkXError("input is not ASCII-encoded") from err
+ if not isinstance(line, str):
+ lines = str(lines)
+ if line and line[-1] == "\n":
+ line = line[:-1]
+ yield line
+
+ G = parse_gml_lines(filter_lines(path), label, destringizer)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_gml(lines, label="label", destringizer=None):
+ """Parse GML graph from a string or iterable.
+
+ Parameters
+ ----------
+ lines : string or iterable of strings
+ Data in GML format.
+
+ label : string, optional
+ If not None, the parsed nodes will be renamed according to node
+ attributes indicated by `label`. Default value: 'label'.
+
+ destringizer : callable, optional
+ A `destringizer` that recovers values stored as strings in GML. If it
+ cannot convert a string to a value, a `ValueError` is raised. Default
+ value : None.
+
+ Returns
+ -------
+ G : NetworkX graph
+ The parsed graph.
+
+ Raises
+ ------
+ NetworkXError
+ If the input cannot be parsed.
+
+ See Also
+ --------
+ write_gml, read_gml
+
+ Notes
+ -----
+ This stores nested GML attributes as dictionaries in the NetworkX graph,
+ node, and edge attribute structures.
+
+ GML files are stored using a 7-bit ASCII encoding with any extended
+ ASCII characters (iso8859-1) appearing as HTML character entities.
+ Without specifying a `stringizer`/`destringizer`, the code is capable of
+ writing `int`/`float`/`str`/`dict`/`list` data as required by the GML
+ specification. For writing other data types, and for reading data other
+ than `str` you need to explicitly supply a `stringizer`/`destringizer`.
+
+ For additional documentation on the GML file format, please see the
+ `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_.
+
+ See the module docstring :mod:`networkx.readwrite.gml` for more details.
+ """
+
+ def decode_line(line):
+ if isinstance(line, bytes):
+ try:
+ line.decode("ascii")
+ except UnicodeDecodeError as err:
+ raise NetworkXError("input is not ASCII-encoded") from err
+ if not isinstance(line, str):
+ line = str(line)
+ return line
+
+ def filter_lines(lines):
+ if isinstance(lines, str):
+ lines = decode_line(lines)
+ lines = lines.splitlines()
+ yield from lines
+ else:
+ for line in lines:
+ line = decode_line(line)
+ if line and line[-1] == "\n":
+ line = line[:-1]
+ if line.find("\n") != -1:
+ raise NetworkXError("input line contains newline")
+ yield line
+
+ G = parse_gml_lines(filter_lines(lines), label, destringizer)
+ return G
+
+
+class Pattern(Enum):
+ """encodes the index of each token-matching pattern in `tokenize`."""
+
+ KEYS = 0
+ REALS = 1
+ INTS = 2
+ STRINGS = 3
+ DICT_START = 4
+ DICT_END = 5
+ COMMENT_WHITESPACE = 6
+
+
+class Token(NamedTuple):
+ category: Pattern
+ value: Any
+ line: int
+ position: int
+
+
+LIST_START_VALUE = "_networkx_list_start"
+
+
+def parse_gml_lines(lines, label, destringizer):
+ """Parse GML `lines` into a graph."""
+
+ def tokenize():
+ patterns = [
+ r"[A-Za-z][0-9A-Za-z_]*\b", # keys
+ # reals
+ r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*|INF)(?:[Ee][+-]?[0-9]+)?",
+ r"[+-]?[0-9]+", # ints
+ r'".*?"', # strings
+ r"\[", # dict start
+ r"\]", # dict end
+ r"#.*$|\s+", # comments and whitespaces
+ ]
+ tokens = re.compile("|".join(f"({pattern})" for pattern in patterns))
+ lineno = 0
+ multilines = [] # entries spread across multiple lines
+ for line in lines:
+ pos = 0
+
+ # deal with entries spread across multiple lines
+ #
+ # should we actually have to deal with escaped "s then do it here
+ if multilines:
+ multilines.append(line.strip())
+ if line[-1] == '"': # closing multiline entry
+ # multiline entries will be joined by space. cannot
+ # reintroduce newlines as this will break the tokenizer
+ line = " ".join(multilines)
+ multilines = []
+ else: # continued multiline entry
+ lineno += 1
+ continue
+ else:
+ if line.count('"') == 1: # opening multiline entry
+ if line.strip()[0] != '"' and line.strip()[-1] != '"':
+ # since we expect something like key "value", the " should not be found at ends
+ # otherwise tokenizer will pick up the formatting mistake.
+ multilines = [line.rstrip()]
+ lineno += 1
+ continue
+
+ length = len(line)
+
+ while pos < length:
+ match = tokens.match(line, pos)
+ if match is None:
+ m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})"
+ raise NetworkXError(m)
+ for i in range(len(patterns)):
+ group = match.group(i + 1)
+ if group is not None:
+ if i == 0: # keys
+ value = group.rstrip()
+ elif i == 1: # reals
+ value = float(group)
+ elif i == 2: # ints
+ value = int(group)
+ else:
+ value = group
+ if i != 6: # comments and whitespaces
+ yield Token(Pattern(i), value, lineno + 1, pos + 1)
+ pos += len(group)
+ break
+ lineno += 1
+ yield Token(None, None, lineno + 1, 1) # EOF
+
+ def unexpected(curr_token, expected):
+ category, value, lineno, pos = curr_token
+ value = repr(value) if value is not None else "EOF"
+ raise NetworkXError(f"expected {expected}, found {value} at ({lineno}, {pos})")
+
+ def consume(curr_token, category, expected):
+ if curr_token.category == category:
+ return next(tokens)
+ unexpected(curr_token, expected)
+
+ def parse_kv(curr_token):
+ dct = defaultdict(list)
+ while curr_token.category == Pattern.KEYS:
+ key = curr_token.value
+ curr_token = next(tokens)
+ category = curr_token.category
+ if category == Pattern.REALS or category == Pattern.INTS:
+ value = curr_token.value
+ curr_token = next(tokens)
+ elif category == Pattern.STRINGS:
+ value = unescape(curr_token.value[1:-1])
+ if destringizer:
+ try:
+ value = destringizer(value)
+ except ValueError:
+ pass
+ # Special handling for empty lists and tuples
+ if value == "()":
+ value = ()
+ if value == "[]":
+ value = []
+ curr_token = next(tokens)
+ elif category == Pattern.DICT_START:
+ curr_token, value = parse_dict(curr_token)
+ else:
+ # Allow for string convertible id and label values
+ if key in ("id", "label", "source", "target"):
+ try:
+ # String convert the token value
+ value = unescape(str(curr_token.value))
+ if destringizer:
+ try:
+ value = destringizer(value)
+ except ValueError:
+ pass
+ curr_token = next(tokens)
+ except Exception:
+ msg = (
+ "an int, float, string, '[' or string"
+ + " convertible ASCII value for node id or label"
+ )
+ unexpected(curr_token, msg)
+ # Special handling for nan and infinity. Since the gml language
+ # defines unquoted strings as keys, the numeric and string branches
+ # are skipped and we end up in this special branch, so we need to
+ # convert the current token value to a float for NAN and plain INF.
+ # +/-INF are handled in the pattern for 'reals' in tokenize(). This
+ # allows labels and values to be nan or infinity, but not keys.
+ elif curr_token.value in {"NAN", "INF"}:
+ value = float(curr_token.value)
+ curr_token = next(tokens)
+ else: # Otherwise error out
+ unexpected(curr_token, "an int, float, string or '['")
+ dct[key].append(value)
+
+ def clean_dict_value(value):
+ if not isinstance(value, list):
+ return value
+ if len(value) == 1:
+ return value[0]
+ if value[0] == LIST_START_VALUE:
+ return value[1:]
+ return value
+
+ dct = {key: clean_dict_value(value) for key, value in dct.items()}
+ return curr_token, dct
+
+ def parse_dict(curr_token):
+ # dict start
+ curr_token = consume(curr_token, Pattern.DICT_START, "'['")
+ # dict contents
+ curr_token, dct = parse_kv(curr_token)
+ # dict end
+ curr_token = consume(curr_token, Pattern.DICT_END, "']'")
+ return curr_token, dct
+
+ def parse_graph():
+ curr_token, dct = parse_kv(next(tokens))
+ if curr_token.category is not None: # EOF
+ unexpected(curr_token, "EOF")
+ if "graph" not in dct:
+ raise NetworkXError("input contains no graph")
+ graph = dct["graph"]
+ if isinstance(graph, list):
+ raise NetworkXError("input contains more than one graph")
+ return graph
+
+ tokens = tokenize()
+ graph = parse_graph()
+
+ directed = graph.pop("directed", False)
+ multigraph = graph.pop("multigraph", False)
+ if not multigraph:
+ G = nx.DiGraph() if directed else nx.Graph()
+ else:
+ G = nx.MultiDiGraph() if directed else nx.MultiGraph()
+ graph_attr = {k: v for k, v in graph.items() if k not in ("node", "edge")}
+ G.graph.update(graph_attr)
+
+ def pop_attr(dct, category, attr, i):
+ try:
+ return dct.pop(attr)
+ except KeyError as err:
+ raise NetworkXError(f"{category} #{i} has no {attr!r} attribute") from err
+
+ nodes = graph.get("node", [])
+ mapping = {}
+ node_labels = set()
+ for i, node in enumerate(nodes if isinstance(nodes, list) else [nodes]):
+ id = pop_attr(node, "node", "id", i)
+ if id in G:
+ raise NetworkXError(f"node id {id!r} is duplicated")
+ if label is not None and label != "id":
+ node_label = pop_attr(node, "node", label, i)
+ if node_label in node_labels:
+ raise NetworkXError(f"node label {node_label!r} is duplicated")
+ node_labels.add(node_label)
+ mapping[id] = node_label
+ G.add_node(id, **node)
+
+ edges = graph.get("edge", [])
+ for i, edge in enumerate(edges if isinstance(edges, list) else [edges]):
+ source = pop_attr(edge, "edge", "source", i)
+ target = pop_attr(edge, "edge", "target", i)
+ if source not in G:
+ raise NetworkXError(f"edge #{i} has undefined source {source!r}")
+ if target not in G:
+ raise NetworkXError(f"edge #{i} has undefined target {target!r}")
+ if not multigraph:
+ if not G.has_edge(source, target):
+ G.add_edge(source, target, **edge)
+ else:
+ arrow = "->" if directed else "--"
+ msg = f"edge #{i} ({source!r}{arrow}{target!r}) is duplicated"
+ raise nx.NetworkXError(msg)
+ else:
+ key = edge.pop("key", None)
+ if key is not None and G.has_edge(source, target, key):
+ arrow = "->" if directed else "--"
+ msg = f"edge #{i} ({source!r}{arrow}{target!r}, {key!r})"
+ msg2 = 'Hint: If multigraph add "multigraph 1" to file header.'
+ raise nx.NetworkXError(msg + " is duplicated\n" + msg2)
+ G.add_edge(source, target, key, **edge)
+
+ if label is not None and label != "id":
+ G = nx.relabel_nodes(G, mapping)
+ return G
+
+
+def literal_stringizer(value):
+ """Convert a `value` to a Python literal in GML representation.
+
+ Parameters
+ ----------
+ value : object
+ The `value` to be converted to GML representation.
+
+ Returns
+ -------
+ rep : string
+ A double-quoted Python literal representing value. Unprintable
+ characters are replaced by XML character references.
+
+ Raises
+ ------
+ ValueError
+ If `value` cannot be converted to GML.
+
+ Notes
+ -----
+ The original value can be recovered using the
+ :func:`networkx.readwrite.gml.literal_destringizer` function.
+ """
+
+ def stringize(value):
+ if isinstance(value, int | bool) or value is None:
+ if value is True: # GML uses 1/0 for boolean values.
+ buf.write(str(1))
+ elif value is False:
+ buf.write(str(0))
+ else:
+ buf.write(str(value))
+ elif isinstance(value, str):
+ text = repr(value)
+ if text[0] != "u":
+ try:
+ value.encode("latin1")
+ except UnicodeEncodeError:
+ text = "u" + text
+ buf.write(text)
+ elif isinstance(value, float | complex | str | bytes):
+ buf.write(repr(value))
+ elif isinstance(value, list):
+ buf.write("[")
+ first = True
+ for item in value:
+ if not first:
+ buf.write(",")
+ else:
+ first = False
+ stringize(item)
+ buf.write("]")
+ elif isinstance(value, tuple):
+ if len(value) > 1:
+ buf.write("(")
+ first = True
+ for item in value:
+ if not first:
+ buf.write(",")
+ else:
+ first = False
+ stringize(item)
+ buf.write(")")
+ elif value:
+ buf.write("(")
+ stringize(value[0])
+ buf.write(",)")
+ else:
+ buf.write("()")
+ elif isinstance(value, dict):
+ buf.write("{")
+ first = True
+ for key, value in value.items():
+ if not first:
+ buf.write(",")
+ else:
+ first = False
+ stringize(key)
+ buf.write(":")
+ stringize(value)
+ buf.write("}")
+ elif isinstance(value, set):
+ buf.write("{")
+ first = True
+ for item in value:
+ if not first:
+ buf.write(",")
+ else:
+ first = False
+ stringize(item)
+ buf.write("}")
+ else:
+ msg = f"{value!r} cannot be converted into a Python literal"
+ raise ValueError(msg)
+
+ buf = StringIO()
+ stringize(value)
+ return buf.getvalue()
+
+
+def generate_gml(G, stringizer=None):
+ r"""Generate a single entry of the graph `G` in GML format.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ The graph to be converted to GML.
+
+ stringizer : callable, optional
+ A `stringizer` which converts non-int/non-float/non-dict values into
+ strings. If it cannot convert a value into a string, it should raise a
+ `ValueError` to indicate that. Default value: None.
+
+ Returns
+ -------
+ lines: generator of strings
+ Lines of GML data. Newlines are not appended.
+
+ Raises
+ ------
+ NetworkXError
+ If `stringizer` cannot convert a value into a string, or the value to
+ convert is not a string while `stringizer` is None.
+
+ See Also
+ --------
+ literal_stringizer
+
+ Notes
+ -----
+ Graph attributes named 'directed', 'multigraph', 'node' or
+ 'edge', node attributes named 'id' or 'label', edge attributes
+ named 'source' or 'target' (or 'key' if `G` is a multigraph)
+ are ignored because these attribute names are used to encode the graph
+ structure.
+
+ GML files are stored using a 7-bit ASCII encoding with any extended
+ ASCII characters (iso8859-1) appearing as HTML character entities.
+ Without specifying a `stringizer`/`destringizer`, the code is capable of
+ writing `int`/`float`/`str`/`dict`/`list` data as required by the GML
+ specification. For writing other data types, and for reading data other
+ than `str` you need to explicitly supply a `stringizer`/`destringizer`.
+
+ For additional documentation on the GML file format, please see the
+ `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_.
+
+ See the module docstring :mod:`networkx.readwrite.gml` for more details.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_node("1")
+ >>> print("\n".join(nx.generate_gml(G)))
+ graph [
+ node [
+ id 0
+ label "1"
+ ]
+ ]
+ >>> G = nx.MultiGraph([("a", "b"), ("a", "b")])
+ >>> print("\n".join(nx.generate_gml(G)))
+ graph [
+ multigraph 1
+ node [
+ id 0
+ label "a"
+ ]
+ node [
+ id 1
+ label "b"
+ ]
+ edge [
+ source 0
+ target 1
+ key 0
+ ]
+ edge [
+ source 0
+ target 1
+ key 1
+ ]
+ ]
+ """
+ valid_keys = re.compile("^[A-Za-z][0-9A-Za-z_]*$")
+
+ def stringize(key, value, ignored_keys, indent, in_list=False):
+ if not isinstance(key, str):
+ raise NetworkXError(f"{key!r} is not a string")
+ if not valid_keys.match(key):
+ raise NetworkXError(f"{key!r} is not a valid key")
+ if not isinstance(key, str):
+ key = str(key)
+ if key not in ignored_keys:
+ if isinstance(value, int | bool):
+ if key == "label":
+ yield indent + key + ' "' + str(value) + '"'
+ elif value is True:
+ # python bool is an instance of int
+ yield indent + key + " 1"
+ elif value is False:
+ yield indent + key + " 0"
+ # GML only supports signed 32-bit integers
+ elif value < -(2**31) or value >= 2**31:
+ yield indent + key + ' "' + str(value) + '"'
+ else:
+ yield indent + key + " " + str(value)
+ elif isinstance(value, float):
+ text = repr(value).upper()
+ # GML matches INF to keys, so prepend + to INF. Use repr(float(*))
+ # instead of string literal to future proof against changes to repr.
+ if text == repr(float("inf")).upper():
+ text = "+" + text
+ else:
+ # GML requires that a real literal contain a decimal point, but
+ # repr may not output a decimal point when the mantissa is
+ # integral and hence needs fixing.
+ epos = text.rfind("E")
+ if epos != -1 and text.find(".", 0, epos) == -1:
+ text = text[:epos] + "." + text[epos:]
+ if key == "label":
+ yield indent + key + ' "' + text + '"'
+ else:
+ yield indent + key + " " + text
+ elif isinstance(value, dict):
+ yield indent + key + " ["
+ next_indent = indent + " "
+ for key, value in value.items():
+ yield from stringize(key, value, (), next_indent)
+ yield indent + "]"
+ elif isinstance(value, tuple) and key == "label":
+ yield indent + key + f" \"({','.join(repr(v) for v in value)})\""
+ elif isinstance(value, list | tuple) and key != "label" and not in_list:
+ if len(value) == 0:
+ yield indent + key + " " + f'"{value!r}"'
+ if len(value) == 1:
+ yield indent + key + " " + f'"{LIST_START_VALUE}"'
+ for val in value:
+ yield from stringize(key, val, (), indent, True)
+ else:
+ if stringizer:
+ try:
+ value = stringizer(value)
+ except ValueError as err:
+ raise NetworkXError(
+ f"{value!r} cannot be converted into a string"
+ ) from err
+ if not isinstance(value, str):
+ raise NetworkXError(f"{value!r} is not a string")
+ yield indent + key + ' "' + escape(value) + '"'
+
+ multigraph = G.is_multigraph()
+ yield "graph ["
+
+ # Output graph attributes
+ if G.is_directed():
+ yield " directed 1"
+ if multigraph:
+ yield " multigraph 1"
+ ignored_keys = {"directed", "multigraph", "node", "edge"}
+ for attr, value in G.graph.items():
+ yield from stringize(attr, value, ignored_keys, " ")
+
+ # Output node data
+ node_id = dict(zip(G, range(len(G))))
+ ignored_keys = {"id", "label"}
+ for node, attrs in G.nodes.items():
+ yield " node ["
+ yield " id " + str(node_id[node])
+ yield from stringize("label", node, (), " ")
+ for attr, value in attrs.items():
+ yield from stringize(attr, value, ignored_keys, " ")
+ yield " ]"
+
+ # Output edge data
+ ignored_keys = {"source", "target"}
+ kwargs = {"data": True}
+ if multigraph:
+ ignored_keys.add("key")
+ kwargs["keys"] = True
+ for e in G.edges(**kwargs):
+ yield " edge ["
+ yield " source " + str(node_id[e[0]])
+ yield " target " + str(node_id[e[1]])
+ if multigraph:
+ yield from stringize("key", e[2], (), " ")
+ for attr, value in e[-1].items():
+ yield from stringize(attr, value, ignored_keys, " ")
+ yield " ]"
+ yield "]"
+
+
+@open_file(1, mode="wb")
+def write_gml(G, path, stringizer=None):
+ """Write a graph `G` in GML format to the file or file handle `path`.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ The graph to be converted to GML.
+
+ path : filename or filehandle
+ The filename or filehandle to write. Files whose names end with .gz or
+ .bz2 will be compressed.
+
+ stringizer : callable, optional
+ A `stringizer` which converts non-int/non-float/non-dict values into
+ strings. If it cannot convert a value into a string, it should raise a
+ `ValueError` to indicate that. Default value: None.
+
+ Raises
+ ------
+ NetworkXError
+ If `stringizer` cannot convert a value into a string, or the value to
+ convert is not a string while `stringizer` is None.
+
+ See Also
+ --------
+ read_gml, generate_gml
+ literal_stringizer
+
+ Notes
+ -----
+ Graph attributes named 'directed', 'multigraph', 'node' or
+ 'edge', node attributes named 'id' or 'label', edge attributes
+ named 'source' or 'target' (or 'key' if `G` is a multigraph)
+ are ignored because these attribute names are used to encode the graph
+ structure.
+
+ GML files are stored using a 7-bit ASCII encoding with any extended
+ ASCII characters (iso8859-1) appearing as HTML character entities.
+ Without specifying a `stringizer`/`destringizer`, the code is capable of
+ writing `int`/`float`/`str`/`dict`/`list` data as required by the GML
+ specification. For writing other data types, and for reading data other
+ than `str` you need to explicitly supply a `stringizer`/`destringizer`.
+
+ Note that while we allow non-standard GML to be read from a file, we make
+ sure to write GML format. In particular, underscores are not allowed in
+ attribute names.
+ For additional documentation on the GML file format, please see the
+ `GML url <https://web.archive.org/web/20190207140002/http://www.fim.uni-passau.de/index.php?id=17297&L=1>`_.
+
+ See the module docstring :mod:`networkx.readwrite.gml` for more details.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_gml(G, "test.gml")
+
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ >>> nx.write_gml(G, "test.gml.gz")
+ """
+ for line in generate_gml(G, stringizer):
+ path.write((line + "\n").encode("ascii"))