Download .venv/Lib/site-packages/networkx/readwrite/gml.py from DrDavis/PythonProject1: direct link, hf CLI and curl.
- Browser
- Download file 31.2 kB
-
https://huggingface.co/DrDavis/PythonProject1/resolve/main/.venv/Lib/site-packages/networkx/readwrite/gml.py
- Command line
-
hf download hf://DrDavis/PythonProject1/.venv/Lib/site-packages/networkx/readwrite/gml.py
-
curl -L -o gml.py https://huggingface.co/DrDavis/PythonProject1/resolve/main/.venv/Lib/site-packages/networkx/readwrite/gml.py
31.2 kB
| """ | |
| 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") | |
| 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 | |
| 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 "]" | |
| 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")) | |