aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/readwrite
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/readwrite')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/__init__.py17
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/adjlist.py310
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/edgelist.py489
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/gexf.py1066
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/gml.py879
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/graph6.py417
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/graphml.py1053
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py19
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py156
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py178
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py330
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py78
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py78
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py175
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py48
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py137
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/leda.py108
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py393
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/p2g.py105
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/pajek.py286
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/sparse6.py377
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py262
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py314
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py557
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py744
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py168
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graphml.py1531
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py30
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py62
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py126
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py166
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py1742
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/readwrite/text.py852
35 files changed, 13253 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/__init__.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/__init__.py
new file mode 100644
index 00000000..a805c50a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/__init__.py
@@ -0,0 +1,17 @@
+"""
+A package for reading and writing graphs in various formats.
+
+"""
+
+from networkx.readwrite.adjlist import *
+from networkx.readwrite.multiline_adjlist import *
+from networkx.readwrite.edgelist import *
+from networkx.readwrite.pajek import *
+from networkx.readwrite.leda import *
+from networkx.readwrite.sparse6 import *
+from networkx.readwrite.graph6 import *
+from networkx.readwrite.gml import *
+from networkx.readwrite.graphml import *
+from networkx.readwrite.gexf import *
+from networkx.readwrite.json_graph import *
+from networkx.readwrite.text import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/adjlist.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/adjlist.py
new file mode 100644
index 00000000..768af5ad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/adjlist.py
@@ -0,0 +1,310 @@
+"""
+**************
+Adjacency List
+**************
+Read and write NetworkX graphs as adjacency lists.
+
+Adjacency list format is useful for graphs without data associated
+with nodes or edges and for nodes that can be meaningfully represented
+as strings.
+
+Format
+------
+The adjacency list format consists of lines with node labels. The
+first label in a line is the source node. Further labels in the line
+are considered target nodes and are added to the graph along with an edge
+between the source node and target node.
+
+The graph with edges a-b, a-c, d-e can be represented as the following
+adjacency list (anything following the # in a line is a comment)::
+
+ a b c # source target target
+ d e
+"""
+
+__all__ = ["generate_adjlist", "write_adjlist", "parse_adjlist", "read_adjlist"]
+
+import networkx as nx
+from networkx.utils import open_file
+
+
+def generate_adjlist(G, delimiter=" "):
+ """Generate a single line of the graph G in adjacency list format.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ delimiter : string, optional
+ Separator for node labels
+
+ Returns
+ -------
+ lines : string
+ Lines of data in adjlist format.
+
+ Examples
+ --------
+ >>> G = nx.lollipop_graph(4, 3)
+ >>> for line in nx.generate_adjlist(G):
+ ... print(line)
+ 0 1 2 3
+ 1 2 3
+ 2 3
+ 3 4
+ 4 5
+ 5 6
+ 6
+
+ See Also
+ --------
+ write_adjlist, read_adjlist
+
+ Notes
+ -----
+ The default `delimiter=" "` will result in unexpected results if node names contain
+ whitespace characters. To avoid this problem, specify an alternate delimiter when spaces are
+ valid in node names.
+
+ NB: This option is not available for data that isn't user-generated.
+
+ """
+ directed = G.is_directed()
+ seen = set()
+ for s, nbrs in G.adjacency():
+ line = str(s) + delimiter
+ for t, data in nbrs.items():
+ if not directed and t in seen:
+ continue
+ if G.is_multigraph():
+ for d in data.values():
+ line += str(t) + delimiter
+ else:
+ line += str(t) + delimiter
+ if not directed:
+ seen.add(s)
+ yield line[: -len(delimiter)]
+
+
+@open_file(1, mode="wb")
+def write_adjlist(G, path, comments="#", delimiter=" ", encoding="utf-8"):
+ """Write graph G in single-line adjacency-list format to path.
+
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ path : string or file
+ Filename or file handle for data output.
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ comments : string, optional
+ Marker for comment lines
+
+ delimiter : string, optional
+ Separator for node labels
+
+ encoding : string, optional
+ Text encoding.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_adjlist(G, "test.adjlist")
+
+ The path can be a filehandle or a string with the name of the file. If a
+ filehandle is provided, it has to be opened in 'wb' mode.
+
+ >>> fh = open("test.adjlist", "wb")
+ >>> nx.write_adjlist(G, fh)
+
+ Notes
+ -----
+ The default `delimiter=" "` will result in unexpected results if node names contain
+ whitespace characters. To avoid this problem, specify an alternate delimiter when spaces are
+ valid in node names.
+ NB: This option is not available for data that isn't user-generated.
+
+ This format does not store graph, node, or edge data.
+
+ See Also
+ --------
+ read_adjlist, generate_adjlist
+ """
+ import sys
+ import time
+
+ pargs = comments + " ".join(sys.argv) + "\n"
+ header = (
+ pargs
+ + comments
+ + f" GMT {time.asctime(time.gmtime())}\n"
+ + comments
+ + f" {G.name}\n"
+ )
+ path.write(header.encode(encoding))
+
+ for line in generate_adjlist(G, delimiter):
+ line += "\n"
+ path.write(line.encode(encoding))
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_adjlist(
+ lines, comments="#", delimiter=None, create_using=None, nodetype=None
+):
+ """Parse lines of a graph adjacency list representation.
+
+ Parameters
+ ----------
+ lines : list or iterator of strings
+ Input data in adjlist format
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ nodetype : Python type, optional
+ Convert nodes to this type.
+
+ comments : string, optional
+ Marker for comment lines
+
+ delimiter : string, optional
+ Separator for node labels. The default is whitespace.
+
+ Returns
+ -------
+ G: NetworkX graph
+ The graph corresponding to the lines in adjacency list format.
+
+ Examples
+ --------
+ >>> lines = ["1 2 5", "2 3 4", "3 5", "4", "5"]
+ >>> G = nx.parse_adjlist(lines, nodetype=int)
+ >>> nodes = [1, 2, 3, 4, 5]
+ >>> all(node in G for node in nodes)
+ True
+ >>> edges = [(1, 2), (1, 5), (2, 3), (2, 4), (3, 5)]
+ >>> all((u, v) in G.edges() or (v, u) in G.edges() for (u, v) in edges)
+ True
+
+ See Also
+ --------
+ read_adjlist
+
+ """
+ G = nx.empty_graph(0, create_using)
+ for line in lines:
+ p = line.find(comments)
+ if p >= 0:
+ line = line[:p]
+ if not len(line):
+ continue
+ vlist = line.rstrip("\n").split(delimiter)
+ u = vlist.pop(0)
+ # convert types
+ if nodetype is not None:
+ try:
+ u = nodetype(u)
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert node ({u}) to type {nodetype}"
+ ) from err
+ G.add_node(u)
+ if nodetype is not None:
+ try:
+ vlist = list(map(nodetype, vlist))
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert nodes ({','.join(vlist)}) to type {nodetype}"
+ ) from err
+ G.add_edges_from([(u, v) for v in vlist])
+ return G
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_adjlist(
+ path,
+ comments="#",
+ delimiter=None,
+ create_using=None,
+ nodetype=None,
+ encoding="utf-8",
+):
+ """Read graph in adjacency list format from path.
+
+ Parameters
+ ----------
+ path : string or file
+ Filename or file handle to read.
+ Filenames ending in .gz or .bz2 will be uncompressed.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ nodetype : Python type, optional
+ Convert nodes to this type.
+
+ comments : string, optional
+ Marker for comment lines
+
+ delimiter : string, optional
+ Separator for node labels. The default is whitespace.
+
+ Returns
+ -------
+ G: NetworkX graph
+ The graph corresponding to the lines in adjacency list format.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_adjlist(G, "test.adjlist")
+ >>> G = nx.read_adjlist("test.adjlist")
+
+ The path can be a filehandle or a string with the name of the file. If a
+ filehandle is provided, it has to be opened in 'rb' mode.
+
+ >>> fh = open("test.adjlist", "rb")
+ >>> G = nx.read_adjlist(fh)
+
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ >>> nx.write_adjlist(G, "test.adjlist.gz")
+ >>> G = nx.read_adjlist("test.adjlist.gz")
+
+ The optional nodetype is a function to convert node strings to nodetype.
+
+ For example
+
+ >>> G = nx.read_adjlist("test.adjlist", nodetype=int)
+
+ will attempt to convert all nodes to integer type.
+
+ Since nodes must be hashable, the function nodetype must return hashable
+ types (e.g. int, float, str, frozenset - or tuples of those, etc.)
+
+ The optional create_using parameter indicates the type of NetworkX graph
+ created. The default is `nx.Graph`, an undirected graph.
+ To read the data as a directed graph use
+
+ >>> G = nx.read_adjlist("test.adjlist", create_using=nx.DiGraph)
+
+ Notes
+ -----
+ This format does not store graph or node data.
+
+ See Also
+ --------
+ write_adjlist
+ """
+ lines = (line.decode(encoding) for line in path)
+ return parse_adjlist(
+ lines,
+ comments=comments,
+ delimiter=delimiter,
+ create_using=create_using,
+ nodetype=nodetype,
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/edgelist.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/edgelist.py
new file mode 100644
index 00000000..393b64ed
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/edgelist.py
@@ -0,0 +1,489 @@
+"""
+**********
+Edge Lists
+**********
+Read and write NetworkX graphs as edge lists.
+
+The multi-line adjacency list format is useful for graphs with nodes
+that can be meaningfully represented as strings. With the edgelist
+format simple edge data can be stored but node or graph data is not.
+There is no way of representing isolated nodes unless the node has a
+self-loop edge.
+
+Format
+------
+You can read or write three formats of edge lists with these functions.
+
+Node pairs with no data::
+
+ 1 2
+
+Python dictionary as data::
+
+ 1 2 {'weight':7, 'color':'green'}
+
+Arbitrary data::
+
+ 1 2 7 green
+"""
+
+__all__ = [
+ "generate_edgelist",
+ "write_edgelist",
+ "parse_edgelist",
+ "read_edgelist",
+ "read_weighted_edgelist",
+ "write_weighted_edgelist",
+]
+
+import networkx as nx
+from networkx.utils import open_file
+
+
+def generate_edgelist(G, delimiter=" ", data=True):
+ """Generate a single line of the graph G in edge list format.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ delimiter : string, optional
+ Separator for node labels
+
+ data : bool or list of keys
+ If False generate no edge data. If True use a dictionary
+ representation of edge data. If a list of keys use a list of data
+ values corresponding to the keys.
+
+ Returns
+ -------
+ lines : string
+ Lines of data in adjlist format.
+
+ Examples
+ --------
+ >>> G = nx.lollipop_graph(4, 3)
+ >>> G[1][2]["weight"] = 3
+ >>> G[3][4]["capacity"] = 12
+ >>> for line in nx.generate_edgelist(G, data=False):
+ ... print(line)
+ 0 1
+ 0 2
+ 0 3
+ 1 2
+ 1 3
+ 2 3
+ 3 4
+ 4 5
+ 5 6
+
+ >>> for line in nx.generate_edgelist(G):
+ ... print(line)
+ 0 1 {}
+ 0 2 {}
+ 0 3 {}
+ 1 2 {'weight': 3}
+ 1 3 {}
+ 2 3 {}
+ 3 4 {'capacity': 12}
+ 4 5 {}
+ 5 6 {}
+
+ >>> for line in nx.generate_edgelist(G, data=["weight"]):
+ ... print(line)
+ 0 1
+ 0 2
+ 0 3
+ 1 2 3
+ 1 3
+ 2 3
+ 3 4
+ 4 5
+ 5 6
+
+ See Also
+ --------
+ write_adjlist, read_adjlist
+ """
+ if data is True:
+ for u, v, d in G.edges(data=True):
+ e = u, v, dict(d)
+ yield delimiter.join(map(str, e))
+ elif data is False:
+ for u, v in G.edges(data=False):
+ e = u, v
+ yield delimiter.join(map(str, e))
+ else:
+ for u, v, d in G.edges(data=True):
+ e = [u, v]
+ try:
+ e.extend(d[k] for k in data)
+ except KeyError:
+ pass # missing data for this edge, should warn?
+ yield delimiter.join(map(str, e))
+
+
+@open_file(1, mode="wb")
+def write_edgelist(G, path, comments="#", delimiter=" ", data=True, encoding="utf-8"):
+ """Write graph as a list of edges.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+ path : file or string
+ File or filename to write. If a file is provided, it must be
+ opened in 'wb' mode. Filenames ending in .gz or .bz2 will be compressed.
+ comments : string, optional
+ The character used to indicate the start of a comment
+ delimiter : string, optional
+ The string used to separate values. The default is whitespace.
+ data : bool or list, optional
+ If False write no edge data.
+ If True write a string representation of the edge data dictionary..
+ If a list (or other iterable) is provided, write the keys specified
+ in the list.
+ encoding: string, optional
+ Specify which encoding to use when writing file.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_edgelist(G, "test.edgelist")
+ >>> G = nx.path_graph(4)
+ >>> fh = open("test.edgelist", "wb")
+ >>> nx.write_edgelist(G, fh)
+ >>> nx.write_edgelist(G, "test.edgelist.gz")
+ >>> nx.write_edgelist(G, "test.edgelist.gz", data=False)
+
+ >>> G = nx.Graph()
+ >>> G.add_edge(1, 2, weight=7, color="red")
+ >>> nx.write_edgelist(G, "test.edgelist", data=False)
+ >>> nx.write_edgelist(G, "test.edgelist", data=["color"])
+ >>> nx.write_edgelist(G, "test.edgelist", data=["color", "weight"])
+
+ See Also
+ --------
+ read_edgelist
+ write_weighted_edgelist
+ """
+
+ for line in generate_edgelist(G, delimiter, data):
+ line += "\n"
+ path.write(line.encode(encoding))
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_edgelist(
+ lines, comments="#", delimiter=None, create_using=None, nodetype=None, data=True
+):
+ """Parse lines of an edge list representation of a graph.
+
+ Parameters
+ ----------
+ lines : list or iterator of strings
+ Input data in edgelist format
+ comments : string, optional
+ Marker for comment lines. Default is `'#'`. To specify that no character
+ should be treated as a comment, use ``comments=None``.
+ delimiter : string, optional
+ Separator for node labels. Default is `None`, meaning any whitespace.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ nodetype : Python type, optional
+ Convert nodes to this type. Default is `None`, meaning no conversion is
+ performed.
+ data : bool or list of (label,type) tuples
+ If `False` generate no edge data or if `True` use a dictionary
+ representation of edge data or a list tuples specifying dictionary
+ key names and types for edge data.
+
+ Returns
+ -------
+ G: NetworkX Graph
+ The graph corresponding to lines
+
+ Examples
+ --------
+ Edgelist with no data:
+
+ >>> lines = ["1 2", "2 3", "3 4"]
+ >>> G = nx.parse_edgelist(lines, nodetype=int)
+ >>> list(G)
+ [1, 2, 3, 4]
+ >>> list(G.edges())
+ [(1, 2), (2, 3), (3, 4)]
+
+ Edgelist with data in Python dictionary representation:
+
+ >>> lines = ["1 2 {'weight': 3}", "2 3 {'weight': 27}", "3 4 {'weight': 3.0}"]
+ >>> G = nx.parse_edgelist(lines, nodetype=int)
+ >>> list(G)
+ [1, 2, 3, 4]
+ >>> list(G.edges(data=True))
+ [(1, 2, {'weight': 3}), (2, 3, {'weight': 27}), (3, 4, {'weight': 3.0})]
+
+ Edgelist with data in a list:
+
+ >>> lines = ["1 2 3", "2 3 27", "3 4 3.0"]
+ >>> G = nx.parse_edgelist(lines, nodetype=int, data=(("weight", float),))
+ >>> list(G)
+ [1, 2, 3, 4]
+ >>> list(G.edges(data=True))
+ [(1, 2, {'weight': 3.0}), (2, 3, {'weight': 27.0}), (3, 4, {'weight': 3.0})]
+
+ See Also
+ --------
+ read_weighted_edgelist
+ """
+ from ast import literal_eval
+
+ G = nx.empty_graph(0, create_using)
+ for line in lines:
+ if comments is not None:
+ p = line.find(comments)
+ if p >= 0:
+ line = line[:p]
+ if not line:
+ continue
+ # split line, should have 2 or more
+ s = line.rstrip("\n").split(delimiter)
+ if len(s) < 2:
+ continue
+ u = s.pop(0)
+ v = s.pop(0)
+ d = s
+ if nodetype is not None:
+ try:
+ u = nodetype(u)
+ v = nodetype(v)
+ except Exception as err:
+ raise TypeError(
+ f"Failed to convert nodes {u},{v} to type {nodetype}."
+ ) from err
+
+ if len(d) == 0 or data is False:
+ # no data or data type specified
+ edgedata = {}
+ elif data is True:
+ # no edge types specified
+ try: # try to evaluate as dictionary
+ if delimiter == ",":
+ edgedata_str = ",".join(d)
+ else:
+ edgedata_str = " ".join(d)
+ edgedata = dict(literal_eval(edgedata_str.strip()))
+ except Exception as err:
+ raise TypeError(
+ f"Failed to convert edge data ({d}) to dictionary."
+ ) from err
+ else:
+ # convert edge data to dictionary with specified keys and type
+ if len(d) != len(data):
+ raise IndexError(
+ f"Edge data {d} and data_keys {data} are not the same length"
+ )
+ edgedata = {}
+ for (edge_key, edge_type), edge_value in zip(data, d):
+ try:
+ edge_value = edge_type(edge_value)
+ except Exception as err:
+ raise TypeError(
+ f"Failed to convert {edge_key} data {edge_value} "
+ f"to type {edge_type}."
+ ) from err
+ edgedata.update({edge_key: edge_value})
+ G.add_edge(u, v, **edgedata)
+ return G
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_edgelist(
+ path,
+ comments="#",
+ delimiter=None,
+ create_using=None,
+ nodetype=None,
+ data=True,
+ edgetype=None,
+ encoding="utf-8",
+):
+ """Read a graph from a list of edges.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to read. If a file is provided, it must be
+ opened in 'rb' mode.
+ Filenames ending in .gz or .bz2 will be uncompressed.
+ comments : string, optional
+ The character used to indicate the start of a comment. To specify that
+ no character should be treated as a comment, use ``comments=None``.
+ delimiter : string, optional
+ The string used to separate values. The default is whitespace.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ nodetype : int, float, str, Python type, optional
+ Convert node data from strings to specified type
+ data : bool or list of (label,type) tuples
+ Tuples specifying dictionary key names and types for edge data
+ edgetype : int, float, str, Python type, optional OBSOLETE
+ Convert edge data from strings to specified type and use as 'weight'
+ encoding: string, optional
+ Specify which encoding to use when reading file.
+
+ Returns
+ -------
+ G : graph
+ A networkx Graph or other type specified with create_using
+
+ Examples
+ --------
+ >>> nx.write_edgelist(nx.path_graph(4), "test.edgelist")
+ >>> G = nx.read_edgelist("test.edgelist")
+
+ >>> fh = open("test.edgelist", "rb")
+ >>> G = nx.read_edgelist(fh)
+ >>> fh.close()
+
+ >>> G = nx.read_edgelist("test.edgelist", nodetype=int)
+ >>> G = nx.read_edgelist("test.edgelist", create_using=nx.DiGraph)
+
+ Edgelist with data in a list:
+
+ >>> textline = "1 2 3"
+ >>> fh = open("test.edgelist", "w")
+ >>> d = fh.write(textline)
+ >>> fh.close()
+ >>> G = nx.read_edgelist("test.edgelist", nodetype=int, data=(("weight", float),))
+ >>> list(G)
+ [1, 2]
+ >>> list(G.edges(data=True))
+ [(1, 2, {'weight': 3.0})]
+
+ See parse_edgelist() for more examples of formatting.
+
+ See Also
+ --------
+ parse_edgelist
+ write_edgelist
+
+ Notes
+ -----
+ Since nodes must be hashable, the function nodetype must return hashable
+ types (e.g. int, float, str, frozenset - or tuples of those, etc.)
+ """
+ lines = (line if isinstance(line, str) else line.decode(encoding) for line in path)
+ return parse_edgelist(
+ lines,
+ comments=comments,
+ delimiter=delimiter,
+ create_using=create_using,
+ nodetype=nodetype,
+ data=data,
+ )
+
+
+def write_weighted_edgelist(G, path, comments="#", delimiter=" ", encoding="utf-8"):
+ """Write graph G as a list of edges with numeric weights.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+ path : file or string
+ File or filename to write. If a file is provided, it must be
+ opened in 'wb' mode.
+ Filenames ending in .gz or .bz2 will be compressed.
+ comments : string, optional
+ The character used to indicate the start of a comment
+ delimiter : string, optional
+ The string used to separate values. The default is whitespace.
+ encoding: string, optional
+ Specify which encoding to use when writing file.
+
+ Examples
+ --------
+ >>> G = nx.Graph()
+ >>> G.add_edge(1, 2, weight=7)
+ >>> nx.write_weighted_edgelist(G, "test.weighted.edgelist")
+
+ See Also
+ --------
+ read_edgelist
+ write_edgelist
+ read_weighted_edgelist
+ """
+ write_edgelist(
+ G,
+ path,
+ comments=comments,
+ delimiter=delimiter,
+ data=("weight",),
+ encoding=encoding,
+ )
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_weighted_edgelist(
+ path,
+ comments="#",
+ delimiter=None,
+ create_using=None,
+ nodetype=None,
+ encoding="utf-8",
+):
+ """Read a graph as list of edges with numeric weights.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to read. If a file is provided, it must be
+ opened in 'rb' mode.
+ Filenames ending in .gz or .bz2 will be uncompressed.
+ comments : string, optional
+ The character used to indicate the start of a comment.
+ delimiter : string, optional
+ The string used to separate values. The default is whitespace.
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+ nodetype : int, float, str, Python type, optional
+ Convert node data from strings to specified type
+ encoding: string, optional
+ Specify which encoding to use when reading file.
+
+ Returns
+ -------
+ G : graph
+ A networkx Graph or other type specified with create_using
+
+ Notes
+ -----
+ Since nodes must be hashable, the function nodetype must return hashable
+ types (e.g. int, float, str, frozenset - or tuples of those, etc.)
+
+ Example edgelist file format.
+
+ With numeric edge data::
+
+ # read with
+ # >>> G=nx.read_weighted_edgelist(fh)
+ # source target data
+ a b 1
+ a c 3.14159
+ d e 42
+
+ See Also
+ --------
+ write_weighted_edgelist
+ """
+ return read_edgelist(
+ path,
+ comments=comments,
+ delimiter=delimiter,
+ create_using=create_using,
+ nodetype=nodetype,
+ data=(("weight", float),),
+ encoding=encoding,
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/gexf.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/gexf.py
new file mode 100644
index 00000000..f830dd12
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/gexf.py
@@ -0,0 +1,1066 @@
+"""Read and write graphs in GEXF format.
+
+.. warning::
+ This parser uses the standard xml library present in Python, which is
+ insecure - see :external+python:mod:`xml` for additional information.
+ Only parse GEFX files you trust.
+
+GEXF (Graph Exchange XML Format) is a language for describing complex
+network structures, their associated data and dynamics.
+
+This implementation does not support mixed graphs (directed and
+undirected edges together).
+
+Format
+------
+GEXF is an XML format. See http://gexf.net/schema.html for the
+specification and http://gexf.net/basic.html for examples.
+"""
+
+import itertools
+import time
+from xml.etree.ElementTree import (
+ Element,
+ ElementTree,
+ SubElement,
+ register_namespace,
+ tostring,
+)
+
+import networkx as nx
+from networkx.utils import open_file
+
+__all__ = ["write_gexf", "read_gexf", "relabel_gexf_graph", "generate_gexf"]
+
+
+@open_file(1, mode="wb")
+def write_gexf(G, path, encoding="utf-8", prettyprint=True, version="1.2draft"):
+ """Write G in GEXF format to path.
+
+ "GEXF (Graph Exchange XML Format) is a language for describing
+ complex networks structures, their associated data and dynamics" [1]_.
+
+ Node attributes are checked according to the version of the GEXF
+ schemas used for parameters which are not user defined,
+ e.g. visualization 'viz' [2]_. See example for usage.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+ path : file or string
+ File or file name to write.
+ File names ending in .gz or .bz2 will be compressed.
+ encoding : string (optional, default: 'utf-8')
+ Encoding for text data.
+ prettyprint : bool (optional, default: True)
+ If True use line breaks and indenting in output XML.
+ version: string (optional, default: '1.2draft')
+ The version of GEXF to be used for nodes attributes checking
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_gexf(G, "test.gexf")
+
+ # visualization data
+ >>> G.nodes[0]["viz"] = {"size": 54}
+ >>> G.nodes[0]["viz"]["position"] = {"x": 0, "y": 1}
+ >>> G.nodes[0]["viz"]["color"] = {"r": 0, "g": 0, "b": 256}
+
+
+ Notes
+ -----
+ This implementation does not support mixed graphs (directed and undirected
+ edges together).
+
+ The node id attribute is set to be the string of the node label.
+ If you want to specify an id use set it as node data, e.g.
+ node['a']['id']=1 to set the id of node 'a' to 1.
+
+ References
+ ----------
+ .. [1] GEXF File Format, http://gexf.net/
+ .. [2] GEXF schema, http://gexf.net/schema.html
+ """
+ writer = GEXFWriter(encoding=encoding, prettyprint=prettyprint, version=version)
+ writer.add_graph(G)
+ writer.write(path)
+
+
+def generate_gexf(G, encoding="utf-8", prettyprint=True, version="1.2draft"):
+ """Generate lines of GEXF format representation of G.
+
+ "GEXF (Graph Exchange XML Format) is a language for describing
+ complex networks structures, their associated data and dynamics" [1]_.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+ encoding : string (optional, default: 'utf-8')
+ Encoding for text data.
+ prettyprint : bool (optional, default: True)
+ If True use line breaks and indenting in output XML.
+ version : string (default: 1.2draft)
+ Version of GEFX File Format (see http://gexf.net/schema.html)
+ Supported values: "1.1draft", "1.2draft"
+
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> linefeed = chr(10) # linefeed=\n
+ >>> s = linefeed.join(nx.generate_gexf(G))
+ >>> for line in nx.generate_gexf(G): # doctest: +SKIP
+ ... print(line)
+
+ Notes
+ -----
+ This implementation does not support mixed graphs (directed and undirected
+ edges together).
+
+ The node id attribute is set to be the string of the node label.
+ If you want to specify an id use set it as node data, e.g.
+ node['a']['id']=1 to set the id of node 'a' to 1.
+
+ References
+ ----------
+ .. [1] GEXF File Format, https://gephi.org/gexf/format/
+ """
+ writer = GEXFWriter(encoding=encoding, prettyprint=prettyprint, version=version)
+ writer.add_graph(G)
+ yield from str(writer).splitlines()
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_gexf(path, node_type=None, relabel=False, version="1.2draft"):
+ """Read graph in GEXF format from path.
+
+ "GEXF (Graph Exchange XML Format) is a language for describing
+ complex networks structures, their associated data and dynamics" [1]_.
+
+ Parameters
+ ----------
+ path : file or string
+ File or file name to read.
+ File names ending in .gz or .bz2 will be decompressed.
+ node_type: Python type (default: None)
+ Convert node ids to this type if not None.
+ relabel : bool (default: False)
+ If True relabel the nodes to use the GEXF node "label" attribute
+ instead of the node "id" attribute as the NetworkX node label.
+ version : string (default: 1.2draft)
+ Version of GEFX File Format (see http://gexf.net/schema.html)
+ Supported values: "1.1draft", "1.2draft"
+
+ Returns
+ -------
+ graph: NetworkX graph
+ If no parallel edges are found a Graph or DiGraph is returned.
+ Otherwise a MultiGraph or MultiDiGraph is returned.
+
+ Notes
+ -----
+ This implementation does not support mixed graphs (directed and undirected
+ edges together).
+
+ References
+ ----------
+ .. [1] GEXF File Format, http://gexf.net/
+ """
+ reader = GEXFReader(node_type=node_type, version=version)
+ if relabel:
+ G = relabel_gexf_graph(reader(path))
+ else:
+ G = reader(path)
+ return G
+
+
+class GEXF:
+ versions = {
+ "1.1draft": {
+ "NS_GEXF": "http://www.gexf.net/1.1draft",
+ "NS_VIZ": "http://www.gexf.net/1.1draft/viz",
+ "NS_XSI": "http://www.w3.org/2001/XMLSchema-instance",
+ "SCHEMALOCATION": " ".join(
+ [
+ "http://www.gexf.net/1.1draft",
+ "http://www.gexf.net/1.1draft/gexf.xsd",
+ ]
+ ),
+ "VERSION": "1.1",
+ },
+ "1.2draft": {
+ "NS_GEXF": "http://www.gexf.net/1.2draft",
+ "NS_VIZ": "http://www.gexf.net/1.2draft/viz",
+ "NS_XSI": "http://www.w3.org/2001/XMLSchema-instance",
+ "SCHEMALOCATION": " ".join(
+ [
+ "http://www.gexf.net/1.2draft",
+ "http://www.gexf.net/1.2draft/gexf.xsd",
+ ]
+ ),
+ "VERSION": "1.2",
+ },
+ }
+
+ def construct_types(self):
+ types = [
+ (int, "integer"),
+ (float, "float"),
+ (float, "double"),
+ (bool, "boolean"),
+ (list, "string"),
+ (dict, "string"),
+ (int, "long"),
+ (str, "liststring"),
+ (str, "anyURI"),
+ (str, "string"),
+ ]
+
+ # These additions to types allow writing numpy types
+ try:
+ import numpy as np
+ except ImportError:
+ pass
+ else:
+ # prepend so that python types are created upon read (last entry wins)
+ types = [
+ (np.float64, "float"),
+ (np.float32, "float"),
+ (np.float16, "float"),
+ (np.int_, "int"),
+ (np.int8, "int"),
+ (np.int16, "int"),
+ (np.int32, "int"),
+ (np.int64, "int"),
+ (np.uint8, "int"),
+ (np.uint16, "int"),
+ (np.uint32, "int"),
+ (np.uint64, "int"),
+ (np.int_, "int"),
+ (np.intc, "int"),
+ (np.intp, "int"),
+ ] + types
+
+ self.xml_type = dict(types)
+ self.python_type = dict(reversed(a) for a in types)
+
+ # http://www.w3.org/TR/xmlschema-2/#boolean
+ convert_bool = {
+ "true": True,
+ "false": False,
+ "True": True,
+ "False": False,
+ "0": False,
+ 0: False,
+ "1": True,
+ 1: True,
+ }
+
+ def set_version(self, version):
+ d = self.versions.get(version)
+ if d is None:
+ raise nx.NetworkXError(f"Unknown GEXF version {version}.")
+ self.NS_GEXF = d["NS_GEXF"]
+ self.NS_VIZ = d["NS_VIZ"]
+ self.NS_XSI = d["NS_XSI"]
+ self.SCHEMALOCATION = d["SCHEMALOCATION"]
+ self.VERSION = d["VERSION"]
+ self.version = version
+
+
+class GEXFWriter(GEXF):
+ # class for writing GEXF format files
+ # use write_gexf() function
+ def __init__(
+ self, graph=None, encoding="utf-8", prettyprint=True, version="1.2draft"
+ ):
+ self.construct_types()
+ self.prettyprint = prettyprint
+ self.encoding = encoding
+ self.set_version(version)
+ self.xml = Element(
+ "gexf",
+ {
+ "xmlns": self.NS_GEXF,
+ "xmlns:xsi": self.NS_XSI,
+ "xsi:schemaLocation": self.SCHEMALOCATION,
+ "version": self.VERSION,
+ },
+ )
+
+ # Make meta element a non-graph element
+ # Also add lastmodifieddate as attribute, not tag
+ meta_element = Element("meta")
+ subelement_text = f"NetworkX {nx.__version__}"
+ SubElement(meta_element, "creator").text = subelement_text
+ meta_element.set("lastmodifieddate", time.strftime("%Y-%m-%d"))
+ self.xml.append(meta_element)
+
+ register_namespace("viz", self.NS_VIZ)
+
+ # counters for edge and attribute identifiers
+ self.edge_id = itertools.count()
+ self.attr_id = itertools.count()
+ self.all_edge_ids = set()
+ # default attributes are stored in dictionaries
+ self.attr = {}
+ self.attr["node"] = {}
+ self.attr["edge"] = {}
+ self.attr["node"]["dynamic"] = {}
+ self.attr["node"]["static"] = {}
+ self.attr["edge"]["dynamic"] = {}
+ self.attr["edge"]["static"] = {}
+
+ if graph is not None:
+ self.add_graph(graph)
+
+ def __str__(self):
+ if self.prettyprint:
+ self.indent(self.xml)
+ s = tostring(self.xml).decode(self.encoding)
+ return s
+
+ def add_graph(self, G):
+ # first pass through G collecting edge ids
+ for u, v, dd in G.edges(data=True):
+ eid = dd.get("id")
+ if eid is not None:
+ self.all_edge_ids.add(str(eid))
+ # set graph attributes
+ if G.graph.get("mode") == "dynamic":
+ mode = "dynamic"
+ else:
+ mode = "static"
+ # Add a graph element to the XML
+ if G.is_directed():
+ default = "directed"
+ else:
+ default = "undirected"
+ name = G.graph.get("name", "")
+ graph_element = Element("graph", defaultedgetype=default, mode=mode, name=name)
+ self.graph_element = graph_element
+ self.add_nodes(G, graph_element)
+ self.add_edges(G, graph_element)
+ self.xml.append(graph_element)
+
+ def add_nodes(self, G, graph_element):
+ nodes_element = Element("nodes")
+ for node, data in G.nodes(data=True):
+ node_data = data.copy()
+ node_id = str(node_data.pop("id", node))
+ kw = {"id": node_id}
+ label = str(node_data.pop("label", node))
+ kw["label"] = label
+ try:
+ pid = node_data.pop("pid")
+ kw["pid"] = str(pid)
+ except KeyError:
+ pass
+ try:
+ start = node_data.pop("start")
+ kw["start"] = str(start)
+ self.alter_graph_mode_timeformat(start)
+ except KeyError:
+ pass
+ try:
+ end = node_data.pop("end")
+ kw["end"] = str(end)
+ self.alter_graph_mode_timeformat(end)
+ except KeyError:
+ pass
+ # add node element with attributes
+ node_element = Element("node", **kw)
+ # add node element and attr subelements
+ default = G.graph.get("node_default", {})
+ node_data = self.add_parents(node_element, node_data)
+ if self.VERSION == "1.1":
+ node_data = self.add_slices(node_element, node_data)
+ else:
+ node_data = self.add_spells(node_element, node_data)
+ node_data = self.add_viz(node_element, node_data)
+ node_data = self.add_attributes("node", node_element, node_data, default)
+ nodes_element.append(node_element)
+ graph_element.append(nodes_element)
+
+ def add_edges(self, G, graph_element):
+ def edge_key_data(G):
+ # helper function to unify multigraph and graph edge iterator
+ if G.is_multigraph():
+ for u, v, key, data in G.edges(data=True, keys=True):
+ edge_data = data.copy()
+ edge_data.update(key=key)
+ edge_id = edge_data.pop("id", None)
+ if edge_id is None:
+ edge_id = next(self.edge_id)
+ while str(edge_id) in self.all_edge_ids:
+ edge_id = next(self.edge_id)
+ self.all_edge_ids.add(str(edge_id))
+ yield u, v, edge_id, edge_data
+ else:
+ for u, v, data in G.edges(data=True):
+ edge_data = data.copy()
+ edge_id = edge_data.pop("id", None)
+ if edge_id is None:
+ edge_id = next(self.edge_id)
+ while str(edge_id) in self.all_edge_ids:
+ edge_id = next(self.edge_id)
+ self.all_edge_ids.add(str(edge_id))
+ yield u, v, edge_id, edge_data
+
+ edges_element = Element("edges")
+ for u, v, key, edge_data in edge_key_data(G):
+ kw = {"id": str(key)}
+ try:
+ edge_label = edge_data.pop("label")
+ kw["label"] = str(edge_label)
+ except KeyError:
+ pass
+ try:
+ edge_weight = edge_data.pop("weight")
+ kw["weight"] = str(edge_weight)
+ except KeyError:
+ pass
+ try:
+ edge_type = edge_data.pop("type")
+ kw["type"] = str(edge_type)
+ except KeyError:
+ pass
+ try:
+ start = edge_data.pop("start")
+ kw["start"] = str(start)
+ self.alter_graph_mode_timeformat(start)
+ except KeyError:
+ pass
+ try:
+ end = edge_data.pop("end")
+ kw["end"] = str(end)
+ self.alter_graph_mode_timeformat(end)
+ except KeyError:
+ pass
+ source_id = str(G.nodes[u].get("id", u))
+ target_id = str(G.nodes[v].get("id", v))
+ edge_element = Element("edge", source=source_id, target=target_id, **kw)
+ default = G.graph.get("edge_default", {})
+ if self.VERSION == "1.1":
+ edge_data = self.add_slices(edge_element, edge_data)
+ else:
+ edge_data = self.add_spells(edge_element, edge_data)
+ edge_data = self.add_viz(edge_element, edge_data)
+ edge_data = self.add_attributes("edge", edge_element, edge_data, default)
+ edges_element.append(edge_element)
+ graph_element.append(edges_element)
+
+ def add_attributes(self, node_or_edge, xml_obj, data, default):
+ # Add attrvalues to node or edge
+ attvalues = Element("attvalues")
+ if len(data) == 0:
+ return data
+ mode = "static"
+ for k, v in data.items():
+ # rename generic multigraph key to avoid any name conflict
+ if k == "key":
+ k = "networkx_key"
+ val_type = type(v)
+ if val_type not in self.xml_type:
+ raise TypeError(f"attribute value type is not allowed: {val_type}")
+ if isinstance(v, list):
+ # dynamic data
+ for val, start, end in v:
+ val_type = type(val)
+ if start is not None or end is not None:
+ mode = "dynamic"
+ self.alter_graph_mode_timeformat(start)
+ self.alter_graph_mode_timeformat(end)
+ break
+ attr_id = self.get_attr_id(
+ str(k), self.xml_type[val_type], node_or_edge, default, mode
+ )
+ for val, start, end in v:
+ e = Element("attvalue")
+ e.attrib["for"] = attr_id
+ e.attrib["value"] = str(val)
+ # Handle nan, inf, -inf differently
+ if val_type == float:
+ if e.attrib["value"] == "inf":
+ e.attrib["value"] = "INF"
+ elif e.attrib["value"] == "nan":
+ e.attrib["value"] = "NaN"
+ elif e.attrib["value"] == "-inf":
+ e.attrib["value"] = "-INF"
+ if start is not None:
+ e.attrib["start"] = str(start)
+ if end is not None:
+ e.attrib["end"] = str(end)
+ attvalues.append(e)
+ else:
+ # static data
+ mode = "static"
+ attr_id = self.get_attr_id(
+ str(k), self.xml_type[val_type], node_or_edge, default, mode
+ )
+ e = Element("attvalue")
+ e.attrib["for"] = attr_id
+ if isinstance(v, bool):
+ e.attrib["value"] = str(v).lower()
+ else:
+ e.attrib["value"] = str(v)
+ # Handle float nan, inf, -inf differently
+ if val_type == float:
+ if e.attrib["value"] == "inf":
+ e.attrib["value"] = "INF"
+ elif e.attrib["value"] == "nan":
+ e.attrib["value"] = "NaN"
+ elif e.attrib["value"] == "-inf":
+ e.attrib["value"] = "-INF"
+ attvalues.append(e)
+ xml_obj.append(attvalues)
+ return data
+
+ def get_attr_id(self, title, attr_type, edge_or_node, default, mode):
+ # find the id of the attribute or generate a new id
+ try:
+ return self.attr[edge_or_node][mode][title]
+ except KeyError:
+ # generate new id
+ new_id = str(next(self.attr_id))
+ self.attr[edge_or_node][mode][title] = new_id
+ attr_kwargs = {"id": new_id, "title": title, "type": attr_type}
+ attribute = Element("attribute", **attr_kwargs)
+ # add subelement for data default value if present
+ default_title = default.get(title)
+ if default_title is not None:
+ default_element = Element("default")
+ default_element.text = str(default_title)
+ attribute.append(default_element)
+ # new insert it into the XML
+ attributes_element = None
+ for a in self.graph_element.findall("attributes"):
+ # find existing attributes element by class and mode
+ a_class = a.get("class")
+ a_mode = a.get("mode", "static")
+ if a_class == edge_or_node and a_mode == mode:
+ attributes_element = a
+ if attributes_element is None:
+ # create new attributes element
+ attr_kwargs = {"mode": mode, "class": edge_or_node}
+ attributes_element = Element("attributes", **attr_kwargs)
+ self.graph_element.insert(0, attributes_element)
+ attributes_element.append(attribute)
+ return new_id
+
+ def add_viz(self, element, node_data):
+ viz = node_data.pop("viz", False)
+ if viz:
+ color = viz.get("color")
+ if color is not None:
+ if self.VERSION == "1.1":
+ e = Element(
+ f"{{{self.NS_VIZ}}}color",
+ r=str(color.get("r")),
+ g=str(color.get("g")),
+ b=str(color.get("b")),
+ )
+ else:
+ e = Element(
+ f"{{{self.NS_VIZ}}}color",
+ r=str(color.get("r")),
+ g=str(color.get("g")),
+ b=str(color.get("b")),
+ a=str(color.get("a", 1.0)),
+ )
+ element.append(e)
+
+ size = viz.get("size")
+ if size is not None:
+ e = Element(f"{{{self.NS_VIZ}}}size", value=str(size))
+ element.append(e)
+
+ thickness = viz.get("thickness")
+ if thickness is not None:
+ e = Element(f"{{{self.NS_VIZ}}}thickness", value=str(thickness))
+ element.append(e)
+
+ shape = viz.get("shape")
+ if shape is not None:
+ if shape.startswith("http"):
+ e = Element(
+ f"{{{self.NS_VIZ}}}shape", value="image", uri=str(shape)
+ )
+ else:
+ e = Element(f"{{{self.NS_VIZ}}}shape", value=str(shape))
+ element.append(e)
+
+ position = viz.get("position")
+ if position is not None:
+ e = Element(
+ f"{{{self.NS_VIZ}}}position",
+ x=str(position.get("x")),
+ y=str(position.get("y")),
+ z=str(position.get("z")),
+ )
+ element.append(e)
+ return node_data
+
+ def add_parents(self, node_element, node_data):
+ parents = node_data.pop("parents", False)
+ if parents:
+ parents_element = Element("parents")
+ for p in parents:
+ e = Element("parent")
+ e.attrib["for"] = str(p)
+ parents_element.append(e)
+ node_element.append(parents_element)
+ return node_data
+
+ def add_slices(self, node_or_edge_element, node_or_edge_data):
+ slices = node_or_edge_data.pop("slices", False)
+ if slices:
+ slices_element = Element("slices")
+ for start, end in slices:
+ e = Element("slice", start=str(start), end=str(end))
+ slices_element.append(e)
+ node_or_edge_element.append(slices_element)
+ return node_or_edge_data
+
+ def add_spells(self, node_or_edge_element, node_or_edge_data):
+ spells = node_or_edge_data.pop("spells", False)
+ if spells:
+ spells_element = Element("spells")
+ for start, end in spells:
+ e = Element("spell")
+ if start is not None:
+ e.attrib["start"] = str(start)
+ self.alter_graph_mode_timeformat(start)
+ if end is not None:
+ e.attrib["end"] = str(end)
+ self.alter_graph_mode_timeformat(end)
+ spells_element.append(e)
+ node_or_edge_element.append(spells_element)
+ return node_or_edge_data
+
+ def alter_graph_mode_timeformat(self, start_or_end):
+ # If 'start' or 'end' appears, alter Graph mode to dynamic and
+ # set timeformat
+ if self.graph_element.get("mode") == "static":
+ if start_or_end is not None:
+ if isinstance(start_or_end, str):
+ timeformat = "date"
+ elif isinstance(start_or_end, float):
+ timeformat = "double"
+ elif isinstance(start_or_end, int):
+ timeformat = "long"
+ else:
+ raise nx.NetworkXError(
+ "timeformat should be of the type int, float or str"
+ )
+ self.graph_element.set("timeformat", timeformat)
+ self.graph_element.set("mode", "dynamic")
+
+ def write(self, fh):
+ # Serialize graph G in GEXF to the open fh
+ if self.prettyprint:
+ self.indent(self.xml)
+ document = ElementTree(self.xml)
+ document.write(fh, encoding=self.encoding, xml_declaration=True)
+
+ def indent(self, elem, level=0):
+ # in-place prettyprint formatter
+ i = "\n" + " " * level
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for elem in elem:
+ self.indent(elem, level + 1)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+
+class GEXFReader(GEXF):
+ # Class to read GEXF format files
+ # use read_gexf() function
+ def __init__(self, node_type=None, version="1.2draft"):
+ self.construct_types()
+ self.node_type = node_type
+ # assume simple graph and test for multigraph on read
+ self.simple_graph = True
+ self.set_version(version)
+
+ def __call__(self, stream):
+ self.xml = ElementTree(file=stream)
+ g = self.xml.find(f"{{{self.NS_GEXF}}}graph")
+ if g is not None:
+ return self.make_graph(g)
+ # try all the versions
+ for version in self.versions:
+ self.set_version(version)
+ g = self.xml.find(f"{{{self.NS_GEXF}}}graph")
+ if g is not None:
+ return self.make_graph(g)
+ raise nx.NetworkXError("No <graph> element in GEXF file.")
+
+ def make_graph(self, graph_xml):
+ # start with empty DiGraph or MultiDiGraph
+ edgedefault = graph_xml.get("defaultedgetype", None)
+ if edgedefault == "directed":
+ G = nx.MultiDiGraph()
+ else:
+ G = nx.MultiGraph()
+
+ # graph attributes
+ graph_name = graph_xml.get("name", "")
+ if graph_name != "":
+ G.graph["name"] = graph_name
+ graph_start = graph_xml.get("start")
+ if graph_start is not None:
+ G.graph["start"] = graph_start
+ graph_end = graph_xml.get("end")
+ if graph_end is not None:
+ G.graph["end"] = graph_end
+ graph_mode = graph_xml.get("mode", "")
+ if graph_mode == "dynamic":
+ G.graph["mode"] = "dynamic"
+ else:
+ G.graph["mode"] = "static"
+
+ # timeformat
+ self.timeformat = graph_xml.get("timeformat")
+ if self.timeformat == "date":
+ self.timeformat = "string"
+
+ # node and edge attributes
+ attributes_elements = graph_xml.findall(f"{{{self.NS_GEXF}}}attributes")
+ # dictionaries to hold attributes and attribute defaults
+ node_attr = {}
+ node_default = {}
+ edge_attr = {}
+ edge_default = {}
+ for a in attributes_elements:
+ attr_class = a.get("class")
+ if attr_class == "node":
+ na, nd = self.find_gexf_attributes(a)
+ node_attr.update(na)
+ node_default.update(nd)
+ G.graph["node_default"] = node_default
+ elif attr_class == "edge":
+ ea, ed = self.find_gexf_attributes(a)
+ edge_attr.update(ea)
+ edge_default.update(ed)
+ G.graph["edge_default"] = edge_default
+ else:
+ raise # unknown attribute class
+
+ # Hack to handle Gephi0.7beta bug
+ # add weight attribute
+ ea = {"weight": {"type": "double", "mode": "static", "title": "weight"}}
+ ed = {}
+ edge_attr.update(ea)
+ edge_default.update(ed)
+ G.graph["edge_default"] = edge_default
+
+ # add nodes
+ nodes_element = graph_xml.find(f"{{{self.NS_GEXF}}}nodes")
+ if nodes_element is not None:
+ for node_xml in nodes_element.findall(f"{{{self.NS_GEXF}}}node"):
+ self.add_node(G, node_xml, node_attr)
+
+ # add edges
+ edges_element = graph_xml.find(f"{{{self.NS_GEXF}}}edges")
+ if edges_element is not None:
+ for edge_xml in edges_element.findall(f"{{{self.NS_GEXF}}}edge"):
+ self.add_edge(G, edge_xml, edge_attr)
+
+ # switch to Graph or DiGraph if no parallel edges were found.
+ if self.simple_graph:
+ if G.is_directed():
+ G = nx.DiGraph(G)
+ else:
+ G = nx.Graph(G)
+ return G
+
+ def add_node(self, G, node_xml, node_attr, node_pid=None):
+ # add a single node with attributes to the graph
+
+ # get attributes and subattributues for node
+ data = self.decode_attr_elements(node_attr, node_xml)
+ data = self.add_parents(data, node_xml) # add any parents
+ if self.VERSION == "1.1":
+ data = self.add_slices(data, node_xml) # add slices
+ else:
+ data = self.add_spells(data, node_xml) # add spells
+ data = self.add_viz(data, node_xml) # add viz
+ data = self.add_start_end(data, node_xml) # add start/end
+
+ # find the node id and cast it to the appropriate type
+ node_id = node_xml.get("id")
+ if self.node_type is not None:
+ node_id = self.node_type(node_id)
+
+ # every node should have a label
+ node_label = node_xml.get("label")
+ data["label"] = node_label
+
+ # parent node id
+ node_pid = node_xml.get("pid", node_pid)
+ if node_pid is not None:
+ data["pid"] = node_pid
+
+ # check for subnodes, recursive
+ subnodes = node_xml.find(f"{{{self.NS_GEXF}}}nodes")
+ if subnodes is not None:
+ for node_xml in subnodes.findall(f"{{{self.NS_GEXF}}}node"):
+ self.add_node(G, node_xml, node_attr, node_pid=node_id)
+
+ G.add_node(node_id, **data)
+
+ def add_start_end(self, data, xml):
+ # start and end times
+ ttype = self.timeformat
+ node_start = xml.get("start")
+ if node_start is not None:
+ data["start"] = self.python_type[ttype](node_start)
+ node_end = xml.get("end")
+ if node_end is not None:
+ data["end"] = self.python_type[ttype](node_end)
+ return data
+
+ def add_viz(self, data, node_xml):
+ # add viz element for node
+ viz = {}
+ color = node_xml.find(f"{{{self.NS_VIZ}}}color")
+ if color is not None:
+ if self.VERSION == "1.1":
+ viz["color"] = {
+ "r": int(color.get("r")),
+ "g": int(color.get("g")),
+ "b": int(color.get("b")),
+ }
+ else:
+ viz["color"] = {
+ "r": int(color.get("r")),
+ "g": int(color.get("g")),
+ "b": int(color.get("b")),
+ "a": float(color.get("a", 1)),
+ }
+
+ size = node_xml.find(f"{{{self.NS_VIZ}}}size")
+ if size is not None:
+ viz["size"] = float(size.get("value"))
+
+ thickness = node_xml.find(f"{{{self.NS_VIZ}}}thickness")
+ if thickness is not None:
+ viz["thickness"] = float(thickness.get("value"))
+
+ shape = node_xml.find(f"{{{self.NS_VIZ}}}shape")
+ if shape is not None:
+ viz["shape"] = shape.get("shape")
+ if viz["shape"] == "image":
+ viz["shape"] = shape.get("uri")
+
+ position = node_xml.find(f"{{{self.NS_VIZ}}}position")
+ if position is not None:
+ viz["position"] = {
+ "x": float(position.get("x", 0)),
+ "y": float(position.get("y", 0)),
+ "z": float(position.get("z", 0)),
+ }
+
+ if len(viz) > 0:
+ data["viz"] = viz
+ return data
+
+ def add_parents(self, data, node_xml):
+ parents_element = node_xml.find(f"{{{self.NS_GEXF}}}parents")
+ if parents_element is not None:
+ data["parents"] = []
+ for p in parents_element.findall(f"{{{self.NS_GEXF}}}parent"):
+ parent = p.get("for")
+ data["parents"].append(parent)
+ return data
+
+ def add_slices(self, data, node_or_edge_xml):
+ slices_element = node_or_edge_xml.find(f"{{{self.NS_GEXF}}}slices")
+ if slices_element is not None:
+ data["slices"] = []
+ for s in slices_element.findall(f"{{{self.NS_GEXF}}}slice"):
+ start = s.get("start")
+ end = s.get("end")
+ data["slices"].append((start, end))
+ return data
+
+ def add_spells(self, data, node_or_edge_xml):
+ spells_element = node_or_edge_xml.find(f"{{{self.NS_GEXF}}}spells")
+ if spells_element is not None:
+ data["spells"] = []
+ ttype = self.timeformat
+ for s in spells_element.findall(f"{{{self.NS_GEXF}}}spell"):
+ start = self.python_type[ttype](s.get("start"))
+ end = self.python_type[ttype](s.get("end"))
+ data["spells"].append((start, end))
+ return data
+
+ def add_edge(self, G, edge_element, edge_attr):
+ # add an edge to the graph
+
+ # raise error if we find mixed directed and undirected edges
+ edge_direction = edge_element.get("type")
+ if G.is_directed() and edge_direction == "undirected":
+ raise nx.NetworkXError("Undirected edge found in directed graph.")
+ if (not G.is_directed()) and edge_direction == "directed":
+ raise nx.NetworkXError("Directed edge found in undirected graph.")
+
+ # Get source and target and recast type if required
+ source = edge_element.get("source")
+ target = edge_element.get("target")
+ if self.node_type is not None:
+ source = self.node_type(source)
+ target = self.node_type(target)
+
+ data = self.decode_attr_elements(edge_attr, edge_element)
+ data = self.add_start_end(data, edge_element)
+
+ if self.VERSION == "1.1":
+ data = self.add_slices(data, edge_element) # add slices
+ else:
+ data = self.add_spells(data, edge_element) # add spells
+
+ # GEXF stores edge ids as an attribute
+ # NetworkX uses them as keys in multigraphs
+ # if networkx_key is not specified as an attribute
+ edge_id = edge_element.get("id")
+ if edge_id is not None:
+ data["id"] = edge_id
+
+ # check if there is a 'multigraph_key' and use that as edge_id
+ multigraph_key = data.pop("networkx_key", None)
+ if multigraph_key is not None:
+ edge_id = multigraph_key
+
+ weight = edge_element.get("weight")
+ if weight is not None:
+ data["weight"] = float(weight)
+
+ edge_label = edge_element.get("label")
+ if edge_label is not None:
+ data["label"] = edge_label
+
+ if G.has_edge(source, target):
+ # seen this edge before - this is a multigraph
+ self.simple_graph = False
+ G.add_edge(source, target, key=edge_id, **data)
+ if edge_direction == "mutual":
+ G.add_edge(target, source, key=edge_id, **data)
+
+ def decode_attr_elements(self, gexf_keys, obj_xml):
+ # Use the key information to decode the attr XML
+ attr = {}
+ # look for outer '<attvalues>' element
+ attr_element = obj_xml.find(f"{{{self.NS_GEXF}}}attvalues")
+ if attr_element is not None:
+ # loop over <attvalue> elements
+ for a in attr_element.findall(f"{{{self.NS_GEXF}}}attvalue"):
+ key = a.get("for") # for is required
+ try: # should be in our gexf_keys dictionary
+ title = gexf_keys[key]["title"]
+ except KeyError as err:
+ raise nx.NetworkXError(f"No attribute defined for={key}.") from err
+ atype = gexf_keys[key]["type"]
+ value = a.get("value")
+ if atype == "boolean":
+ value = self.convert_bool[value]
+ else:
+ value = self.python_type[atype](value)
+ if gexf_keys[key]["mode"] == "dynamic":
+ # for dynamic graphs use list of three-tuples
+ # [(value1,start1,end1), (value2,start2,end2), etc]
+ ttype = self.timeformat
+ start = self.python_type[ttype](a.get("start"))
+ end = self.python_type[ttype](a.get("end"))
+ if title in attr:
+ attr[title].append((value, start, end))
+ else:
+ attr[title] = [(value, start, end)]
+ else:
+ # for static graphs just assign the value
+ attr[title] = value
+ return attr
+
+ def find_gexf_attributes(self, attributes_element):
+ # Extract all the attributes and defaults
+ attrs = {}
+ defaults = {}
+ mode = attributes_element.get("mode")
+ for k in attributes_element.findall(f"{{{self.NS_GEXF}}}attribute"):
+ attr_id = k.get("id")
+ title = k.get("title")
+ atype = k.get("type")
+ attrs[attr_id] = {"title": title, "type": atype, "mode": mode}
+ # check for the 'default' subelement of key element and add
+ default = k.find(f"{{{self.NS_GEXF}}}default")
+ if default is not None:
+ if atype == "boolean":
+ value = self.convert_bool[default.text]
+ else:
+ value = self.python_type[atype](default.text)
+ defaults[title] = value
+ return attrs, defaults
+
+
+def relabel_gexf_graph(G):
+ """Relabel graph using "label" node keyword for node label.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph read from GEXF data
+
+ Returns
+ -------
+ H : graph
+ A NetworkX graph with relabeled nodes
+
+ Raises
+ ------
+ NetworkXError
+ If node labels are missing or not unique while relabel=True.
+
+ Notes
+ -----
+ This function relabels the nodes in a NetworkX graph with the
+ "label" attribute. It also handles relabeling the specific GEXF
+ node attributes "parents", and "pid".
+ """
+ # build mapping of node labels, do some error checking
+ try:
+ mapping = [(u, G.nodes[u]["label"]) for u in G]
+ except KeyError as err:
+ raise nx.NetworkXError(
+ "Failed to relabel nodes: missing node labels found. Use relabel=False."
+ ) from err
+ x, y = zip(*mapping)
+ if len(set(y)) != len(G):
+ raise nx.NetworkXError(
+ "Failed to relabel nodes: "
+ "duplicate node labels found. "
+ "Use relabel=False."
+ )
+ mapping = dict(mapping)
+ H = nx.relabel_nodes(G, mapping)
+ # relabel attributes
+ for n in G:
+ m = mapping[n]
+ H.nodes[m]["id"] = n
+ H.nodes[m].pop("label")
+ if "pid" in H.nodes[m]:
+ H.nodes[m]["pid"] = mapping[G.nodes[n]["pid"]]
+ if "parents" in H.nodes[m]:
+ H.nodes[m]["parents"] = [mapping[p] for p in G.nodes[n]["parents"]]
+ return H
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"))
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/graph6.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/graph6.py
new file mode 100644
index 00000000..4ff2f93c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/graph6.py
@@ -0,0 +1,417 @@
+# Original author: D. Eppstein, UC Irvine, August 12, 2003.
+# The original code at http://www.ics.uci.edu/~eppstein/PADS/ is public domain.
+"""Functions for reading and writing graphs in the *graph6* format.
+
+The *graph6* file format is suitable for small graphs or large dense
+graphs. For large sparse graphs, use the *sparse6* format.
+
+For more information, see the `graph6`_ homepage.
+
+.. _graph6: http://users.cecs.anu.edu.au/~bdm/data/formats.html
+
+"""
+
+from itertools import islice
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.utils import not_implemented_for, open_file
+
+__all__ = ["from_graph6_bytes", "read_graph6", "to_graph6_bytes", "write_graph6"]
+
+
+def _generate_graph6_bytes(G, nodes, header):
+ """Yield bytes in the graph6 encoding of a graph.
+
+ `G` is an undirected simple graph. `nodes` is the list of nodes for
+ which the node-induced subgraph will be encoded; if `nodes` is the
+ list of all nodes in the graph, the entire graph will be
+ encoded. `header` is a Boolean that specifies whether to generate
+ the header ``b'>>graph6<<'`` before the remaining data.
+
+ This function generates `bytes` objects in the following order:
+
+ 1. the header (if requested),
+ 2. the encoding of the number of nodes,
+ 3. each character, one-at-a-time, in the encoding of the requested
+ node-induced subgraph,
+ 4. a newline character.
+
+ This function raises :exc:`ValueError` if the graph is too large for
+ the graph6 format (that is, greater than ``2 ** 36`` nodes).
+
+ """
+ n = len(G)
+ if n >= 2**36:
+ raise ValueError(
+ "graph6 is only defined if number of nodes is less than 2 ** 36"
+ )
+ if header:
+ yield b">>graph6<<"
+ for d in n_to_data(n):
+ yield str.encode(chr(d + 63))
+ # This generates the same as `(v in G[u] for u, v in combinations(G, 2))`,
+ # but in "column-major" order instead of "row-major" order.
+ bits = (nodes[j] in G[nodes[i]] for j in range(1, n) for i in range(j))
+ chunk = list(islice(bits, 6))
+ while chunk:
+ d = sum(b << 5 - i for i, b in enumerate(chunk))
+ yield str.encode(chr(d + 63))
+ chunk = list(islice(bits, 6))
+ yield b"\n"
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def from_graph6_bytes(bytes_in):
+ """Read a simple undirected graph in graph6 format from bytes.
+
+ Parameters
+ ----------
+ bytes_in : bytes
+ Data in graph6 format, without a trailing newline.
+
+ Returns
+ -------
+ G : Graph
+
+ Raises
+ ------
+ NetworkXError
+ If bytes_in is unable to be parsed in graph6 format
+
+ ValueError
+ If any character ``c`` in bytes_in does not satisfy
+ ``63 <= ord(c) < 127``.
+
+ Examples
+ --------
+ >>> G = nx.from_graph6_bytes(b"A_")
+ >>> sorted(G.edges())
+ [(0, 1)]
+
+ See Also
+ --------
+ read_graph6, write_graph6
+
+ References
+ ----------
+ .. [1] Graph6 specification
+ <http://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+
+ def bits():
+ """Returns sequence of individual bits from 6-bit-per-value
+ list of data values."""
+ for d in data:
+ for i in [5, 4, 3, 2, 1, 0]:
+ yield (d >> i) & 1
+
+ if bytes_in.startswith(b">>graph6<<"):
+ bytes_in = bytes_in[10:]
+
+ data = [c - 63 for c in bytes_in]
+ if any(c > 63 for c in data):
+ raise ValueError("each input character must be in range(63, 127)")
+
+ n, data = data_to_n(data)
+ nd = (n * (n - 1) // 2 + 5) // 6
+ if len(data) != nd:
+ raise NetworkXError(
+ f"Expected {n * (n - 1) // 2} bits but got {len(data) * 6} in graph6"
+ )
+
+ G = nx.Graph()
+ G.add_nodes_from(range(n))
+ for (i, j), b in zip(((i, j) for j in range(1, n) for i in range(j)), bits()):
+ if b:
+ G.add_edge(i, j)
+
+ return G
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+def to_graph6_bytes(G, nodes=None, header=True):
+ """Convert a simple undirected graph to bytes in graph6 format.
+
+ Parameters
+ ----------
+ G : Graph (undirected)
+
+ nodes: list or iterable
+ Nodes are labeled 0...n-1 in the order provided. If None the ordering
+ given by ``G.nodes()`` is used.
+
+ header: bool
+ If True add '>>graph6<<' bytes to head of data.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the graph is directed or is a multigraph.
+
+ ValueError
+ If the graph has at least ``2 ** 36`` nodes; the graph6 format
+ is only defined for graphs of order less than ``2 ** 36``.
+
+ Examples
+ --------
+ >>> nx.to_graph6_bytes(nx.path_graph(2))
+ b'>>graph6<<A_\\n'
+
+ See Also
+ --------
+ from_graph6_bytes, read_graph6, write_graph6_bytes
+
+ Notes
+ -----
+ The returned bytes end with a newline character.
+
+ The format does not support edge or node labels, parallel edges or
+ self loops. If self loops are present they are silently ignored.
+
+ References
+ ----------
+ .. [1] Graph6 specification
+ <http://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ if nodes is not None:
+ G = G.subgraph(nodes)
+ H = nx.convert_node_labels_to_integers(G)
+ nodes = sorted(H.nodes())
+ return b"".join(_generate_graph6_bytes(H, nodes, header))
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_graph6(path):
+ """Read simple undirected graphs in graph6 format from path.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to write.
+
+ Returns
+ -------
+ G : Graph or list of Graphs
+ If the file contains multiple lines then a list of graphs is returned
+
+ Raises
+ ------
+ NetworkXError
+ If the string is unable to be parsed in graph6 format
+
+ Examples
+ --------
+ You can read a graph6 file by giving the path to the file::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile(delete=False) as f:
+ ... _ = f.write(b">>graph6<<A_\\n")
+ ... _ = f.seek(0)
+ ... G = nx.read_graph6(f.name)
+ >>> list(G.edges())
+ [(0, 1)]
+
+ You can also read a graph6 file by giving an open file-like object::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile() as f:
+ ... _ = f.write(b">>graph6<<A_\\n")
+ ... _ = f.seek(0)
+ ... G = nx.read_graph6(f)
+ >>> list(G.edges())
+ [(0, 1)]
+
+ See Also
+ --------
+ from_graph6_bytes, write_graph6
+
+ References
+ ----------
+ .. [1] Graph6 specification
+ <http://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ glist = []
+ for line in path:
+ line = line.strip()
+ if not len(line):
+ continue
+ glist.append(from_graph6_bytes(line))
+ if len(glist) == 1:
+ return glist[0]
+ else:
+ return glist
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@open_file(1, mode="wb")
+def write_graph6(G, path, nodes=None, header=True):
+ """Write a simple undirected graph to a path in graph6 format.
+
+ Parameters
+ ----------
+ G : Graph (undirected)
+
+ path : str
+ The path naming the file to which to write the graph.
+
+ nodes: list or iterable
+ Nodes are labeled 0...n-1 in the order provided. If None the ordering
+ given by ``G.nodes()`` is used.
+
+ header: bool
+ If True add '>>graph6<<' string to head of data
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the graph is directed or is a multigraph.
+
+ ValueError
+ If the graph has at least ``2 ** 36`` nodes; the graph6 format
+ is only defined for graphs of order less than ``2 ** 36``.
+
+ Examples
+ --------
+ You can write a graph6 file by giving the path to a file::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile(delete=False) as f:
+ ... nx.write_graph6(nx.path_graph(2), f.name)
+ ... _ = f.seek(0)
+ ... print(f.read())
+ b'>>graph6<<A_\\n'
+
+ See Also
+ --------
+ from_graph6_bytes, read_graph6
+
+ Notes
+ -----
+ The function writes a newline character after writing the encoding
+ of the graph.
+
+ The format does not support edge or node labels, parallel edges or
+ self loops. If self loops are present they are silently ignored.
+
+ References
+ ----------
+ .. [1] Graph6 specification
+ <http://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ return write_graph6_file(G, path, nodes=nodes, header=header)
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+def write_graph6_file(G, f, nodes=None, header=True):
+ """Write a simple undirected graph to a file-like object in graph6 format.
+
+ Parameters
+ ----------
+ G : Graph (undirected)
+
+ f : file-like object
+ The file to write.
+
+ nodes: list or iterable
+ Nodes are labeled 0...n-1 in the order provided. If None the ordering
+ given by ``G.nodes()`` is used.
+
+ header: bool
+ If True add '>>graph6<<' string to head of data
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the graph is directed or is a multigraph.
+
+ ValueError
+ If the graph has at least ``2 ** 36`` nodes; the graph6 format
+ is only defined for graphs of order less than ``2 ** 36``.
+
+ Examples
+ --------
+ You can write a graph6 file by giving an open file-like object::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile() as f:
+ ... nx.write_graph6(nx.path_graph(2), f)
+ ... _ = f.seek(0)
+ ... print(f.read())
+ b'>>graph6<<A_\\n'
+
+ See Also
+ --------
+ from_graph6_bytes, read_graph6
+
+ Notes
+ -----
+ The function writes a newline character after writing the encoding
+ of the graph.
+
+ The format does not support edge or node labels, parallel edges or
+ self loops. If self loops are present they are silently ignored.
+
+ References
+ ----------
+ .. [1] Graph6 specification
+ <http://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ if nodes is not None:
+ G = G.subgraph(nodes)
+ H = nx.convert_node_labels_to_integers(G)
+ nodes = sorted(H.nodes())
+ for b in _generate_graph6_bytes(H, nodes, header):
+ f.write(b)
+
+
+def data_to_n(data):
+ """Read initial one-, four- or eight-unit value from graph6
+ integer sequence.
+
+ Return (value, rest of seq.)"""
+ if data[0] <= 62:
+ return data[0], data[1:]
+ if data[1] <= 62:
+ return (data[1] << 12) + (data[2] << 6) + data[3], data[4:]
+ return (
+ (data[2] << 30)
+ + (data[3] << 24)
+ + (data[4] << 18)
+ + (data[5] << 12)
+ + (data[6] << 6)
+ + data[7],
+ data[8:],
+ )
+
+
+def n_to_data(n):
+ """Convert an integer to one-, four- or eight-unit graph6 sequence.
+
+ This function is undefined if `n` is not in ``range(2 ** 36)``.
+
+ """
+ if n <= 62:
+ return [n]
+ elif n <= 258047:
+ return [63, (n >> 12) & 0x3F, (n >> 6) & 0x3F, n & 0x3F]
+ else: # if n <= 68719476735:
+ return [
+ 63,
+ 63,
+ (n >> 30) & 0x3F,
+ (n >> 24) & 0x3F,
+ (n >> 18) & 0x3F,
+ (n >> 12) & 0x3F,
+ (n >> 6) & 0x3F,
+ n & 0x3F,
+ ]
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/graphml.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/graphml.py
new file mode 100644
index 00000000..7d0a1da0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/graphml.py
@@ -0,0 +1,1053 @@
+"""
+*******
+GraphML
+*******
+Read and write graphs in GraphML format.
+
+.. warning::
+
+ This parser uses the standard xml library present in Python, which is
+ insecure - see :external+python:mod:`xml` for additional information.
+ Only parse GraphML files you trust.
+
+This implementation does not support mixed graphs (directed and unidirected
+edges together), hyperedges, nested graphs, or ports.
+
+"GraphML is a comprehensive and easy-to-use file format for graphs. It
+consists of a language core to describe the structural properties of a
+graph and a flexible extension mechanism to add application-specific
+data. Its main features include support of
+
+ * directed, undirected, and mixed graphs,
+ * hypergraphs,
+ * hierarchical graphs,
+ * graphical representations,
+ * references to external data,
+ * application-specific attribute data, and
+ * light-weight parsers.
+
+Unlike many other file formats for graphs, GraphML does not use a
+custom syntax. Instead, it is based on XML and hence ideally suited as
+a common denominator for all kinds of services generating, archiving,
+or processing graphs."
+
+http://graphml.graphdrawing.org/
+
+Format
+------
+GraphML is an XML format. See
+http://graphml.graphdrawing.org/specification.html for the specification and
+http://graphml.graphdrawing.org/primer/graphml-primer.html
+for examples.
+"""
+
+import warnings
+from collections import defaultdict
+
+import networkx as nx
+from networkx.utils import open_file
+
+__all__ = [
+ "write_graphml",
+ "read_graphml",
+ "generate_graphml",
+ "write_graphml_xml",
+ "write_graphml_lxml",
+ "parse_graphml",
+ "GraphMLWriter",
+ "GraphMLReader",
+]
+
+
+@open_file(1, mode="wb")
+def write_graphml_xml(
+ G,
+ path,
+ encoding="utf-8",
+ prettyprint=True,
+ infer_numeric_types=False,
+ named_key_ids=False,
+ edge_id_from_attribute=None,
+):
+ """Write G in GraphML XML format to path
+
+ Parameters
+ ----------
+ G : graph
+ A networkx graph
+ path : file or string
+ File or filename to write.
+ Filenames ending in .gz or .bz2 will be compressed.
+ encoding : string (optional)
+ Encoding for text data.
+ prettyprint : bool (optional)
+ If True use line breaks and indenting in output XML.
+ infer_numeric_types : boolean
+ Determine if numeric types should be generalized.
+ For example, if edges have both int and float 'weight' attributes,
+ we infer in GraphML that both are floats.
+ named_key_ids : bool (optional)
+ If True use attr.name as value for key elements' id attribute.
+ edge_id_from_attribute : dict key (optional)
+ If provided, the graphml edge id is set by looking up the corresponding
+ edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data,
+ the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_graphml(G, "test.graphml")
+
+ Notes
+ -----
+ This implementation does not support mixed graphs (directed
+ and unidirected edges together) hyperedges, nested graphs, or ports.
+ """
+ writer = GraphMLWriter(
+ encoding=encoding,
+ prettyprint=prettyprint,
+ infer_numeric_types=infer_numeric_types,
+ named_key_ids=named_key_ids,
+ edge_id_from_attribute=edge_id_from_attribute,
+ )
+ writer.add_graph_element(G)
+ writer.dump(path)
+
+
+@open_file(1, mode="wb")
+def write_graphml_lxml(
+ G,
+ path,
+ encoding="utf-8",
+ prettyprint=True,
+ infer_numeric_types=False,
+ named_key_ids=False,
+ edge_id_from_attribute=None,
+):
+ """Write G in GraphML XML format to path
+
+ This function uses the LXML framework and should be faster than
+ the version using the xml library.
+
+ Parameters
+ ----------
+ G : graph
+ A networkx graph
+ path : file or string
+ File or filename to write.
+ Filenames ending in .gz or .bz2 will be compressed.
+ encoding : string (optional)
+ Encoding for text data.
+ prettyprint : bool (optional)
+ If True use line breaks and indenting in output XML.
+ infer_numeric_types : boolean
+ Determine if numeric types should be generalized.
+ For example, if edges have both int and float 'weight' attributes,
+ we infer in GraphML that both are floats.
+ named_key_ids : bool (optional)
+ If True use attr.name as value for key elements' id attribute.
+ edge_id_from_attribute : dict key (optional)
+ If provided, the graphml edge id is set by looking up the corresponding
+ edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data,
+ the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_graphml_lxml(G, "fourpath.graphml")
+
+ Notes
+ -----
+ This implementation does not support mixed graphs (directed
+ and unidirected edges together) hyperedges, nested graphs, or ports.
+ """
+ try:
+ import lxml.etree as lxmletree
+ except ImportError:
+ return write_graphml_xml(
+ G,
+ path,
+ encoding,
+ prettyprint,
+ infer_numeric_types,
+ named_key_ids,
+ edge_id_from_attribute,
+ )
+
+ writer = GraphMLWriterLxml(
+ path,
+ graph=G,
+ encoding=encoding,
+ prettyprint=prettyprint,
+ infer_numeric_types=infer_numeric_types,
+ named_key_ids=named_key_ids,
+ edge_id_from_attribute=edge_id_from_attribute,
+ )
+ writer.dump()
+
+
+def generate_graphml(
+ G,
+ encoding="utf-8",
+ prettyprint=True,
+ named_key_ids=False,
+ edge_id_from_attribute=None,
+):
+ """Generate GraphML lines for G
+
+ Parameters
+ ----------
+ G : graph
+ A networkx graph
+ encoding : string (optional)
+ Encoding for text data.
+ prettyprint : bool (optional)
+ If True use line breaks and indenting in output XML.
+ named_key_ids : bool (optional)
+ If True use attr.name as value for key elements' id attribute.
+ edge_id_from_attribute : dict key (optional)
+ If provided, the graphml edge id is set by looking up the corresponding
+ edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data,
+ the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> linefeed = chr(10) # linefeed = \n
+ >>> s = linefeed.join(nx.generate_graphml(G))
+ >>> for line in nx.generate_graphml(G): # doctest: +SKIP
+ ... print(line)
+
+ Notes
+ -----
+ This implementation does not support mixed graphs (directed and unidirected
+ edges together) hyperedges, nested graphs, or ports.
+ """
+ writer = GraphMLWriter(
+ encoding=encoding,
+ prettyprint=prettyprint,
+ named_key_ids=named_key_ids,
+ edge_id_from_attribute=edge_id_from_attribute,
+ )
+ writer.add_graph_element(G)
+ yield from str(writer).splitlines()
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_graphml(path, node_type=str, edge_key_type=int, force_multigraph=False):
+ """Read graph in GraphML format from path.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to write.
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ node_type: Python type (default: str)
+ Convert node ids to this type
+
+ edge_key_type: Python type (default: int)
+ Convert graphml edge ids to this type. Multigraphs use id as edge key.
+ Non-multigraphs add to edge attribute dict with name "id".
+
+ force_multigraph : bool (default: False)
+ If True, return a multigraph with edge keys. If False (the default)
+ return a multigraph when multiedges are in the graph.
+
+ Returns
+ -------
+ graph: NetworkX graph
+ If parallel edges are present or `force_multigraph=True` then
+ a MultiGraph or MultiDiGraph is returned. Otherwise a Graph/DiGraph.
+ The returned graph is directed if the file indicates it should be.
+
+ Notes
+ -----
+ Default node and edge attributes are not propagated to each node and edge.
+ They can be obtained from `G.graph` and applied to node and edge attributes
+ if desired using something like this:
+
+ >>> default_color = G.graph["node_default"]["color"] # doctest: +SKIP
+ >>> for node, data in G.nodes(data=True): # doctest: +SKIP
+ ... if "color" not in data:
+ ... data["color"] = default_color
+ >>> default_color = G.graph["edge_default"]["color"] # doctest: +SKIP
+ >>> for u, v, data in G.edges(data=True): # doctest: +SKIP
+ ... if "color" not in data:
+ ... data["color"] = default_color
+
+ This implementation does not support mixed graphs (directed and unidirected
+ edges together), hypergraphs, nested graphs, or ports.
+
+ For multigraphs the GraphML edge "id" will be used as the edge
+ key. If not specified then they "key" attribute will be used. If
+ there is no "key" attribute a default NetworkX multigraph edge key
+ will be provided.
+
+ Files with the yEd "yfiles" extension can be read. The type of the node's
+ shape is preserved in the `shape_type` node attribute.
+
+ yEd compressed files ("file.graphmlz" extension) can be read by renaming
+ the file to "file.graphml.gz".
+
+ """
+ reader = GraphMLReader(node_type, edge_key_type, force_multigraph)
+ # need to check for multiple graphs
+ glist = list(reader(path=path))
+ if len(glist) == 0:
+ # If no graph comes back, try looking for an incomplete header
+ header = b'<graphml xmlns="http://graphml.graphdrawing.org/xmlns">'
+ path.seek(0)
+ old_bytes = path.read()
+ new_bytes = old_bytes.replace(b"<graphml>", header)
+ glist = list(reader(string=new_bytes))
+ if len(glist) == 0:
+ raise nx.NetworkXError("file not successfully read as graphml")
+ return glist[0]
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_graphml(
+ graphml_string, node_type=str, edge_key_type=int, force_multigraph=False
+):
+ """Read graph in GraphML format from string.
+
+ Parameters
+ ----------
+ graphml_string : string
+ String containing graphml information
+ (e.g., contents of a graphml file).
+
+ node_type: Python type (default: str)
+ Convert node ids to this type
+
+ edge_key_type: Python type (default: int)
+ Convert graphml edge ids to this type. Multigraphs use id as edge key.
+ Non-multigraphs add to edge attribute dict with name "id".
+
+ force_multigraph : bool (default: False)
+ If True, return a multigraph with edge keys. If False (the default)
+ return a multigraph when multiedges are in the graph.
+
+
+ Returns
+ -------
+ graph: NetworkX graph
+ If no parallel edges are found a Graph or DiGraph is returned.
+ Otherwise a MultiGraph or MultiDiGraph is returned.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> linefeed = chr(10) # linefeed = \n
+ >>> s = linefeed.join(nx.generate_graphml(G))
+ >>> H = nx.parse_graphml(s)
+
+ Notes
+ -----
+ Default node and edge attributes are not propagated to each node and edge.
+ They can be obtained from `G.graph` and applied to node and edge attributes
+ if desired using something like this:
+
+ >>> default_color = G.graph["node_default"]["color"] # doctest: +SKIP
+ >>> for node, data in G.nodes(data=True): # doctest: +SKIP
+ ... if "color" not in data:
+ ... data["color"] = default_color
+ >>> default_color = G.graph["edge_default"]["color"] # doctest: +SKIP
+ >>> for u, v, data in G.edges(data=True): # doctest: +SKIP
+ ... if "color" not in data:
+ ... data["color"] = default_color
+
+ This implementation does not support mixed graphs (directed and unidirected
+ edges together), hypergraphs, nested graphs, or ports.
+
+ For multigraphs the GraphML edge "id" will be used as the edge
+ key. If not specified then they "key" attribute will be used. If
+ there is no "key" attribute a default NetworkX multigraph edge key
+ will be provided.
+
+ """
+ reader = GraphMLReader(node_type, edge_key_type, force_multigraph)
+ # need to check for multiple graphs
+ glist = list(reader(string=graphml_string))
+ if len(glist) == 0:
+ # If no graph comes back, try looking for an incomplete header
+ header = '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">'
+ new_string = graphml_string.replace("<graphml>", header)
+ glist = list(reader(string=new_string))
+ if len(glist) == 0:
+ raise nx.NetworkXError("file not successfully read as graphml")
+ return glist[0]
+
+
+class GraphML:
+ NS_GRAPHML = "http://graphml.graphdrawing.org/xmlns"
+ NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
+ # xmlns:y="http://www.yworks.com/xml/graphml"
+ NS_Y = "http://www.yworks.com/xml/graphml"
+ SCHEMALOCATION = " ".join(
+ [
+ "http://graphml.graphdrawing.org/xmlns",
+ "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd",
+ ]
+ )
+
+ def construct_types(self):
+ types = [
+ (int, "integer"), # for Gephi GraphML bug
+ (str, "yfiles"),
+ (str, "string"),
+ (int, "int"),
+ (int, "long"),
+ (float, "float"),
+ (float, "double"),
+ (bool, "boolean"),
+ ]
+
+ # These additions to types allow writing numpy types
+ try:
+ import numpy as np
+ except:
+ pass
+ else:
+ # prepend so that python types are created upon read (last entry wins)
+ types = [
+ (np.float64, "float"),
+ (np.float32, "float"),
+ (np.float16, "float"),
+ (np.int_, "int"),
+ (np.int8, "int"),
+ (np.int16, "int"),
+ (np.int32, "int"),
+ (np.int64, "int"),
+ (np.uint8, "int"),
+ (np.uint16, "int"),
+ (np.uint32, "int"),
+ (np.uint64, "int"),
+ (np.int_, "int"),
+ (np.intc, "int"),
+ (np.intp, "int"),
+ ] + types
+
+ self.xml_type = dict(types)
+ self.python_type = dict(reversed(a) for a in types)
+
+ # This page says that data types in GraphML follow Java(TM).
+ # http://graphml.graphdrawing.org/primer/graphml-primer.html#AttributesDefinition
+ # true and false are the only boolean literals:
+ # http://en.wikibooks.org/wiki/Java_Programming/Literals#Boolean_Literals
+ convert_bool = {
+ # We use data.lower() in actual use.
+ "true": True,
+ "false": False,
+ # Include integer strings for convenience.
+ "0": False,
+ 0: False,
+ "1": True,
+ 1: True,
+ }
+
+ def get_xml_type(self, key):
+ """Wrapper around the xml_type dict that raises a more informative
+ exception message when a user attempts to use data of a type not
+ supported by GraphML."""
+ try:
+ return self.xml_type[key]
+ except KeyError as err:
+ raise TypeError(
+ f"GraphML does not support type {key} as data values."
+ ) from err
+
+
+class GraphMLWriter(GraphML):
+ def __init__(
+ self,
+ graph=None,
+ encoding="utf-8",
+ prettyprint=True,
+ infer_numeric_types=False,
+ named_key_ids=False,
+ edge_id_from_attribute=None,
+ ):
+ self.construct_types()
+ from xml.etree.ElementTree import Element
+
+ self.myElement = Element
+
+ self.infer_numeric_types = infer_numeric_types
+ self.prettyprint = prettyprint
+ self.named_key_ids = named_key_ids
+ self.edge_id_from_attribute = edge_id_from_attribute
+ self.encoding = encoding
+ self.xml = self.myElement(
+ "graphml",
+ {
+ "xmlns": self.NS_GRAPHML,
+ "xmlns:xsi": self.NS_XSI,
+ "xsi:schemaLocation": self.SCHEMALOCATION,
+ },
+ )
+ self.keys = {}
+ self.attributes = defaultdict(list)
+ self.attribute_types = defaultdict(set)
+
+ if graph is not None:
+ self.add_graph_element(graph)
+
+ def __str__(self):
+ from xml.etree.ElementTree import tostring
+
+ if self.prettyprint:
+ self.indent(self.xml)
+ s = tostring(self.xml).decode(self.encoding)
+ return s
+
+ def attr_type(self, name, scope, value):
+ """Infer the attribute type of data named name. Currently this only
+ supports inference of numeric types.
+
+ If self.infer_numeric_types is false, type is used. Otherwise, pick the
+ most general of types found across all values with name and scope. This
+ means edges with data named 'weight' are treated separately from nodes
+ with data named 'weight'.
+ """
+ if self.infer_numeric_types:
+ types = self.attribute_types[(name, scope)]
+
+ if len(types) > 1:
+ types = {self.get_xml_type(t) for t in types}
+ if "string" in types:
+ return str
+ elif "float" in types or "double" in types:
+ return float
+ else:
+ return int
+ else:
+ return list(types)[0]
+ else:
+ return type(value)
+
+ def get_key(self, name, attr_type, scope, default):
+ keys_key = (name, attr_type, scope)
+ try:
+ return self.keys[keys_key]
+ except KeyError:
+ if self.named_key_ids:
+ new_id = name
+ else:
+ new_id = f"d{len(list(self.keys))}"
+
+ self.keys[keys_key] = new_id
+ key_kwargs = {
+ "id": new_id,
+ "for": scope,
+ "attr.name": name,
+ "attr.type": attr_type,
+ }
+ key_element = self.myElement("key", **key_kwargs)
+ # add subelement for data default value if present
+ if default is not None:
+ default_element = self.myElement("default")
+ default_element.text = str(default)
+ key_element.append(default_element)
+ self.xml.insert(0, key_element)
+ return new_id
+
+ def add_data(self, name, element_type, value, scope="all", default=None):
+ """
+ Make a data element for an edge or a node. Keep a log of the
+ type in the keys table.
+ """
+ if element_type not in self.xml_type:
+ raise nx.NetworkXError(
+ f"GraphML writer does not support {element_type} as data values."
+ )
+ keyid = self.get_key(name, self.get_xml_type(element_type), scope, default)
+ data_element = self.myElement("data", key=keyid)
+ data_element.text = str(value)
+ return data_element
+
+ def add_attributes(self, scope, xml_obj, data, default):
+ """Appends attribute data to edges or nodes, and stores type information
+ to be added later. See add_graph_element.
+ """
+ for k, v in data.items():
+ self.attribute_types[(str(k), scope)].add(type(v))
+ self.attributes[xml_obj].append([k, v, scope, default.get(k)])
+
+ def add_nodes(self, G, graph_element):
+ default = G.graph.get("node_default", {})
+ for node, data in G.nodes(data=True):
+ node_element = self.myElement("node", id=str(node))
+ self.add_attributes("node", node_element, data, default)
+ graph_element.append(node_element)
+
+ def add_edges(self, G, graph_element):
+ if G.is_multigraph():
+ for u, v, key, data in G.edges(data=True, keys=True):
+ edge_element = self.myElement(
+ "edge",
+ source=str(u),
+ target=str(v),
+ id=str(data.get(self.edge_id_from_attribute))
+ if self.edge_id_from_attribute
+ and self.edge_id_from_attribute in data
+ else str(key),
+ )
+ default = G.graph.get("edge_default", {})
+ self.add_attributes("edge", edge_element, data, default)
+ graph_element.append(edge_element)
+ else:
+ for u, v, data in G.edges(data=True):
+ if self.edge_id_from_attribute and self.edge_id_from_attribute in data:
+ # select attribute to be edge id
+ edge_element = self.myElement(
+ "edge",
+ source=str(u),
+ target=str(v),
+ id=str(data.get(self.edge_id_from_attribute)),
+ )
+ else:
+ # default: no edge id
+ edge_element = self.myElement("edge", source=str(u), target=str(v))
+ default = G.graph.get("edge_default", {})
+ self.add_attributes("edge", edge_element, data, default)
+ graph_element.append(edge_element)
+
+ def add_graph_element(self, G):
+ """
+ Serialize graph G in GraphML to the stream.
+ """
+ if G.is_directed():
+ default_edge_type = "directed"
+ else:
+ default_edge_type = "undirected"
+
+ graphid = G.graph.pop("id", None)
+ if graphid is None:
+ graph_element = self.myElement("graph", edgedefault=default_edge_type)
+ else:
+ graph_element = self.myElement(
+ "graph", edgedefault=default_edge_type, id=graphid
+ )
+ default = {}
+ data = {
+ k: v
+ for (k, v) in G.graph.items()
+ if k not in ["node_default", "edge_default"]
+ }
+ self.add_attributes("graph", graph_element, data, default)
+ self.add_nodes(G, graph_element)
+ self.add_edges(G, graph_element)
+
+ # self.attributes contains a mapping from XML Objects to a list of
+ # data that needs to be added to them.
+ # We postpone processing in order to do type inference/generalization.
+ # See self.attr_type
+ for xml_obj, data in self.attributes.items():
+ for k, v, scope, default in data:
+ xml_obj.append(
+ self.add_data(
+ str(k), self.attr_type(k, scope, v), str(v), scope, default
+ )
+ )
+ self.xml.append(graph_element)
+
+ def add_graphs(self, graph_list):
+ """Add many graphs to this GraphML document."""
+ for G in graph_list:
+ self.add_graph_element(G)
+
+ def dump(self, stream):
+ from xml.etree.ElementTree import ElementTree
+
+ if self.prettyprint:
+ self.indent(self.xml)
+ document = ElementTree(self.xml)
+ document.write(stream, encoding=self.encoding, xml_declaration=True)
+
+ def indent(self, elem, level=0):
+ # in-place prettyprint formatter
+ i = "\n" + level * " "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for elem in elem:
+ self.indent(elem, level + 1)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+
+class IncrementalElement:
+ """Wrapper for _IncrementalWriter providing an Element like interface.
+
+ This wrapper does not intend to be a complete implementation but rather to
+ deal with those calls used in GraphMLWriter.
+ """
+
+ def __init__(self, xml, prettyprint):
+ self.xml = xml
+ self.prettyprint = prettyprint
+
+ def append(self, element):
+ self.xml.write(element, pretty_print=self.prettyprint)
+
+
+class GraphMLWriterLxml(GraphMLWriter):
+ def __init__(
+ self,
+ path,
+ graph=None,
+ encoding="utf-8",
+ prettyprint=True,
+ infer_numeric_types=False,
+ named_key_ids=False,
+ edge_id_from_attribute=None,
+ ):
+ self.construct_types()
+ import lxml.etree as lxmletree
+
+ self.myElement = lxmletree.Element
+
+ self._encoding = encoding
+ self._prettyprint = prettyprint
+ self.named_key_ids = named_key_ids
+ self.edge_id_from_attribute = edge_id_from_attribute
+ self.infer_numeric_types = infer_numeric_types
+
+ self._xml_base = lxmletree.xmlfile(path, encoding=encoding)
+ self._xml = self._xml_base.__enter__()
+ self._xml.write_declaration()
+
+ # We need to have a xml variable that support insertion. This call is
+ # used for adding the keys to the document.
+ # We will store those keys in a plain list, and then after the graph
+ # element is closed we will add them to the main graphml element.
+ self.xml = []
+ self._keys = self.xml
+ self._graphml = self._xml.element(
+ "graphml",
+ {
+ "xmlns": self.NS_GRAPHML,
+ "xmlns:xsi": self.NS_XSI,
+ "xsi:schemaLocation": self.SCHEMALOCATION,
+ },
+ )
+ self._graphml.__enter__()
+ self.keys = {}
+ self.attribute_types = defaultdict(set)
+
+ if graph is not None:
+ self.add_graph_element(graph)
+
+ def add_graph_element(self, G):
+ """
+ Serialize graph G in GraphML to the stream.
+ """
+ if G.is_directed():
+ default_edge_type = "directed"
+ else:
+ default_edge_type = "undirected"
+
+ graphid = G.graph.pop("id", None)
+ if graphid is None:
+ graph_element = self._xml.element("graph", edgedefault=default_edge_type)
+ else:
+ graph_element = self._xml.element(
+ "graph", edgedefault=default_edge_type, id=graphid
+ )
+
+ # gather attributes types for the whole graph
+ # to find the most general numeric format needed.
+ # Then pass through attributes to create key_id for each.
+ graphdata = {
+ k: v
+ for k, v in G.graph.items()
+ if k not in ("node_default", "edge_default")
+ }
+ node_default = G.graph.get("node_default", {})
+ edge_default = G.graph.get("edge_default", {})
+ # Graph attributes
+ for k, v in graphdata.items():
+ self.attribute_types[(str(k), "graph")].add(type(v))
+ for k, v in graphdata.items():
+ element_type = self.get_xml_type(self.attr_type(k, "graph", v))
+ self.get_key(str(k), element_type, "graph", None)
+ # Nodes and data
+ for node, d in G.nodes(data=True):
+ for k, v in d.items():
+ self.attribute_types[(str(k), "node")].add(type(v))
+ for node, d in G.nodes(data=True):
+ for k, v in d.items():
+ T = self.get_xml_type(self.attr_type(k, "node", v))
+ self.get_key(str(k), T, "node", node_default.get(k))
+ # Edges and data
+ if G.is_multigraph():
+ for u, v, ekey, d in G.edges(keys=True, data=True):
+ for k, v in d.items():
+ self.attribute_types[(str(k), "edge")].add(type(v))
+ for u, v, ekey, d in G.edges(keys=True, data=True):
+ for k, v in d.items():
+ T = self.get_xml_type(self.attr_type(k, "edge", v))
+ self.get_key(str(k), T, "edge", edge_default.get(k))
+ else:
+ for u, v, d in G.edges(data=True):
+ for k, v in d.items():
+ self.attribute_types[(str(k), "edge")].add(type(v))
+ for u, v, d in G.edges(data=True):
+ for k, v in d.items():
+ T = self.get_xml_type(self.attr_type(k, "edge", v))
+ self.get_key(str(k), T, "edge", edge_default.get(k))
+
+ # Now add attribute keys to the xml file
+ for key in self.xml:
+ self._xml.write(key, pretty_print=self._prettyprint)
+
+ # The incremental_writer writes each node/edge as it is created
+ incremental_writer = IncrementalElement(self._xml, self._prettyprint)
+ with graph_element:
+ self.add_attributes("graph", incremental_writer, graphdata, {})
+ self.add_nodes(G, incremental_writer) # adds attributes too
+ self.add_edges(G, incremental_writer) # adds attributes too
+
+ def add_attributes(self, scope, xml_obj, data, default):
+ """Appends attribute data."""
+ for k, v in data.items():
+ data_element = self.add_data(
+ str(k), self.attr_type(str(k), scope, v), str(v), scope, default.get(k)
+ )
+ xml_obj.append(data_element)
+
+ def __str__(self):
+ return object.__str__(self)
+
+ def dump(self, stream=None):
+ self._graphml.__exit__(None, None, None)
+ self._xml_base.__exit__(None, None, None)
+
+
+# default is lxml is present.
+write_graphml = write_graphml_lxml
+
+
+class GraphMLReader(GraphML):
+ """Read a GraphML document. Produces NetworkX graph objects."""
+
+ def __init__(self, node_type=str, edge_key_type=int, force_multigraph=False):
+ self.construct_types()
+ self.node_type = node_type
+ self.edge_key_type = edge_key_type
+ self.multigraph = force_multigraph # If False, test for multiedges
+ self.edge_ids = {} # dict mapping (u,v) tuples to edge id attributes
+
+ def __call__(self, path=None, string=None):
+ from xml.etree.ElementTree import ElementTree, fromstring
+
+ if path is not None:
+ self.xml = ElementTree(file=path)
+ elif string is not None:
+ self.xml = fromstring(string)
+ else:
+ raise ValueError("Must specify either 'path' or 'string' as kwarg")
+ (keys, defaults) = self.find_graphml_keys(self.xml)
+ for g in self.xml.findall(f"{{{self.NS_GRAPHML}}}graph"):
+ yield self.make_graph(g, keys, defaults)
+
+ def make_graph(self, graph_xml, graphml_keys, defaults, G=None):
+ # set default graph type
+ edgedefault = graph_xml.get("edgedefault", None)
+ if G is None:
+ if edgedefault == "directed":
+ G = nx.MultiDiGraph()
+ else:
+ G = nx.MultiGraph()
+ # set defaults for graph attributes
+ G.graph["node_default"] = {}
+ G.graph["edge_default"] = {}
+ for key_id, value in defaults.items():
+ key_for = graphml_keys[key_id]["for"]
+ name = graphml_keys[key_id]["name"]
+ python_type = graphml_keys[key_id]["type"]
+ if key_for == "node":
+ G.graph["node_default"].update({name: python_type(value)})
+ if key_for == "edge":
+ G.graph["edge_default"].update({name: python_type(value)})
+ # hyperedges are not supported
+ hyperedge = graph_xml.find(f"{{{self.NS_GRAPHML}}}hyperedge")
+ if hyperedge is not None:
+ raise nx.NetworkXError("GraphML reader doesn't support hyperedges")
+ # add nodes
+ for node_xml in graph_xml.findall(f"{{{self.NS_GRAPHML}}}node"):
+ self.add_node(G, node_xml, graphml_keys, defaults)
+ # add edges
+ for edge_xml in graph_xml.findall(f"{{{self.NS_GRAPHML}}}edge"):
+ self.add_edge(G, edge_xml, graphml_keys)
+ # add graph data
+ data = self.decode_data_elements(graphml_keys, graph_xml)
+ G.graph.update(data)
+
+ # switch to Graph or DiGraph if no parallel edges were found
+ if self.multigraph:
+ return G
+
+ G = nx.DiGraph(G) if G.is_directed() else nx.Graph(G)
+ # add explicit edge "id" from file as attribute in NX graph.
+ nx.set_edge_attributes(G, values=self.edge_ids, name="id")
+ return G
+
+ def add_node(self, G, node_xml, graphml_keys, defaults):
+ """Add a node to the graph."""
+ # warn on finding unsupported ports tag
+ ports = node_xml.find(f"{{{self.NS_GRAPHML}}}port")
+ if ports is not None:
+ warnings.warn("GraphML port tag not supported.")
+ # find the node by id and cast it to the appropriate type
+ node_id = self.node_type(node_xml.get("id"))
+ # get data/attributes for node
+ data = self.decode_data_elements(graphml_keys, node_xml)
+ G.add_node(node_id, **data)
+ # get child nodes
+ if node_xml.attrib.get("yfiles.foldertype") == "group":
+ graph_xml = node_xml.find(f"{{{self.NS_GRAPHML}}}graph")
+ self.make_graph(graph_xml, graphml_keys, defaults, G)
+
+ def add_edge(self, G, edge_element, graphml_keys):
+ """Add an edge to the graph."""
+ # warn on finding unsupported ports tag
+ ports = edge_element.find(f"{{{self.NS_GRAPHML}}}port")
+ if ports is not None:
+ warnings.warn("GraphML port tag not supported.")
+
+ # raise error if we find mixed directed and undirected edges
+ directed = edge_element.get("directed")
+ if G.is_directed() and directed == "false":
+ msg = "directed=false edge found in directed graph."
+ raise nx.NetworkXError(msg)
+ if (not G.is_directed()) and directed == "true":
+ msg = "directed=true edge found in undirected graph."
+ raise nx.NetworkXError(msg)
+
+ source = self.node_type(edge_element.get("source"))
+ target = self.node_type(edge_element.get("target"))
+ data = self.decode_data_elements(graphml_keys, edge_element)
+ # GraphML stores edge ids as an attribute
+ # NetworkX uses them as keys in multigraphs too if no key
+ # attribute is specified
+ edge_id = edge_element.get("id")
+ if edge_id:
+ # self.edge_ids is used by `make_graph` method for non-multigraphs
+ self.edge_ids[source, target] = edge_id
+ try:
+ edge_id = self.edge_key_type(edge_id)
+ except ValueError: # Could not convert.
+ pass
+ else:
+ edge_id = data.get("key")
+
+ if G.has_edge(source, target):
+ # mark this as a multigraph
+ self.multigraph = True
+
+ # Use add_edges_from to avoid error with add_edge when `'key' in data`
+ # Note there is only one edge here...
+ G.add_edges_from([(source, target, edge_id, data)])
+
+ def decode_data_elements(self, graphml_keys, obj_xml):
+ """Use the key information to decode the data XML if present."""
+ data = {}
+ for data_element in obj_xml.findall(f"{{{self.NS_GRAPHML}}}data"):
+ key = data_element.get("key")
+ try:
+ data_name = graphml_keys[key]["name"]
+ data_type = graphml_keys[key]["type"]
+ except KeyError as err:
+ raise nx.NetworkXError(f"Bad GraphML data: no key {key}") from err
+ text = data_element.text
+ # assume anything with subelements is a yfiles extension
+ if text is not None and len(list(data_element)) == 0:
+ if data_type == bool:
+ # Ignore cases.
+ # http://docs.oracle.com/javase/6/docs/api/java/lang/
+ # Boolean.html#parseBoolean%28java.lang.String%29
+ data[data_name] = self.convert_bool[text.lower()]
+ else:
+ data[data_name] = data_type(text)
+ elif len(list(data_element)) > 0:
+ # Assume yfiles as subelements, try to extract node_label
+ node_label = None
+ # set GenericNode's configuration as shape type
+ gn = data_element.find(f"{{{self.NS_Y}}}GenericNode")
+ if gn is not None:
+ data["shape_type"] = gn.get("configuration")
+ for node_type in ["GenericNode", "ShapeNode", "SVGNode", "ImageNode"]:
+ pref = f"{{{self.NS_Y}}}{node_type}/{{{self.NS_Y}}}"
+ geometry = data_element.find(f"{pref}Geometry")
+ if geometry is not None:
+ data["x"] = geometry.get("x")
+ data["y"] = geometry.get("y")
+ if node_label is None:
+ node_label = data_element.find(f"{pref}NodeLabel")
+ shape = data_element.find(f"{pref}Shape")
+ if shape is not None:
+ data["shape_type"] = shape.get("type")
+ if node_label is not None:
+ data["label"] = node_label.text
+
+ # check all the different types of edges available in yEd.
+ for edge_type in [
+ "PolyLineEdge",
+ "SplineEdge",
+ "QuadCurveEdge",
+ "BezierEdge",
+ "ArcEdge",
+ ]:
+ pref = f"{{{self.NS_Y}}}{edge_type}/{{{self.NS_Y}}}"
+ edge_label = data_element.find(f"{pref}EdgeLabel")
+ if edge_label is not None:
+ break
+ if edge_label is not None:
+ data["label"] = edge_label.text
+ elif text is None:
+ data[data_name] = ""
+ return data
+
+ def find_graphml_keys(self, graph_element):
+ """Extracts all the keys and key defaults from the xml."""
+ graphml_keys = {}
+ graphml_key_defaults = {}
+ for k in graph_element.findall(f"{{{self.NS_GRAPHML}}}key"):
+ attr_id = k.get("id")
+ attr_type = k.get("attr.type")
+ attr_name = k.get("attr.name")
+ yfiles_type = k.get("yfiles.type")
+ if yfiles_type is not None:
+ attr_name = yfiles_type
+ attr_type = "yfiles"
+ if attr_type is None:
+ attr_type = "string"
+ warnings.warn(f"No key type for id {attr_id}. Using string")
+ if attr_name is None:
+ raise nx.NetworkXError(f"Unknown key for id {attr_id}.")
+ graphml_keys[attr_id] = {
+ "name": attr_name,
+ "type": self.python_type[attr_type],
+ "for": k.get("for"),
+ }
+ # check for "default" sub-element of key element
+ default = k.find(f"{{{self.NS_GRAPHML}}}default")
+ if default is not None:
+ # Handle default values identically to data element values
+ python_type = graphml_keys[attr_id]["type"]
+ if python_type == bool:
+ graphml_key_defaults[attr_id] = self.convert_bool[
+ default.text.lower()
+ ]
+ else:
+ graphml_key_defaults[attr_id] = python_type(default.text)
+ return graphml_keys, graphml_key_defaults
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py
new file mode 100644
index 00000000..532c71d7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py
@@ -0,0 +1,19 @@
+"""
+*********
+JSON data
+*********
+Generate and parse JSON serializable data for NetworkX graphs.
+
+These formats are suitable for use with the d3.js examples https://d3js.org/
+
+The three formats that you can generate with NetworkX are:
+
+ - node-link like in the d3.js example https://bl.ocks.org/mbostock/4062045
+ - tree like in the d3.js example https://bl.ocks.org/mbostock/4063550
+ - adjacency like in the d3.js example https://bost.ocks.org/mike/miserables/
+"""
+
+from networkx.readwrite.json_graph.node_link import *
+from networkx.readwrite.json_graph.adjacency import *
+from networkx.readwrite.json_graph.tree import *
+from networkx.readwrite.json_graph.cytoscape import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py
new file mode 100644
index 00000000..3b057475
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py
@@ -0,0 +1,156 @@
+import networkx as nx
+
+__all__ = ["adjacency_data", "adjacency_graph"]
+
+_attrs = {"id": "id", "key": "key"}
+
+
+def adjacency_data(G, attrs=_attrs):
+ """Returns data in adjacency format that is suitable for JSON serialization
+ and use in JavaScript documents.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ attrs : dict
+ A dictionary that contains two keys 'id' and 'key'. The corresponding
+ values provide the attribute names for storing NetworkX-internal graph
+ data. The values should be unique. Default value:
+ :samp:`dict(id='id', key='key')`.
+
+ If some user-defined graph data use these attribute names as data keys,
+ they may be silently dropped.
+
+ Returns
+ -------
+ data : dict
+ A dictionary with adjacency formatted data.
+
+ Raises
+ ------
+ NetworkXError
+ If values in attrs are not unique.
+
+ Examples
+ --------
+ >>> from networkx.readwrite import json_graph
+ >>> G = nx.Graph([(1, 2)])
+ >>> data = json_graph.adjacency_data(G)
+
+ To serialize with json
+
+ >>> import json
+ >>> s = json.dumps(data)
+
+ Notes
+ -----
+ Graph, node, and link attributes will be written when using this format
+ but attribute keys must be strings if you want to serialize the resulting
+ data with JSON.
+
+ The default value of attrs will be changed in a future release of NetworkX.
+
+ See Also
+ --------
+ adjacency_graph, node_link_data, tree_data
+ """
+ multigraph = G.is_multigraph()
+ id_ = attrs["id"]
+ # Allow 'key' to be omitted from attrs if the graph is not a multigraph.
+ key = None if not multigraph else attrs["key"]
+ if id_ == key:
+ raise nx.NetworkXError("Attribute names are not unique.")
+ data = {}
+ data["directed"] = G.is_directed()
+ data["multigraph"] = multigraph
+ data["graph"] = list(G.graph.items())
+ data["nodes"] = []
+ data["adjacency"] = []
+ for n, nbrdict in G.adjacency():
+ data["nodes"].append({**G.nodes[n], id_: n})
+ adj = []
+ if multigraph:
+ for nbr, keys in nbrdict.items():
+ for k, d in keys.items():
+ adj.append({**d, id_: nbr, key: k})
+ else:
+ for nbr, d in nbrdict.items():
+ adj.append({**d, id_: nbr})
+ data["adjacency"].append(adj)
+ return data
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def adjacency_graph(data, directed=False, multigraph=True, attrs=_attrs):
+ """Returns graph from adjacency data format.
+
+ Parameters
+ ----------
+ data : dict
+ Adjacency list formatted graph data
+
+ directed : bool
+ If True, and direction not specified in data, return a directed graph.
+
+ multigraph : bool
+ If True, and multigraph not specified in data, return a multigraph.
+
+ attrs : dict
+ A dictionary that contains two keys 'id' and 'key'. The corresponding
+ values provide the attribute names for storing NetworkX-internal graph
+ data. The values should be unique. Default value:
+ :samp:`dict(id='id', key='key')`.
+
+ Returns
+ -------
+ G : NetworkX graph
+ A NetworkX graph object
+
+ Examples
+ --------
+ >>> from networkx.readwrite import json_graph
+ >>> G = nx.Graph([(1, 2)])
+ >>> data = json_graph.adjacency_data(G)
+ >>> H = json_graph.adjacency_graph(data)
+
+ Notes
+ -----
+ The default value of attrs will be changed in a future release of NetworkX.
+
+ See Also
+ --------
+ adjacency_graph, node_link_data, tree_data
+ """
+ multigraph = data.get("multigraph", multigraph)
+ directed = data.get("directed", directed)
+ if multigraph:
+ graph = nx.MultiGraph()
+ else:
+ graph = nx.Graph()
+ if directed:
+ graph = graph.to_directed()
+ id_ = attrs["id"]
+ # Allow 'key' to be omitted from attrs if the graph is not a multigraph.
+ key = None if not multigraph else attrs["key"]
+ graph.graph = dict(data.get("graph", []))
+ mapping = []
+ for d in data["nodes"]:
+ node_data = d.copy()
+ node = node_data.pop(id_)
+ mapping.append(node)
+ graph.add_node(node)
+ graph.nodes[node].update(node_data)
+ for i, d in enumerate(data["adjacency"]):
+ source = mapping[i]
+ for tdata in d:
+ target_data = tdata.copy()
+ target = target_data.pop(id_)
+ if not multigraph:
+ graph.add_edge(source, target)
+ graph[source][target].update(target_data)
+ else:
+ ky = target_data.pop(key, None)
+ graph.add_edge(source, target, key=ky)
+ graph[source][target][ky].update(target_data)
+ return graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py
new file mode 100644
index 00000000..2f3b2176
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py
@@ -0,0 +1,178 @@
+import networkx as nx
+
+__all__ = ["cytoscape_data", "cytoscape_graph"]
+
+
+def cytoscape_data(G, name="name", ident="id"):
+ """Returns data in Cytoscape JSON format (cyjs).
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+ The graph to convert to cytoscape format
+ name : string
+ A string which is mapped to the 'name' node element in cyjs format.
+ Must not have the same value as `ident`.
+ ident : string
+ A string which is mapped to the 'id' node element in cyjs format.
+ Must not have the same value as `name`.
+
+ Returns
+ -------
+ data: dict
+ A dictionary with cyjs formatted data.
+
+ Raises
+ ------
+ NetworkXError
+ If the values for `name` and `ident` are identical.
+
+ See Also
+ --------
+ cytoscape_graph: convert a dictionary in cyjs format to a graph
+
+ References
+ ----------
+ .. [1] Cytoscape user's manual:
+ http://manual.cytoscape.org/en/stable/index.html
+
+ Examples
+ --------
+ >>> G = nx.path_graph(2)
+ >>> nx.cytoscape_data(G) # doctest: +SKIP
+ {'data': [],
+ 'directed': False,
+ 'multigraph': False,
+ 'elements': {'nodes': [{'data': {'id': '0', 'value': 0, 'name': '0'}},
+ {'data': {'id': '1', 'value': 1, 'name': '1'}}],
+ 'edges': [{'data': {'source': 0, 'target': 1}}]}}
+ """
+ if name == ident:
+ raise nx.NetworkXError("name and ident must be different.")
+
+ jsondata = {"data": list(G.graph.items())}
+ jsondata["directed"] = G.is_directed()
+ jsondata["multigraph"] = G.is_multigraph()
+ jsondata["elements"] = {"nodes": [], "edges": []}
+ nodes = jsondata["elements"]["nodes"]
+ edges = jsondata["elements"]["edges"]
+
+ for i, j in G.nodes.items():
+ n = {"data": j.copy()}
+ n["data"]["id"] = j.get(ident) or str(i)
+ n["data"]["value"] = i
+ n["data"]["name"] = j.get(name) or str(i)
+ nodes.append(n)
+
+ if G.is_multigraph():
+ for e in G.edges(keys=True):
+ n = {"data": G.adj[e[0]][e[1]][e[2]].copy()}
+ n["data"]["source"] = e[0]
+ n["data"]["target"] = e[1]
+ n["data"]["key"] = e[2]
+ edges.append(n)
+ else:
+ for e in G.edges():
+ n = {"data": G.adj[e[0]][e[1]].copy()}
+ n["data"]["source"] = e[0]
+ n["data"]["target"] = e[1]
+ edges.append(n)
+ return jsondata
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def cytoscape_graph(data, name="name", ident="id"):
+ """
+ Create a NetworkX graph from a dictionary in cytoscape JSON format.
+
+ Parameters
+ ----------
+ data : dict
+ A dictionary of data conforming to cytoscape JSON format.
+ name : string
+ A string which is mapped to the 'name' node element in cyjs format.
+ Must not have the same value as `ident`.
+ ident : string
+ A string which is mapped to the 'id' node element in cyjs format.
+ Must not have the same value as `name`.
+
+ Returns
+ -------
+ graph : a NetworkX graph instance
+ The `graph` can be an instance of `Graph`, `DiGraph`, `MultiGraph`, or
+ `MultiDiGraph` depending on the input data.
+
+ Raises
+ ------
+ NetworkXError
+ If the `name` and `ident` attributes are identical.
+
+ See Also
+ --------
+ cytoscape_data: convert a NetworkX graph to a dict in cyjs format
+
+ References
+ ----------
+ .. [1] Cytoscape user's manual:
+ http://manual.cytoscape.org/en/stable/index.html
+
+ Examples
+ --------
+ >>> data_dict = {
+ ... "data": [],
+ ... "directed": False,
+ ... "multigraph": False,
+ ... "elements": {
+ ... "nodes": [
+ ... {"data": {"id": "0", "value": 0, "name": "0"}},
+ ... {"data": {"id": "1", "value": 1, "name": "1"}},
+ ... ],
+ ... "edges": [{"data": {"source": 0, "target": 1}}],
+ ... },
+ ... }
+ >>> G = nx.cytoscape_graph(data_dict)
+ >>> G.name
+ ''
+ >>> G.nodes()
+ NodeView((0, 1))
+ >>> G.nodes(data=True)[0]
+ {'id': '0', 'value': 0, 'name': '0'}
+ >>> G.edges(data=True)
+ EdgeDataView([(0, 1, {'source': 0, 'target': 1})])
+ """
+ if name == ident:
+ raise nx.NetworkXError("name and ident must be different.")
+
+ multigraph = data.get("multigraph")
+ directed = data.get("directed")
+ if multigraph:
+ graph = nx.MultiGraph()
+ else:
+ graph = nx.Graph()
+ if directed:
+ graph = graph.to_directed()
+ graph.graph = dict(data.get("data"))
+ for d in data["elements"]["nodes"]:
+ node_data = d["data"].copy()
+ node = d["data"]["value"]
+
+ if d["data"].get(name):
+ node_data[name] = d["data"].get(name)
+ if d["data"].get(ident):
+ node_data[ident] = d["data"].get(ident)
+
+ graph.add_node(node)
+ graph.nodes[node].update(node_data)
+
+ for d in data["elements"]["edges"]:
+ edge_data = d["data"].copy()
+ sour = d["data"]["source"]
+ targ = d["data"]["target"]
+ if multigraph:
+ key = d["data"].get("key", 0)
+ graph.add_edge(sour, targ, key=key)
+ graph.edges[sour, targ, key].update(edge_data)
+ else:
+ graph.add_edge(sour, targ)
+ graph.edges[sour, targ].update(edge_data)
+ return graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py
new file mode 100644
index 00000000..63ca9789
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py
@@ -0,0 +1,330 @@
+import warnings
+from itertools import count
+
+import networkx as nx
+
+__all__ = ["node_link_data", "node_link_graph"]
+
+
+def _to_tuple(x):
+ """Converts lists to tuples, including nested lists.
+
+ All other non-list inputs are passed through unmodified. This function is
+ intended to be used to convert potentially nested lists from json files
+ into valid nodes.
+
+ Examples
+ --------
+ >>> _to_tuple([1, 2, [3, 4]])
+ (1, 2, (3, 4))
+ """
+ if not isinstance(x, tuple | list):
+ return x
+ return tuple(map(_to_tuple, x))
+
+
+def node_link_data(
+ G,
+ *,
+ source="source",
+ target="target",
+ name="id",
+ key="key",
+ edges=None,
+ nodes="nodes",
+ link=None,
+):
+ """Returns data in node-link format that is suitable for JSON serialization
+ and use in JavaScript documents.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ source : string
+ A string that provides the 'source' attribute name for storing NetworkX-internal graph data.
+ target : string
+ A string that provides the 'target' attribute name for storing NetworkX-internal graph data.
+ name : string
+ A string that provides the 'name' attribute name for storing NetworkX-internal graph data.
+ key : string
+ A string that provides the 'key' attribute name for storing NetworkX-internal graph data.
+ edges : string
+ A string that provides the 'edges' attribute name for storing NetworkX-internal graph data.
+ nodes : string
+ A string that provides the 'nodes' attribute name for storing NetworkX-internal graph data.
+ link : string
+ .. deprecated:: 3.4
+
+ The `link` argument is deprecated and will be removed in version `3.6`.
+ Use the `edges` keyword instead.
+
+ A string that provides the 'edges' attribute name for storing NetworkX-internal graph data.
+
+ Returns
+ -------
+ data : dict
+ A dictionary with node-link formatted data.
+
+ Raises
+ ------
+ NetworkXError
+ If the values of 'source', 'target' and 'key' are not unique.
+
+ Examples
+ --------
+ >>> from pprint import pprint
+ >>> G = nx.Graph([("A", "B")])
+ >>> data1 = nx.node_link_data(G, edges="edges")
+ >>> pprint(data1)
+ {'directed': False,
+ 'edges': [{'source': 'A', 'target': 'B'}],
+ 'graph': {},
+ 'multigraph': False,
+ 'nodes': [{'id': 'A'}, {'id': 'B'}]}
+
+ To serialize with JSON
+
+ >>> import json
+ >>> s1 = json.dumps(data1)
+ >>> s1
+ '{"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "A"}, {"id": "B"}], "edges": [{"source": "A", "target": "B"}]}'
+
+ A graph can also be serialized by passing `node_link_data` as an encoder function.
+
+ >>> s1 = json.dumps(G, default=nx.node_link_data)
+ >>> s1
+ '{"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "A"}, {"id": "B"}], "links": [{"source": "A", "target": "B"}]}'
+
+ The attribute names for storing NetworkX-internal graph data can
+ be specified as keyword options.
+
+ >>> H = nx.gn_graph(2)
+ >>> data2 = nx.node_link_data(
+ ... H, edges="links", source="from", target="to", nodes="vertices"
+ ... )
+ >>> pprint(data2)
+ {'directed': True,
+ 'graph': {},
+ 'links': [{'from': 1, 'to': 0}],
+ 'multigraph': False,
+ 'vertices': [{'id': 0}, {'id': 1}]}
+
+ Notes
+ -----
+ Graph, node, and link attributes are stored in this format. Note that
+ attribute keys will be converted to strings in order to comply with JSON.
+
+ Attribute 'key' is only used for multigraphs.
+
+ To use `node_link_data` in conjunction with `node_link_graph`,
+ the keyword names for the attributes must match.
+
+ See Also
+ --------
+ node_link_graph, adjacency_data, tree_data
+ """
+ # TODO: Remove between the lines when `link` deprecation expires
+ # -------------------------------------------------------------
+ if link is not None:
+ warnings.warn(
+ "Keyword argument 'link' is deprecated; use 'edges' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ if edges is not None:
+ raise ValueError(
+ "Both 'edges' and 'link' are specified. Use 'edges', 'link' will be remove in a future release"
+ )
+ else:
+ edges = link
+ else:
+ if edges is None:
+ warnings.warn(
+ (
+ '\nThe default value will be `edges="edges" in NetworkX 3.6.\n\n'
+ "To make this warning go away, explicitly set the edges kwarg, e.g.:\n\n"
+ ' nx.node_link_data(G, edges="links") to preserve current behavior, or\n'
+ ' nx.node_link_data(G, edges="edges") for forward compatibility.'
+ ),
+ FutureWarning,
+ )
+ edges = "links"
+ # ------------------------------------------------------------
+
+ multigraph = G.is_multigraph()
+
+ # Allow 'key' to be omitted from attrs if the graph is not a multigraph.
+ key = None if not multigraph else key
+ if len({source, target, key}) < 3:
+ raise nx.NetworkXError("Attribute names are not unique.")
+ data = {
+ "directed": G.is_directed(),
+ "multigraph": multigraph,
+ "graph": G.graph,
+ nodes: [{**G.nodes[n], name: n} for n in G],
+ }
+ if multigraph:
+ data[edges] = [
+ {**d, source: u, target: v, key: k}
+ for u, v, k, d in G.edges(keys=True, data=True)
+ ]
+ else:
+ data[edges] = [{**d, source: u, target: v} for u, v, d in G.edges(data=True)]
+ return data
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def node_link_graph(
+ data,
+ directed=False,
+ multigraph=True,
+ *,
+ source="source",
+ target="target",
+ name="id",
+ key="key",
+ edges=None,
+ nodes="nodes",
+ link=None,
+):
+ """Returns graph from node-link data format.
+
+ Useful for de-serialization from JSON.
+
+ Parameters
+ ----------
+ data : dict
+ node-link formatted graph data
+
+ directed : bool
+ If True, and direction not specified in data, return a directed graph.
+
+ multigraph : bool
+ If True, and multigraph not specified in data, return a multigraph.
+
+ source : string
+ A string that provides the 'source' attribute name for storing NetworkX-internal graph data.
+ target : string
+ A string that provides the 'target' attribute name for storing NetworkX-internal graph data.
+ name : string
+ A string that provides the 'name' attribute name for storing NetworkX-internal graph data.
+ key : string
+ A string that provides the 'key' attribute name for storing NetworkX-internal graph data.
+ edges : string
+ A string that provides the 'edges' attribute name for storing NetworkX-internal graph data.
+ nodes : string
+ A string that provides the 'nodes' attribute name for storing NetworkX-internal graph data.
+ link : string
+ .. deprecated:: 3.4
+
+ The `link` argument is deprecated and will be removed in version `3.6`.
+ Use the `edges` keyword instead.
+
+ A string that provides the 'edges' attribute name for storing NetworkX-internal graph data.
+
+ Returns
+ -------
+ G : NetworkX graph
+ A NetworkX graph object
+
+ Examples
+ --------
+
+ Create data in node-link format by converting a graph.
+
+ >>> from pprint import pprint
+ >>> G = nx.Graph([("A", "B")])
+ >>> data = nx.node_link_data(G, edges="edges")
+ >>> pprint(data)
+ {'directed': False,
+ 'edges': [{'source': 'A', 'target': 'B'}],
+ 'graph': {},
+ 'multigraph': False,
+ 'nodes': [{'id': 'A'}, {'id': 'B'}]}
+
+ Revert data in node-link format to a graph.
+
+ >>> H = nx.node_link_graph(data, edges="edges")
+ >>> print(H.edges)
+ [('A', 'B')]
+
+ To serialize and deserialize a graph with JSON,
+
+ >>> import json
+ >>> d = json.dumps(nx.node_link_data(G, edges="edges"))
+ >>> H = nx.node_link_graph(json.loads(d), edges="edges")
+ >>> print(G.edges, H.edges)
+ [('A', 'B')] [('A', 'B')]
+
+
+ Notes
+ -----
+ Attribute 'key' is only used for multigraphs.
+
+ To use `node_link_data` in conjunction with `node_link_graph`,
+ the keyword names for the attributes must match.
+
+ See Also
+ --------
+ node_link_data, adjacency_data, tree_data
+ """
+ # TODO: Remove between the lines when `link` deprecation expires
+ # -------------------------------------------------------------
+ if link is not None:
+ warnings.warn(
+ "Keyword argument 'link' is deprecated; use 'edges' instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ if edges is not None:
+ raise ValueError(
+ "Both 'edges' and 'link' are specified. Use 'edges', 'link' will be remove in a future release"
+ )
+ else:
+ edges = link
+ else:
+ if edges is None:
+ warnings.warn(
+ (
+ '\nThe default value will be changed to `edges="edges" in NetworkX 3.6.\n\n'
+ "To make this warning go away, explicitly set the edges kwarg, e.g.:\n\n"
+ ' nx.node_link_graph(data, edges="links") to preserve current behavior, or\n'
+ ' nx.node_link_graph(data, edges="edges") for forward compatibility.'
+ ),
+ FutureWarning,
+ )
+ edges = "links"
+ # -------------------------------------------------------------
+
+ multigraph = data.get("multigraph", multigraph)
+ directed = data.get("directed", directed)
+ if multigraph:
+ graph = nx.MultiGraph()
+ else:
+ graph = nx.Graph()
+ if directed:
+ graph = graph.to_directed()
+
+ # Allow 'key' to be omitted from attrs if the graph is not a multigraph.
+ key = None if not multigraph else key
+ graph.graph = data.get("graph", {})
+ c = count()
+ for d in data[nodes]:
+ node = _to_tuple(d.get(name, next(c)))
+ nodedata = {str(k): v for k, v in d.items() if k != name}
+ graph.add_node(node, **nodedata)
+ for d in data[edges]:
+ src = tuple(d[source]) if isinstance(d[source], list) else d[source]
+ tgt = tuple(d[target]) if isinstance(d[target], list) else d[target]
+ if not multigraph:
+ edgedata = {str(k): v for k, v in d.items() if k != source and k != target}
+ graph.add_edge(src, tgt, **edgedata)
+ else:
+ ky = d.get(key, None)
+ edgedata = {
+ str(k): v
+ for k, v in d.items()
+ if k != source and k != target and k != key
+ }
+ graph.add_edge(src, tgt, ky, **edgedata)
+ return graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py
new file mode 100644
index 00000000..37506382
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py
@@ -0,0 +1,78 @@
+import copy
+import json
+
+import pytest
+
+import networkx as nx
+from networkx.readwrite.json_graph import adjacency_data, adjacency_graph
+from networkx.utils import graphs_equal
+
+
+class TestAdjacency:
+ def test_graph(self):
+ G = nx.path_graph(4)
+ H = adjacency_graph(adjacency_data(G))
+ assert graphs_equal(G, H)
+
+ def test_graph_attributes(self):
+ G = nx.path_graph(4)
+ G.add_node(1, color="red")
+ G.add_edge(1, 2, width=7)
+ G.graph["foo"] = "bar"
+ G.graph[1] = "one"
+
+ H = adjacency_graph(adjacency_data(G))
+ assert graphs_equal(G, H)
+ assert H.graph["foo"] == "bar"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+
+ d = json.dumps(adjacency_data(G))
+ H = adjacency_graph(json.loads(d))
+ assert graphs_equal(G, H)
+ assert H.graph["foo"] == "bar"
+ assert H.graph[1] == "one"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+
+ def test_digraph(self):
+ G = nx.DiGraph()
+ nx.add_path(G, [1, 2, 3])
+ H = adjacency_graph(adjacency_data(G))
+ assert H.is_directed()
+ assert graphs_equal(G, H)
+
+ def test_multidigraph(self):
+ G = nx.MultiDiGraph()
+ nx.add_path(G, [1, 2, 3])
+ H = adjacency_graph(adjacency_data(G))
+ assert H.is_directed()
+ assert H.is_multigraph()
+ assert graphs_equal(G, H)
+
+ def test_multigraph(self):
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key="first")
+ G.add_edge(1, 2, key="second", color="blue")
+ H = adjacency_graph(adjacency_data(G))
+ assert graphs_equal(G, H)
+ assert H[1][2]["second"]["color"] == "blue"
+
+ def test_input_data_is_not_modified_when_building_graph(self):
+ G = nx.path_graph(4)
+ input_data = adjacency_data(G)
+ orig_data = copy.deepcopy(input_data)
+ # Ensure input is unmodified by deserialisation
+ assert graphs_equal(G, adjacency_graph(input_data))
+ assert input_data == orig_data
+
+ def test_adjacency_form_json_serialisable(self):
+ G = nx.path_graph(4)
+ H = adjacency_graph(json.loads(json.dumps(adjacency_data(G))))
+ assert graphs_equal(G, H)
+
+ def test_exception(self):
+ with pytest.raises(nx.NetworkXError):
+ G = nx.MultiDiGraph()
+ attrs = {"id": "node", "key": "node"}
+ adjacency_data(G, attrs)
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py
new file mode 100644
index 00000000..5d47f21f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py
@@ -0,0 +1,78 @@
+import copy
+import json
+
+import pytest
+
+import networkx as nx
+from networkx.readwrite.json_graph import cytoscape_data, cytoscape_graph
+
+
+def test_graph():
+ G = nx.path_graph(4)
+ H = cytoscape_graph(cytoscape_data(G))
+ assert nx.is_isomorphic(G, H)
+
+
+def test_input_data_is_not_modified_when_building_graph():
+ G = nx.path_graph(4)
+ input_data = cytoscape_data(G)
+ orig_data = copy.deepcopy(input_data)
+ # Ensure input is unmodified by cytoscape_graph (gh-4173)
+ cytoscape_graph(input_data)
+ assert input_data == orig_data
+
+
+def test_graph_attributes():
+ G = nx.path_graph(4)
+ G.add_node(1, color="red")
+ G.add_edge(1, 2, width=7)
+ G.graph["foo"] = "bar"
+ G.graph[1] = "one"
+ G.add_node(3, name="node", id="123")
+
+ H = cytoscape_graph(cytoscape_data(G))
+ assert H.graph["foo"] == "bar"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+ assert H.nodes[3]["name"] == "node"
+ assert H.nodes[3]["id"] == "123"
+
+ d = json.dumps(cytoscape_data(G))
+ H = cytoscape_graph(json.loads(d))
+ assert H.graph["foo"] == "bar"
+ assert H.graph[1] == "one"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+ assert H.nodes[3]["name"] == "node"
+ assert H.nodes[3]["id"] == "123"
+
+
+def test_digraph():
+ G = nx.DiGraph()
+ nx.add_path(G, [1, 2, 3])
+ H = cytoscape_graph(cytoscape_data(G))
+ assert H.is_directed()
+ assert nx.is_isomorphic(G, H)
+
+
+def test_multidigraph():
+ G = nx.MultiDiGraph()
+ nx.add_path(G, [1, 2, 3])
+ H = cytoscape_graph(cytoscape_data(G))
+ assert H.is_directed()
+ assert H.is_multigraph()
+
+
+def test_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key="first")
+ G.add_edge(1, 2, key="second", color="blue")
+ H = cytoscape_graph(cytoscape_data(G))
+ assert nx.is_isomorphic(G, H)
+ assert H[1][2]["second"]["color"] == "blue"
+
+
+def test_exception():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.MultiDiGraph()
+ cytoscape_data(G, name="foo", ident="foo")
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py
new file mode 100644
index 00000000..f903f606
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py
@@ -0,0 +1,175 @@
+import json
+
+import pytest
+
+import networkx as nx
+from networkx.readwrite.json_graph import node_link_data, node_link_graph
+
+
+def test_node_link_edges_default_future_warning():
+ "Test FutureWarning is raised when `edges=None` in node_link_data and node_link_graph"
+ G = nx.Graph([(1, 2)])
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ data = nx.node_link_data(G) # edges=None, the default
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = nx.node_link_graph(data) # edges=None, the default
+
+
+def test_node_link_deprecated_link_param():
+ G = nx.Graph([(1, 2)])
+ with pytest.warns(DeprecationWarning, match="Keyword argument 'link'"):
+ data = nx.node_link_data(G, link="links")
+ with pytest.warns(DeprecationWarning, match="Keyword argument 'link'"):
+ H = nx.node_link_graph(data, link="links")
+
+
+class TestNodeLink:
+ # TODO: To be removed when signature change complete
+ def test_custom_attrs_dep(self):
+ G = nx.path_graph(4)
+ G.add_node(1, color="red")
+ G.add_edge(1, 2, width=7)
+ G.graph[1] = "one"
+ G.graph["foo"] = "bar"
+
+ attrs = {
+ "source": "c_source",
+ "target": "c_target",
+ "name": "c_id",
+ "key": "c_key",
+ "link": "c_links",
+ }
+
+ H = node_link_graph(node_link_data(G, **attrs), multigraph=False, **attrs)
+ assert nx.is_isomorphic(G, H)
+ assert H.graph["foo"] == "bar"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+
+ # provide only a partial dictionary of keywords.
+ # This is similar to an example in the doc string
+ attrs = {
+ "link": "c_links",
+ "source": "c_source",
+ "target": "c_target",
+ }
+ H = node_link_graph(node_link_data(G, **attrs), multigraph=False, **attrs)
+ assert nx.is_isomorphic(G, H)
+ assert H.graph["foo"] == "bar"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+
+ def test_exception_dep(self):
+ G = nx.MultiDiGraph()
+ with pytest.raises(nx.NetworkXError):
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ node_link_data(G, name="node", source="node", target="node", key="node")
+
+ def test_graph(self):
+ G = nx.path_graph(4)
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(node_link_data(G))
+ assert nx.is_isomorphic(G, H)
+
+ def test_graph_attributes(self):
+ G = nx.path_graph(4)
+ G.add_node(1, color="red")
+ G.add_edge(1, 2, width=7)
+ G.graph[1] = "one"
+ G.graph["foo"] = "bar"
+
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(node_link_data(G))
+ assert H.graph["foo"] == "bar"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ d = json.dumps(node_link_data(G))
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(json.loads(d))
+ assert H.graph["foo"] == "bar"
+ assert H.graph["1"] == "one"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
+
+ def test_digraph(self):
+ G = nx.DiGraph()
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(node_link_data(G))
+ assert H.is_directed()
+
+ def test_multigraph(self):
+ G = nx.MultiGraph()
+ G.add_edge(1, 2, key="first")
+ G.add_edge(1, 2, key="second", color="blue")
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(node_link_data(G))
+ assert nx.is_isomorphic(G, H)
+ assert H[1][2]["second"]["color"] == "blue"
+
+ def test_graph_with_tuple_nodes(self):
+ G = nx.Graph()
+ G.add_edge((0, 0), (1, 0), color=[255, 255, 0])
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ d = node_link_data(G)
+ dumped_d = json.dumps(d)
+ dd = json.loads(dumped_d)
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(dd)
+ assert H.nodes[(0, 0)] == G.nodes[(0, 0)]
+ assert H[(0, 0)][(1, 0)]["color"] == [255, 255, 0]
+
+ def test_unicode_keys(self):
+ q = "qualité"
+ G = nx.Graph()
+ G.add_node(1, **{q: q})
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ s = node_link_data(G)
+ output = json.dumps(s, ensure_ascii=False)
+ data = json.loads(output)
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(data)
+ assert H.nodes[1][q] == q
+
+ def test_exception(self):
+ G = nx.MultiDiGraph()
+ attrs = {"name": "node", "source": "node", "target": "node", "key": "node"}
+ with pytest.raises(nx.NetworkXError):
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ node_link_data(G, **attrs)
+
+ def test_string_ids(self):
+ q = "qualité"
+ G = nx.DiGraph()
+ G.add_node("A")
+ G.add_node(q)
+ G.add_edge("A", q)
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ data = node_link_data(G)
+ assert data["links"][0]["source"] == "A"
+ assert data["links"][0]["target"] == q
+ with pytest.warns(FutureWarning, match="\nThe default value will be"):
+ H = node_link_graph(data)
+ assert nx.is_isomorphic(G, H)
+
+ def test_custom_attrs(self):
+ G = nx.path_graph(4)
+ G.add_node(1, color="red")
+ G.add_edge(1, 2, width=7)
+ G.graph[1] = "one"
+ G.graph["foo"] = "bar"
+
+ attrs = {
+ "source": "c_source",
+ "target": "c_target",
+ "name": "c_id",
+ "key": "c_key",
+ "link": "c_links",
+ }
+
+ H = node_link_graph(node_link_data(G, **attrs), multigraph=False, **attrs)
+ assert nx.is_isomorphic(G, H)
+ assert H.graph["foo"] == "bar"
+ assert H.nodes[1]["color"] == "red"
+ assert H[1][2]["width"] == 7
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py
new file mode 100644
index 00000000..643a14d8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py
@@ -0,0 +1,48 @@
+import json
+
+import pytest
+
+import networkx as nx
+from networkx.readwrite.json_graph import tree_data, tree_graph
+
+
+def test_graph():
+ G = nx.DiGraph()
+ G.add_nodes_from([1, 2, 3], color="red")
+ G.add_edge(1, 2, foo=7)
+ G.add_edge(1, 3, foo=10)
+ G.add_edge(3, 4, foo=10)
+ H = tree_graph(tree_data(G, 1))
+ assert nx.is_isomorphic(G, H)
+
+
+def test_graph_attributes():
+ G = nx.DiGraph()
+ G.add_nodes_from([1, 2, 3], color="red")
+ G.add_edge(1, 2, foo=7)
+ G.add_edge(1, 3, foo=10)
+ G.add_edge(3, 4, foo=10)
+ H = tree_graph(tree_data(G, 1))
+ assert H.nodes[1]["color"] == "red"
+
+ d = json.dumps(tree_data(G, 1))
+ H = tree_graph(json.loads(d))
+ assert H.nodes[1]["color"] == "red"
+
+
+def test_exceptions():
+ with pytest.raises(TypeError, match="is not a tree."):
+ G = nx.complete_graph(3)
+ tree_data(G, 0)
+ with pytest.raises(TypeError, match="is not directed."):
+ G = nx.path_graph(3)
+ tree_data(G, 0)
+ with pytest.raises(TypeError, match="is not weakly connected."):
+ G = nx.path_graph(3, create_using=nx.DiGraph)
+ G.add_edge(2, 0)
+ G.add_node(3)
+ tree_data(G, 0)
+ with pytest.raises(nx.NetworkXError, match="must be different."):
+ G = nx.MultiDiGraph()
+ G.add_node(0)
+ tree_data(G, 0, ident="node", children="node")
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py
new file mode 100644
index 00000000..22b07b09
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py
@@ -0,0 +1,137 @@
+from itertools import chain
+
+import networkx as nx
+
+__all__ = ["tree_data", "tree_graph"]
+
+
+def tree_data(G, root, ident="id", children="children"):
+ """Returns data in tree format that is suitable for JSON serialization
+ and use in JavaScript documents.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ G must be an oriented tree
+
+ root : node
+ The root of the tree
+
+ ident : string
+ Attribute name for storing NetworkX-internal graph data. `ident` must
+ have a different value than `children`. The default is 'id'.
+
+ children : string
+ Attribute name for storing NetworkX-internal graph data. `children`
+ must have a different value than `ident`. The default is 'children'.
+
+ Returns
+ -------
+ data : dict
+ A dictionary with node-link formatted data.
+
+ Raises
+ ------
+ NetworkXError
+ If `children` and `ident` attributes are identical.
+
+ Examples
+ --------
+ >>> from networkx.readwrite import json_graph
+ >>> G = nx.DiGraph([(1, 2)])
+ >>> data = json_graph.tree_data(G, root=1)
+
+ To serialize with json
+
+ >>> import json
+ >>> s = json.dumps(data)
+
+ Notes
+ -----
+ Node attributes are stored in this format but keys
+ for attributes must be strings if you want to serialize with JSON.
+
+ Graph and edge attributes are not stored.
+
+ See Also
+ --------
+ tree_graph, node_link_data, adjacency_data
+ """
+ if G.number_of_nodes() != G.number_of_edges() + 1:
+ raise TypeError("G is not a tree.")
+ if not G.is_directed():
+ raise TypeError("G is not directed.")
+ if not nx.is_weakly_connected(G):
+ raise TypeError("G is not weakly connected.")
+
+ if ident == children:
+ raise nx.NetworkXError("The values for `id` and `children` must be different.")
+
+ def add_children(n, G):
+ nbrs = G[n]
+ if len(nbrs) == 0:
+ return []
+ children_ = []
+ for child in nbrs:
+ d = {**G.nodes[child], ident: child}
+ c = add_children(child, G)
+ if c:
+ d[children] = c
+ children_.append(d)
+ return children_
+
+ return {**G.nodes[root], ident: root, children: add_children(root, G)}
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def tree_graph(data, ident="id", children="children"):
+ """Returns graph from tree data format.
+
+ Parameters
+ ----------
+ data : dict
+ Tree formatted graph data
+
+ ident : string
+ Attribute name for storing NetworkX-internal graph data. `ident` must
+ have a different value than `children`. The default is 'id'.
+
+ children : string
+ Attribute name for storing NetworkX-internal graph data. `children`
+ must have a different value than `ident`. The default is 'children'.
+
+ Returns
+ -------
+ G : NetworkX DiGraph
+
+ Examples
+ --------
+ >>> from networkx.readwrite import json_graph
+ >>> G = nx.DiGraph([(1, 2)])
+ >>> data = json_graph.tree_data(G, root=1)
+ >>> H = json_graph.tree_graph(data)
+
+ See Also
+ --------
+ tree_data, node_link_data, adjacency_data
+ """
+ graph = nx.DiGraph()
+
+ def add_children(parent, children_):
+ for data in children_:
+ child = data[ident]
+ graph.add_edge(parent, child)
+ grandchildren = data.get(children, [])
+ if grandchildren:
+ add_children(child, grandchildren)
+ nodedata = {
+ str(k): v for k, v in data.items() if k != ident and k != children
+ }
+ graph.add_node(child, **nodedata)
+
+ root = data[ident]
+ children_ = data.get(children, [])
+ nodedata = {str(k): v for k, v in data.items() if k != ident and k != children}
+ graph.add_node(root, **nodedata)
+ add_children(root, children_)
+ return graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/leda.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/leda.py
new file mode 100644
index 00000000..9fb57db1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/leda.py
@@ -0,0 +1,108 @@
+"""
+Read graphs in LEDA format.
+
+LEDA is a C++ class library for efficient data types and algorithms.
+
+Format
+------
+See http://www.algorithmic-solutions.info/leda_guide/graphs/leda_native_graph_fileformat.html
+
+"""
+# Original author: D. Eppstein, UC Irvine, August 12, 2003.
+# The original code at http://www.ics.uci.edu/~eppstein/PADS/ is public domain.
+
+__all__ = ["read_leda", "parse_leda"]
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.utils import open_file
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_leda(path, encoding="UTF-8"):
+ """Read graph in LEDA format from path.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to read. Filenames ending in .gz or .bz2 will be
+ uncompressed.
+
+ Returns
+ -------
+ G : NetworkX graph
+
+ Examples
+ --------
+ G=nx.read_leda('file.leda')
+
+ References
+ ----------
+ .. [1] http://www.algorithmic-solutions.info/leda_guide/graphs/leda_native_graph_fileformat.html
+ """
+ lines = (line.decode(encoding) for line in path)
+ G = parse_leda(lines)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_leda(lines):
+ """Read graph in LEDA format from string or iterable.
+
+ Parameters
+ ----------
+ lines : string or iterable
+ Data in LEDA format.
+
+ Returns
+ -------
+ G : NetworkX graph
+
+ Examples
+ --------
+ G=nx.parse_leda(string)
+
+ References
+ ----------
+ .. [1] http://www.algorithmic-solutions.info/leda_guide/graphs/leda_native_graph_fileformat.html
+ """
+ if isinstance(lines, str):
+ lines = iter(lines.split("\n"))
+ lines = iter(
+ [
+ line.rstrip("\n")
+ for line in lines
+ if not (line.startswith(("#", "\n")) or line == "")
+ ]
+ )
+ for i in range(3):
+ next(lines)
+ # Graph
+ du = int(next(lines)) # -1=directed, -2=undirected
+ if du == -1:
+ G = nx.DiGraph()
+ else:
+ G = nx.Graph()
+
+ # Nodes
+ n = int(next(lines)) # number of nodes
+ node = {}
+ for i in range(1, n + 1): # LEDA counts from 1 to n
+ symbol = next(lines).rstrip().strip("|{}| ")
+ if symbol == "":
+ symbol = str(i) # use int if no label - could be trouble
+ node[i] = symbol
+
+ G.add_nodes_from([s for i, s in node.items()])
+
+ # Edges
+ m = int(next(lines)) # number of edges
+ for i in range(m):
+ try:
+ s, t, reversal, label = next(lines).split()
+ except BaseException as err:
+ raise NetworkXError(f"Too few fields in LEDA.GRAPH edge {i+1}") from err
+ # BEWARE: no handling of reversal edges
+ G.add_edge(node[int(s)], node[int(t)], label=label[2:-2])
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py
new file mode 100644
index 00000000..808445db
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py
@@ -0,0 +1,393 @@
+"""
+*************************
+Multi-line Adjacency List
+*************************
+Read and write NetworkX graphs as multi-line adjacency lists.
+
+The multi-line adjacency list format is useful for graphs with
+nodes that can be meaningfully represented as strings. With this format
+simple edge data can be stored but node or graph data is not.
+
+Format
+------
+The first label in a line is the source node label followed by the node degree
+d. The next d lines are target node labels and optional edge data.
+That pattern repeats for all nodes in the graph.
+
+The graph with edges a-b, a-c, d-e can be represented as the following
+adjacency list (anything following the # in a line is a comment)::
+
+ # example.multiline-adjlist
+ a 2
+ b
+ c
+ d 1
+ e
+"""
+
+__all__ = [
+ "generate_multiline_adjlist",
+ "write_multiline_adjlist",
+ "parse_multiline_adjlist",
+ "read_multiline_adjlist",
+]
+
+import networkx as nx
+from networkx.utils import open_file
+
+
+def generate_multiline_adjlist(G, delimiter=" "):
+ """Generate a single line of the graph G in multiline adjacency list format.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ delimiter : string, optional
+ Separator for node labels
+
+ Returns
+ -------
+ lines : string
+ Lines of data in multiline adjlist format.
+
+ Examples
+ --------
+ >>> G = nx.lollipop_graph(4, 3)
+ >>> for line in nx.generate_multiline_adjlist(G):
+ ... print(line)
+ 0 3
+ 1 {}
+ 2 {}
+ 3 {}
+ 1 2
+ 2 {}
+ 3 {}
+ 2 1
+ 3 {}
+ 3 1
+ 4 {}
+ 4 1
+ 5 {}
+ 5 1
+ 6 {}
+ 6 0
+
+ See Also
+ --------
+ write_multiline_adjlist, read_multiline_adjlist
+ """
+ if G.is_directed():
+ if G.is_multigraph():
+ for s, nbrs in G.adjacency():
+ nbr_edges = [
+ (u, data)
+ for u, datadict in nbrs.items()
+ for key, data in datadict.items()
+ ]
+ deg = len(nbr_edges)
+ yield str(s) + delimiter + str(deg)
+ for u, d in nbr_edges:
+ if d is None:
+ yield str(u)
+ else:
+ yield str(u) + delimiter + str(d)
+ else: # directed single edges
+ for s, nbrs in G.adjacency():
+ deg = len(nbrs)
+ yield str(s) + delimiter + str(deg)
+ for u, d in nbrs.items():
+ if d is None:
+ yield str(u)
+ else:
+ yield str(u) + delimiter + str(d)
+ else: # undirected
+ if G.is_multigraph():
+ seen = set() # helper dict used to avoid duplicate edges
+ for s, nbrs in G.adjacency():
+ nbr_edges = [
+ (u, data)
+ for u, datadict in nbrs.items()
+ if u not in seen
+ for key, data in datadict.items()
+ ]
+ deg = len(nbr_edges)
+ yield str(s) + delimiter + str(deg)
+ for u, d in nbr_edges:
+ if d is None:
+ yield str(u)
+ else:
+ yield str(u) + delimiter + str(d)
+ seen.add(s)
+ else: # undirected single edges
+ seen = set() # helper dict used to avoid duplicate edges
+ for s, nbrs in G.adjacency():
+ nbr_edges = [(u, d) for u, d in nbrs.items() if u not in seen]
+ deg = len(nbr_edges)
+ yield str(s) + delimiter + str(deg)
+ for u, d in nbr_edges:
+ if d is None:
+ yield str(u)
+ else:
+ yield str(u) + delimiter + str(d)
+ seen.add(s)
+
+
+@open_file(1, mode="wb")
+def write_multiline_adjlist(G, path, delimiter=" ", comments="#", encoding="utf-8"):
+ """Write the graph G in multiline adjacency list format to path
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ path : string or file
+ Filename or file handle to write to.
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ comments : string, optional
+ Marker for comment lines
+
+ delimiter : string, optional
+ Separator for node labels
+
+ encoding : string, optional
+ Text encoding.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_multiline_adjlist(G, "test.adjlist")
+
+ The path can be a file handle or a string with the name of the file. If a
+ file handle is provided, it has to be opened in 'wb' mode.
+
+ >>> fh = open("test.adjlist", "wb")
+ >>> nx.write_multiline_adjlist(G, fh)
+
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ >>> nx.write_multiline_adjlist(G, "test.adjlist.gz")
+
+ See Also
+ --------
+ read_multiline_adjlist
+ """
+ import sys
+ import time
+
+ pargs = comments + " ".join(sys.argv)
+ header = (
+ f"{pargs}\n"
+ + comments
+ + f" GMT {time.asctime(time.gmtime())}\n"
+ + comments
+ + f" {G.name}\n"
+ )
+ path.write(header.encode(encoding))
+
+ for multiline in generate_multiline_adjlist(G, delimiter):
+ multiline += "\n"
+ path.write(multiline.encode(encoding))
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_multiline_adjlist(
+ lines, comments="#", delimiter=None, create_using=None, nodetype=None, edgetype=None
+):
+ """Parse lines of a multiline adjacency list representation of a graph.
+
+ Parameters
+ ----------
+ lines : list or iterator of strings
+ Input data in multiline adjlist format
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ nodetype : Python type, optional
+ Convert nodes to this type.
+
+ edgetype : Python type, optional
+ Convert edges to this type.
+
+ comments : string, optional
+ Marker for comment lines
+
+ delimiter : string, optional
+ Separator for node labels. The default is whitespace.
+
+ Returns
+ -------
+ G: NetworkX graph
+ The graph corresponding to the lines in multiline adjacency list format.
+
+ Examples
+ --------
+ >>> lines = [
+ ... "1 2",
+ ... "2 {'weight':3, 'name': 'Frodo'}",
+ ... "3 {}",
+ ... "2 1",
+ ... "5 {'weight':6, 'name': 'Saruman'}",
+ ... ]
+ >>> G = nx.parse_multiline_adjlist(iter(lines), nodetype=int)
+ >>> list(G)
+ [1, 2, 3, 5]
+
+ """
+ from ast import literal_eval
+
+ G = nx.empty_graph(0, create_using)
+ for line in lines:
+ p = line.find(comments)
+ if p >= 0:
+ line = line[:p]
+ if not line:
+ continue
+ try:
+ (u, deg) = line.rstrip("\n").split(delimiter)
+ deg = int(deg)
+ except BaseException as err:
+ raise TypeError(f"Failed to read node and degree on line ({line})") from err
+ if nodetype is not None:
+ try:
+ u = nodetype(u)
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert node ({u}) to type {nodetype}"
+ ) from err
+ G.add_node(u)
+ for i in range(deg):
+ while True:
+ try:
+ line = next(lines)
+ except StopIteration as err:
+ msg = f"Failed to find neighbor for node ({u})"
+ raise TypeError(msg) from err
+ p = line.find(comments)
+ if p >= 0:
+ line = line[:p]
+ if line:
+ break
+ vlist = line.rstrip("\n").split(delimiter)
+ numb = len(vlist)
+ if numb < 1:
+ continue # isolated node
+ v = vlist.pop(0)
+ data = "".join(vlist)
+ if nodetype is not None:
+ try:
+ v = nodetype(v)
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert node ({v}) to type {nodetype}"
+ ) from err
+ if edgetype is not None:
+ try:
+ edgedata = {"weight": edgetype(data)}
+ except BaseException as err:
+ raise TypeError(
+ f"Failed to convert edge data ({data}) to type {edgetype}"
+ ) from err
+ else:
+ try: # try to evaluate
+ edgedata = literal_eval(data)
+ except:
+ edgedata = {}
+ G.add_edge(u, v, **edgedata)
+
+ return G
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_multiline_adjlist(
+ path,
+ comments="#",
+ delimiter=None,
+ create_using=None,
+ nodetype=None,
+ edgetype=None,
+ encoding="utf-8",
+):
+ """Read graph in multi-line adjacency list format from path.
+
+ Parameters
+ ----------
+ path : string or file
+ Filename or file handle to read.
+ Filenames ending in .gz or .bz2 will be uncompressed.
+
+ create_using : NetworkX graph constructor, optional (default=nx.Graph)
+ Graph type to create. If graph instance, then cleared before populated.
+
+ nodetype : Python type, optional
+ Convert nodes to this type.
+
+ edgetype : Python type, optional
+ Convert edge data to this type.
+
+ comments : string, optional
+ Marker for comment lines
+
+ delimiter : string, optional
+ Separator for node labels. The default is whitespace.
+
+ Returns
+ -------
+ G: NetworkX graph
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_multiline_adjlist(G, "test.adjlist")
+ >>> G = nx.read_multiline_adjlist("test.adjlist")
+
+ The path can be a file or a string with the name of the file. If a
+ file s provided, it has to be opened in 'rb' mode.
+
+ >>> fh = open("test.adjlist", "rb")
+ >>> G = nx.read_multiline_adjlist(fh)
+
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ >>> nx.write_multiline_adjlist(G, "test.adjlist.gz")
+ >>> G = nx.read_multiline_adjlist("test.adjlist.gz")
+
+ The optional nodetype is a function to convert node strings to nodetype.
+
+ For example
+
+ >>> G = nx.read_multiline_adjlist("test.adjlist", nodetype=int)
+
+ will attempt to convert all nodes to integer type.
+
+ The optional edgetype is a function to convert edge data strings to
+ edgetype.
+
+ >>> G = nx.read_multiline_adjlist("test.adjlist")
+
+ The optional create_using parameter is a NetworkX graph container.
+ The default is Graph(), an undirected graph. To read the data as
+ a directed graph use
+
+ >>> G = nx.read_multiline_adjlist("test.adjlist", create_using=nx.DiGraph)
+
+ Notes
+ -----
+ This format does not store graph, node, or edge data.
+
+ See Also
+ --------
+ write_multiline_adjlist
+ """
+ lines = (line.decode(encoding) for line in path)
+ return parse_multiline_adjlist(
+ lines,
+ comments=comments,
+ delimiter=delimiter,
+ create_using=create_using,
+ nodetype=nodetype,
+ edgetype=edgetype,
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/p2g.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/p2g.py
new file mode 100644
index 00000000..804adb23
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/p2g.py
@@ -0,0 +1,105 @@
+"""
+This module provides the following: read and write of p2g format
+used in metabolic pathway studies.
+
+See https://web.archive.org/web/20080626113807/http://www.cs.purdue.edu/homes/koyuturk/pathway/ for a description.
+
+The summary is included here:
+
+A file that describes a uniquely labeled graph (with extension ".gr")
+format looks like the following:
+
+
+name
+3 4
+a
+1 2
+b
+
+c
+0 2
+
+"name" is simply a description of what the graph corresponds to. The
+second line displays the number of nodes and number of edges,
+respectively. This sample graph contains three nodes labeled "a", "b",
+and "c". The rest of the graph contains two lines for each node. The
+first line for a node contains the node label. After the declaration
+of the node label, the out-edges of that node in the graph are
+provided. For instance, "a" is linked to nodes 1 and 2, which are
+labeled "b" and "c", while the node labeled "b" has no outgoing
+edges. Observe that node labeled "c" has an outgoing edge to
+itself. Indeed, self-loops are allowed. Node index starts from 0.
+
+"""
+
+import networkx as nx
+from networkx.utils import open_file
+
+
+@open_file(1, mode="w")
+def write_p2g(G, path, encoding="utf-8"):
+ """Write NetworkX graph in p2g format.
+
+ Notes
+ -----
+ This format is meant to be used with directed graphs with
+ possible self loops.
+ """
+ path.write((f"{G.name}\n").encode(encoding))
+ path.write((f"{G.order()} {G.size()}\n").encode(encoding))
+ nodes = list(G)
+ # make dictionary mapping nodes to integers
+ nodenumber = dict(zip(nodes, range(len(nodes))))
+ for n in nodes:
+ path.write((f"{n}\n").encode(encoding))
+ for nbr in G.neighbors(n):
+ path.write((f"{nodenumber[nbr]} ").encode(encoding))
+ path.write("\n".encode(encoding))
+
+
+@open_file(0, mode="r")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_p2g(path, encoding="utf-8"):
+ """Read graph in p2g format from path.
+
+ Returns
+ -------
+ MultiDiGraph
+
+ Notes
+ -----
+ If you want a DiGraph (with no self loops allowed and no edge data)
+ use D=nx.DiGraph(read_p2g(path))
+ """
+ lines = (line.decode(encoding) for line in path)
+ G = parse_p2g(lines)
+ return G
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_p2g(lines):
+ """Parse p2g format graph from string or iterable.
+
+ Returns
+ -------
+ MultiDiGraph
+ """
+ description = next(lines).strip()
+ # are multiedges (parallel edges) allowed?
+ G = nx.MultiDiGraph(name=description, selfloops=True)
+ nnodes, nedges = map(int, next(lines).split())
+ nodelabel = {}
+ nbrs = {}
+ # loop over the nodes keeping track of node labels and out neighbors
+ # defer adding edges until all node labels are known
+ for i in range(nnodes):
+ n = next(lines).strip()
+ nodelabel[i] = n
+ G.add_node(n)
+ nbrs[n] = map(int, next(lines).split())
+ # now we know all of the node labels so we can add the edges
+ # with the correct labels
+ for n in G:
+ for nbr in nbrs[n]:
+ G.add_edge(n, nodelabel[nbr])
+ return G
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/pajek.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/pajek.py
new file mode 100644
index 00000000..f148f162
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/pajek.py
@@ -0,0 +1,286 @@
+"""
+*****
+Pajek
+*****
+Read graphs in Pajek format.
+
+This implementation handles directed and undirected graphs including
+those with self loops and parallel edges.
+
+Format
+------
+See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
+for format information.
+
+"""
+
+import warnings
+
+import networkx as nx
+from networkx.utils import open_file
+
+__all__ = ["read_pajek", "parse_pajek", "generate_pajek", "write_pajek"]
+
+
+def generate_pajek(G):
+ """Generate lines in Pajek graph format.
+
+ Parameters
+ ----------
+ G : graph
+ A Networkx graph
+
+ References
+ ----------
+ See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
+ for format information.
+ """
+ if G.name == "":
+ name = "NetworkX"
+ else:
+ name = G.name
+ # Apparently many Pajek format readers can't process this line
+ # So we'll leave it out for now.
+ # yield '*network %s'%name
+
+ # write nodes with attributes
+ yield f"*vertices {G.order()}"
+ nodes = list(G)
+ # make dictionary mapping nodes to integers
+ nodenumber = dict(zip(nodes, range(1, len(nodes) + 1)))
+ for n in nodes:
+ # copy node attributes and pop mandatory attributes
+ # to avoid duplication.
+ na = G.nodes.get(n, {}).copy()
+ x = na.pop("x", 0.0)
+ y = na.pop("y", 0.0)
+ try:
+ id = int(na.pop("id", nodenumber[n]))
+ except ValueError as err:
+ err.args += (
+ (
+ "Pajek format requires 'id' to be an int()."
+ " Refer to the 'Relabeling nodes' section."
+ ),
+ )
+ raise
+ nodenumber[n] = id
+ shape = na.pop("shape", "ellipse")
+ s = " ".join(map(make_qstr, (id, n, x, y, shape)))
+ # only optional attributes are left in na.
+ for k, v in na.items():
+ if isinstance(v, str) and v.strip() != "":
+ s += f" {make_qstr(k)} {make_qstr(v)}"
+ else:
+ warnings.warn(
+ f"Node attribute {k} is not processed. {('Empty attribute' if isinstance(v, str) else 'Non-string attribute')}."
+ )
+ yield s
+
+ # write edges with attributes
+ if G.is_directed():
+ yield "*arcs"
+ else:
+ yield "*edges"
+ for u, v, edgedata in G.edges(data=True):
+ d = edgedata.copy()
+ value = d.pop("weight", 1.0) # use 1 as default edge value
+ s = " ".join(map(make_qstr, (nodenumber[u], nodenumber[v], value)))
+ for k, v in d.items():
+ if isinstance(v, str) and v.strip() != "":
+ s += f" {make_qstr(k)} {make_qstr(v)}"
+ else:
+ warnings.warn(
+ f"Edge attribute {k} is not processed. {('Empty attribute' if isinstance(v, str) else 'Non-string attribute')}."
+ )
+ yield s
+
+
+@open_file(1, mode="wb")
+def write_pajek(G, path, encoding="UTF-8"):
+ """Write graph in Pajek format to path.
+
+ Parameters
+ ----------
+ G : graph
+ A Networkx graph
+ path : file or string
+ File or filename to write.
+ Filenames ending in .gz or .bz2 will be compressed.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_pajek(G, "test.net")
+
+ Warnings
+ --------
+ Optional node attributes and edge attributes must be non-empty strings.
+ Otherwise it will not be written into the file. You will need to
+ convert those attributes to strings if you want to keep them.
+
+ References
+ ----------
+ See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
+ for format information.
+ """
+ for line in generate_pajek(G):
+ line += "\n"
+ path.write(line.encode(encoding))
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_pajek(path, encoding="UTF-8"):
+ """Read graph in Pajek format from path.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to write.
+ Filenames ending in .gz or .bz2 will be uncompressed.
+
+ Returns
+ -------
+ G : NetworkX MultiGraph or MultiDiGraph.
+
+ Examples
+ --------
+ >>> G = nx.path_graph(4)
+ >>> nx.write_pajek(G, "test.net")
+ >>> G = nx.read_pajek("test.net")
+
+ To create a Graph instead of a MultiGraph use
+
+ >>> G1 = nx.Graph(G)
+
+ References
+ ----------
+ See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
+ for format information.
+ """
+ lines = (line.decode(encoding) for line in path)
+ return parse_pajek(lines)
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def parse_pajek(lines):
+ """Parse Pajek format graph from string or iterable.
+
+ Parameters
+ ----------
+ lines : string or iterable
+ Data in Pajek format.
+
+ Returns
+ -------
+ G : NetworkX graph
+
+ See Also
+ --------
+ read_pajek
+
+ """
+ import shlex
+
+ # multigraph=False
+ if isinstance(lines, str):
+ lines = iter(lines.split("\n"))
+ lines = iter([line.rstrip("\n") for line in lines])
+ G = nx.MultiDiGraph() # are multiedges allowed in Pajek? assume yes
+ labels = [] # in the order of the file, needed for matrix
+ while lines:
+ try:
+ l = next(lines)
+ except: # EOF
+ break
+ if l.lower().startswith("*network"):
+ try:
+ label, name = l.split(None, 1)
+ except ValueError:
+ # Line was not of the form: *network NAME
+ pass
+ else:
+ G.graph["name"] = name
+ elif l.lower().startswith("*vertices"):
+ nodelabels = {}
+ l, nnodes = l.split()
+ for i in range(int(nnodes)):
+ l = next(lines)
+ try:
+ splitline = [
+ x.decode("utf-8") for x in shlex.split(str(l).encode("utf-8"))
+ ]
+ except AttributeError:
+ splitline = shlex.split(str(l))
+ id, label = splitline[0:2]
+ labels.append(label)
+ G.add_node(label)
+ nodelabels[id] = label
+ G.nodes[label]["id"] = id
+ try:
+ x, y, shape = splitline[2:5]
+ G.nodes[label].update(
+ {"x": float(x), "y": float(y), "shape": shape}
+ )
+ except:
+ pass
+ extra_attr = zip(splitline[5::2], splitline[6::2])
+ G.nodes[label].update(extra_attr)
+ elif l.lower().startswith("*edges") or l.lower().startswith("*arcs"):
+ if l.lower().startswith("*edge"):
+ # switch from multidigraph to multigraph
+ G = nx.MultiGraph(G)
+ if l.lower().startswith("*arcs"):
+ # switch to directed with multiple arcs for each existing edge
+ G = G.to_directed()
+ for l in lines:
+ try:
+ splitline = [
+ x.decode("utf-8") for x in shlex.split(str(l).encode("utf-8"))
+ ]
+ except AttributeError:
+ splitline = shlex.split(str(l))
+
+ if len(splitline) < 2:
+ continue
+ ui, vi = splitline[0:2]
+ u = nodelabels.get(ui, ui)
+ v = nodelabels.get(vi, vi)
+ # parse the data attached to this edge and put in a dictionary
+ edge_data = {}
+ try:
+ # there should always be a single value on the edge?
+ w = splitline[2:3]
+ edge_data.update({"weight": float(w[0])})
+ except:
+ pass
+ # if there isn't, just assign a 1
+ # edge_data.update({'value':1})
+ extra_attr = zip(splitline[3::2], splitline[4::2])
+ edge_data.update(extra_attr)
+ # if G.has_edge(u,v):
+ # multigraph=True
+ G.add_edge(u, v, **edge_data)
+ elif l.lower().startswith("*matrix"):
+ G = nx.DiGraph(G)
+ adj_list = (
+ (labels[row], labels[col], {"weight": int(data)})
+ for (row, line) in enumerate(lines)
+ for (col, data) in enumerate(line.split())
+ if int(data) != 0
+ )
+ G.add_edges_from(adj_list)
+
+ return G
+
+
+def make_qstr(t):
+ """Returns the string representation of t.
+ Add outer double-quotes if the string has a space.
+ """
+ if not isinstance(t, str):
+ t = str(t)
+ if " " in t:
+ t = f'"{t}"'
+ return t
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/sparse6.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/sparse6.py
new file mode 100644
index 00000000..74d16dbc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/sparse6.py
@@ -0,0 +1,377 @@
+# Original author: D. Eppstein, UC Irvine, August 12, 2003.
+# The original code at https://www.ics.uci.edu/~eppstein/PADS/ is public domain.
+"""Functions for reading and writing graphs in the *sparse6* format.
+
+The *sparse6* file format is a space-efficient format for large sparse
+graphs. For small graphs or large dense graphs, use the *graph6* file
+format.
+
+For more information, see the `sparse6`_ homepage.
+
+.. _sparse6: https://users.cecs.anu.edu.au/~bdm/data/formats.html
+
+"""
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.readwrite.graph6 import data_to_n, n_to_data
+from networkx.utils import not_implemented_for, open_file
+
+__all__ = ["from_sparse6_bytes", "read_sparse6", "to_sparse6_bytes", "write_sparse6"]
+
+
+def _generate_sparse6_bytes(G, nodes, header):
+ """Yield bytes in the sparse6 encoding of a graph.
+
+ `G` is an undirected simple graph. `nodes` is the list of nodes for
+ which the node-induced subgraph will be encoded; if `nodes` is the
+ list of all nodes in the graph, the entire graph will be
+ encoded. `header` is a Boolean that specifies whether to generate
+ the header ``b'>>sparse6<<'`` before the remaining data.
+
+ This function generates `bytes` objects in the following order:
+
+ 1. the header (if requested),
+ 2. the encoding of the number of nodes,
+ 3. each character, one-at-a-time, in the encoding of the requested
+ node-induced subgraph,
+ 4. a newline character.
+
+ This function raises :exc:`ValueError` if the graph is too large for
+ the graph6 format (that is, greater than ``2 ** 36`` nodes).
+
+ """
+ n = len(G)
+ if n >= 2**36:
+ raise ValueError(
+ "sparse6 is only defined if number of nodes is less than 2 ** 36"
+ )
+ if header:
+ yield b">>sparse6<<"
+ yield b":"
+ for d in n_to_data(n):
+ yield str.encode(chr(d + 63))
+
+ k = 1
+ while 1 << k < n:
+ k += 1
+
+ def enc(x):
+ """Big endian k-bit encoding of x"""
+ return [1 if (x & 1 << (k - 1 - i)) else 0 for i in range(k)]
+
+ edges = sorted((max(u, v), min(u, v)) for u, v in G.edges())
+ bits = []
+ curv = 0
+ for v, u in edges:
+ if v == curv: # current vertex edge
+ bits.append(0)
+ bits.extend(enc(u))
+ elif v == curv + 1: # next vertex edge
+ curv += 1
+ bits.append(1)
+ bits.extend(enc(u))
+ else: # skip to vertex v and then add edge to u
+ curv = v
+ bits.append(1)
+ bits.extend(enc(v))
+ bits.append(0)
+ bits.extend(enc(u))
+ if k < 6 and n == (1 << k) and ((-len(bits)) % 6) >= k and curv < (n - 1):
+ # Padding special case: small k, n=2^k,
+ # more than k bits of padding needed,
+ # current vertex is not (n-1) --
+ # appending 1111... would add a loop on (n-1)
+ bits.append(0)
+ bits.extend([1] * ((-len(bits)) % 6))
+ else:
+ bits.extend([1] * ((-len(bits)) % 6))
+
+ data = [
+ (bits[i + 0] << 5)
+ + (bits[i + 1] << 4)
+ + (bits[i + 2] << 3)
+ + (bits[i + 3] << 2)
+ + (bits[i + 4] << 1)
+ + (bits[i + 5] << 0)
+ for i in range(0, len(bits), 6)
+ ]
+
+ for d in data:
+ yield str.encode(chr(d + 63))
+ yield b"\n"
+
+
+@nx._dispatchable(graphs=None, returns_graph=True)
+def from_sparse6_bytes(string):
+ """Read an undirected graph in sparse6 format from string.
+
+ Parameters
+ ----------
+ string : string
+ Data in sparse6 format
+
+ Returns
+ -------
+ G : Graph
+
+ Raises
+ ------
+ NetworkXError
+ If the string is unable to be parsed in sparse6 format
+
+ Examples
+ --------
+ >>> G = nx.from_sparse6_bytes(b":A_")
+ >>> sorted(G.edges())
+ [(0, 1), (0, 1), (0, 1)]
+
+ See Also
+ --------
+ read_sparse6, write_sparse6
+
+ References
+ ----------
+ .. [1] Sparse6 specification
+ <https://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ if string.startswith(b">>sparse6<<"):
+ string = string[11:]
+ if not string.startswith(b":"):
+ raise NetworkXError("Expected leading colon in sparse6")
+
+ chars = [c - 63 for c in string[1:]]
+ n, data = data_to_n(chars)
+ k = 1
+ while 1 << k < n:
+ k += 1
+
+ def parseData():
+ """Returns stream of pairs b[i], x[i] for sparse6 format."""
+ chunks = iter(data)
+ d = None # partial data word
+ dLen = 0 # how many unparsed bits are left in d
+
+ while 1:
+ if dLen < 1:
+ try:
+ d = next(chunks)
+ except StopIteration:
+ return
+ dLen = 6
+ dLen -= 1
+ b = (d >> dLen) & 1 # grab top remaining bit
+
+ x = d & ((1 << dLen) - 1) # partially built up value of x
+ xLen = dLen # how many bits included so far in x
+ while xLen < k: # now grab full chunks until we have enough
+ try:
+ d = next(chunks)
+ except StopIteration:
+ return
+ dLen = 6
+ x = (x << 6) + d
+ xLen += 6
+ x = x >> (xLen - k) # shift back the extra bits
+ dLen = xLen - k
+ yield b, x
+
+ v = 0
+
+ G = nx.MultiGraph()
+ G.add_nodes_from(range(n))
+
+ multigraph = False
+ for b, x in parseData():
+ if b == 1:
+ v += 1
+ # padding with ones can cause overlarge number here
+ if x >= n or v >= n:
+ break
+ elif x > v:
+ v = x
+ else:
+ if G.has_edge(x, v):
+ multigraph = True
+ G.add_edge(x, v)
+ if not multigraph:
+ G = nx.Graph(G)
+ return G
+
+
+def to_sparse6_bytes(G, nodes=None, header=True):
+ """Convert an undirected graph to bytes in sparse6 format.
+
+ Parameters
+ ----------
+ G : Graph (undirected)
+
+ nodes: list or iterable
+ Nodes are labeled 0...n-1 in the order provided. If None the ordering
+ given by ``G.nodes()`` is used.
+
+ header: bool
+ If True add '>>sparse6<<' bytes to head of data.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the graph is directed.
+
+ ValueError
+ If the graph has at least ``2 ** 36`` nodes; the sparse6 format
+ is only defined for graphs of order less than ``2 ** 36``.
+
+ Examples
+ --------
+ >>> nx.to_sparse6_bytes(nx.path_graph(2))
+ b'>>sparse6<<:An\\n'
+
+ See Also
+ --------
+ to_sparse6_bytes, read_sparse6, write_sparse6_bytes
+
+ Notes
+ -----
+ The returned bytes end with a newline character.
+
+ The format does not support edge or node labels.
+
+ References
+ ----------
+ .. [1] Graph6 specification
+ <https://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ if nodes is not None:
+ G = G.subgraph(nodes)
+ G = nx.convert_node_labels_to_integers(G, ordering="sorted")
+ return b"".join(_generate_sparse6_bytes(G, nodes, header))
+
+
+@open_file(0, mode="rb")
+@nx._dispatchable(graphs=None, returns_graph=True)
+def read_sparse6(path):
+ """Read an undirected graph in sparse6 format from path.
+
+ Parameters
+ ----------
+ path : file or string
+ File or filename to write.
+
+ Returns
+ -------
+ G : Graph/Multigraph or list of Graphs/MultiGraphs
+ If the file contains multiple lines then a list of graphs is returned
+
+ Raises
+ ------
+ NetworkXError
+ If the string is unable to be parsed in sparse6 format
+
+ Examples
+ --------
+ You can read a sparse6 file by giving the path to the file::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile(delete=False) as f:
+ ... _ = f.write(b">>sparse6<<:An\\n")
+ ... _ = f.seek(0)
+ ... G = nx.read_sparse6(f.name)
+ >>> list(G.edges())
+ [(0, 1)]
+
+ You can also read a sparse6 file by giving an open file-like object::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile() as f:
+ ... _ = f.write(b">>sparse6<<:An\\n")
+ ... _ = f.seek(0)
+ ... G = nx.read_sparse6(f)
+ >>> list(G.edges())
+ [(0, 1)]
+
+ See Also
+ --------
+ read_sparse6, from_sparse6_bytes
+
+ References
+ ----------
+ .. [1] Sparse6 specification
+ <https://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ glist = []
+ for line in path:
+ line = line.strip()
+ if not len(line):
+ continue
+ glist.append(from_sparse6_bytes(line))
+ if len(glist) == 1:
+ return glist[0]
+ else:
+ return glist
+
+
+@not_implemented_for("directed")
+@open_file(1, mode="wb")
+def write_sparse6(G, path, nodes=None, header=True):
+ """Write graph G to given path in sparse6 format.
+
+ Parameters
+ ----------
+ G : Graph (undirected)
+
+ path : file or string
+ File or filename to write
+
+ nodes: list or iterable
+ Nodes are labeled 0...n-1 in the order provided. If None the ordering
+ given by G.nodes() is used.
+
+ header: bool
+ If True add '>>sparse6<<' string to head of data
+
+ Raises
+ ------
+ NetworkXError
+ If the graph is directed
+
+ Examples
+ --------
+ You can write a sparse6 file by giving the path to the file::
+
+ >>> import tempfile
+ >>> with tempfile.NamedTemporaryFile(delete=False) as f:
+ ... nx.write_sparse6(nx.path_graph(2), f.name)
+ ... print(f.read())
+ b'>>sparse6<<:An\\n'
+
+ You can also write a sparse6 file by giving an open file-like object::
+
+ >>> with tempfile.NamedTemporaryFile() as f:
+ ... nx.write_sparse6(nx.path_graph(2), f)
+ ... _ = f.seek(0)
+ ... print(f.read())
+ b'>>sparse6<<:An\\n'
+
+ See Also
+ --------
+ read_sparse6, from_sparse6_bytes
+
+ Notes
+ -----
+ The format does not support edge or node labels.
+
+ References
+ ----------
+ .. [1] Sparse6 specification
+ <https://users.cecs.anu.edu.au/~bdm/data/formats.html>
+
+ """
+ if nodes is not None:
+ G = G.subgraph(nodes)
+ G = nx.convert_node_labels_to_integers(G, ordering="sorted")
+ for b in _generate_sparse6_bytes(G, nodes, header):
+ path.write(b)
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py
new file mode 100644
index 00000000..f2218eba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py
@@ -0,0 +1,262 @@
+"""
+Unit tests for adjlist.
+"""
+
+import io
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, graphs_equal, nodes_equal
+
+
+class TestAdjlist:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.Graph(name="test")
+ e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")]
+ cls.G.add_edges_from(e)
+ cls.G.add_node("g")
+ cls.DG = nx.DiGraph(cls.G)
+ cls.XG = nx.MultiGraph()
+ cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)])
+ cls.XDG = nx.MultiDiGraph(cls.XG)
+
+ def test_read_multiline_adjlist_1(self):
+ # Unit test for https://networkx.lanl.gov/trac/ticket/252
+ s = b"""# comment line
+1 2
+# comment line
+2
+3
+"""
+ bytesIO = io.BytesIO(s)
+ G = nx.read_multiline_adjlist(bytesIO)
+ adj = {"1": {"3": {}, "2": {}}, "3": {"1": {}}, "2": {"1": {}}}
+ assert graphs_equal(G, nx.Graph(adj))
+
+ def test_unicode(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+
+ fname = tmp_path / "adjlist.txt"
+ nx.write_multiline_adjlist(G, fname)
+ H = nx.read_multiline_adjlist(fname)
+ assert graphs_equal(G, H)
+
+ def test_latin1_err(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ fname = tmp_path / "adjlist.txt"
+ with pytest.raises(UnicodeEncodeError):
+ nx.write_multiline_adjlist(G, fname, encoding="latin-1")
+
+ def test_latin1(self, tmp_path):
+ G = nx.Graph()
+ name1 = "Bj" + chr(246) + "rk"
+ name2 = chr(220) + "ber"
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ fname = tmp_path / "adjlist.txt"
+ nx.write_multiline_adjlist(G, fname, encoding="latin-1")
+ H = nx.read_multiline_adjlist(fname, encoding="latin-1")
+ assert graphs_equal(G, H)
+
+ def test_parse_adjlist(self):
+ lines = ["1 2 5", "2 3 4", "3 5", "4", "5"]
+ nx.parse_adjlist(lines, nodetype=int) # smoke test
+ with pytest.raises(TypeError):
+ nx.parse_adjlist(lines, nodetype="int")
+ lines = ["1 2 5", "2 b", "c"]
+ with pytest.raises(TypeError):
+ nx.parse_adjlist(lines, nodetype=int)
+
+ def test_adjlist_graph(self, tmp_path):
+ G = self.G
+ fname = tmp_path / "adjlist.txt"
+ nx.write_adjlist(G, fname)
+ H = nx.read_adjlist(fname)
+ H2 = nx.read_adjlist(fname)
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_adjlist_digraph(self, tmp_path):
+ G = self.DG
+ fname = tmp_path / "adjlist.txt"
+ nx.write_adjlist(G, fname)
+ H = nx.read_adjlist(fname, create_using=nx.DiGraph())
+ H2 = nx.read_adjlist(fname, create_using=nx.DiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_adjlist_integers(self, tmp_path):
+ fname = tmp_path / "adjlist.txt"
+ G = nx.convert_node_labels_to_integers(self.G)
+ nx.write_adjlist(G, fname)
+ H = nx.read_adjlist(fname, nodetype=int)
+ H2 = nx.read_adjlist(fname, nodetype=int)
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_adjlist_multigraph(self, tmp_path):
+ G = self.XG
+ fname = tmp_path / "adjlist.txt"
+ nx.write_adjlist(G, fname)
+ H = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiGraph())
+ H2 = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_adjlist_multidigraph(self, tmp_path):
+ G = self.XDG
+ fname = tmp_path / "adjlist.txt"
+ nx.write_adjlist(G, fname)
+ H = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiDiGraph())
+ H2 = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiDiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_adjlist_delimiter(self):
+ fh = io.BytesIO()
+ G = nx.path_graph(3)
+ nx.write_adjlist(G, fh, delimiter=":")
+ fh.seek(0)
+ H = nx.read_adjlist(fh, nodetype=int, delimiter=":")
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+
+class TestMultilineAdjlist:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.Graph(name="test")
+ e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")]
+ cls.G.add_edges_from(e)
+ cls.G.add_node("g")
+ cls.DG = nx.DiGraph(cls.G)
+ cls.DG.remove_edge("b", "a")
+ cls.DG.remove_edge("b", "c")
+ cls.XG = nx.MultiGraph()
+ cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)])
+ cls.XDG = nx.MultiDiGraph(cls.XG)
+
+ def test_parse_multiline_adjlist(self):
+ lines = [
+ "1 2",
+ "b {'weight':3, 'name': 'Frodo'}",
+ "c {}",
+ "d 1",
+ "e {'weight':6, 'name': 'Saruman'}",
+ ]
+ nx.parse_multiline_adjlist(iter(lines)) # smoke test
+ with pytest.raises(TypeError):
+ nx.parse_multiline_adjlist(iter(lines), nodetype=int)
+ nx.parse_multiline_adjlist(iter(lines), edgetype=str) # smoke test
+ with pytest.raises(TypeError):
+ nx.parse_multiline_adjlist(iter(lines), nodetype=int)
+ lines = ["1 a"]
+ with pytest.raises(TypeError):
+ nx.parse_multiline_adjlist(iter(lines))
+ lines = ["a 2"]
+ with pytest.raises(TypeError):
+ nx.parse_multiline_adjlist(iter(lines), nodetype=int)
+ lines = ["1 2"]
+ with pytest.raises(TypeError):
+ nx.parse_multiline_adjlist(iter(lines))
+ lines = ["1 2", "2 {}"]
+ with pytest.raises(TypeError):
+ nx.parse_multiline_adjlist(iter(lines))
+
+ def test_multiline_adjlist_graph(self, tmp_path):
+ G = self.G
+ fname = tmp_path / "adjlist.txt"
+ nx.write_multiline_adjlist(G, fname)
+ H = nx.read_multiline_adjlist(fname)
+ H2 = nx.read_multiline_adjlist(fname)
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_multiline_adjlist_digraph(self, tmp_path):
+ G = self.DG
+ fname = tmp_path / "adjlist.txt"
+ nx.write_multiline_adjlist(G, fname)
+ H = nx.read_multiline_adjlist(fname, create_using=nx.DiGraph())
+ H2 = nx.read_multiline_adjlist(fname, create_using=nx.DiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_multiline_adjlist_integers(self, tmp_path):
+ fname = tmp_path / "adjlist.txt"
+ G = nx.convert_node_labels_to_integers(self.G)
+ nx.write_multiline_adjlist(G, fname)
+ H = nx.read_multiline_adjlist(fname, nodetype=int)
+ H2 = nx.read_multiline_adjlist(fname, nodetype=int)
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_multiline_adjlist_multigraph(self, tmp_path):
+ G = self.XG
+ fname = tmp_path / "adjlist.txt"
+ nx.write_multiline_adjlist(G, fname)
+ H = nx.read_multiline_adjlist(fname, nodetype=int, create_using=nx.MultiGraph())
+ H2 = nx.read_multiline_adjlist(
+ fname, nodetype=int, create_using=nx.MultiGraph()
+ )
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_multiline_adjlist_multidigraph(self, tmp_path):
+ G = self.XDG
+ fname = tmp_path / "adjlist.txt"
+ nx.write_multiline_adjlist(G, fname)
+ H = nx.read_multiline_adjlist(
+ fname, nodetype=int, create_using=nx.MultiDiGraph()
+ )
+ H2 = nx.read_multiline_adjlist(
+ fname, nodetype=int, create_using=nx.MultiDiGraph()
+ )
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_multiline_adjlist_delimiter(self):
+ fh = io.BytesIO()
+ G = nx.path_graph(3)
+ nx.write_multiline_adjlist(G, fh, delimiter=":")
+ fh.seek(0)
+ H = nx.read_multiline_adjlist(fh, nodetype=int, delimiter=":")
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+
+@pytest.mark.parametrize(
+ ("lines", "delim"),
+ (
+ (["1 2 5", "2 3 4", "3 5", "4", "5"], None), # No extra whitespace
+ (["1\t2\t5", "2\t3\t4", "3\t5", "4", "5"], "\t"), # tab-delimited
+ (
+ ["1\t2\t5", "2\t3\t4", "3\t5\t", "4\t", "5"],
+ "\t",
+ ), # tab-delimited, extra delims
+ (
+ ["1\t2\t5", "2\t3\t4", "3\t5\t\t\n", "4\t", "5"],
+ "\t",
+ ), # extra delim+newlines
+ ),
+)
+def test_adjlist_rstrip_parsing(lines, delim):
+ """Regression test related to gh-7465"""
+ expected = nx.Graph([(1, 2), (1, 5), (2, 3), (2, 4), (3, 5)])
+ nx.utils.graphs_equal(nx.parse_adjlist(lines, delimiter=delim), expected)
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py
new file mode 100644
index 00000000..fe58b3b7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py
@@ -0,0 +1,314 @@
+"""
+Unit tests for edgelists.
+"""
+
+import io
+import textwrap
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, graphs_equal, nodes_equal
+
+edges_no_data = textwrap.dedent(
+ """
+ # comment line
+ 1 2
+ # comment line
+ 2 3
+ """
+)
+
+
+edges_with_values = textwrap.dedent(
+ """
+ # comment line
+ 1 2 2.0
+ # comment line
+ 2 3 3.0
+ """
+)
+
+
+edges_with_weight = textwrap.dedent(
+ """
+ # comment line
+ 1 2 {'weight':2.0}
+ # comment line
+ 2 3 {'weight':3.0}
+ """
+)
+
+
+edges_with_multiple_attrs = textwrap.dedent(
+ """
+ # comment line
+ 1 2 {'weight':2.0, 'color':'green'}
+ # comment line
+ 2 3 {'weight':3.0, 'color':'red'}
+ """
+)
+
+
+edges_with_multiple_attrs_csv = textwrap.dedent(
+ """
+ # comment line
+ 1, 2, {'weight':2.0, 'color':'green'}
+ # comment line
+ 2, 3, {'weight':3.0, 'color':'red'}
+ """
+)
+
+
+_expected_edges_weights = [(1, 2, {"weight": 2.0}), (2, 3, {"weight": 3.0})]
+_expected_edges_multiattr = [
+ (1, 2, {"weight": 2.0, "color": "green"}),
+ (2, 3, {"weight": 3.0, "color": "red"}),
+]
+
+
+@pytest.mark.parametrize(
+ ("data", "extra_kwargs"),
+ (
+ (edges_no_data, {}),
+ (edges_with_values, {}),
+ (edges_with_weight, {}),
+ (edges_with_multiple_attrs, {}),
+ (edges_with_multiple_attrs_csv, {"delimiter": ","}),
+ ),
+)
+def test_read_edgelist_no_data(data, extra_kwargs):
+ bytesIO = io.BytesIO(data.encode("utf-8"))
+ G = nx.read_edgelist(bytesIO, nodetype=int, data=False, **extra_kwargs)
+ assert edges_equal(G.edges(), [(1, 2), (2, 3)])
+
+
+def test_read_weighted_edgelist():
+ bytesIO = io.BytesIO(edges_with_values.encode("utf-8"))
+ G = nx.read_weighted_edgelist(bytesIO, nodetype=int)
+ assert edges_equal(G.edges(data=True), _expected_edges_weights)
+
+
+@pytest.mark.parametrize(
+ ("data", "extra_kwargs", "expected"),
+ (
+ (edges_with_weight, {}, _expected_edges_weights),
+ (edges_with_multiple_attrs, {}, _expected_edges_multiattr),
+ (edges_with_multiple_attrs_csv, {"delimiter": ","}, _expected_edges_multiattr),
+ ),
+)
+def test_read_edgelist_with_data(data, extra_kwargs, expected):
+ bytesIO = io.BytesIO(data.encode("utf-8"))
+ G = nx.read_edgelist(bytesIO, nodetype=int, **extra_kwargs)
+ assert edges_equal(G.edges(data=True), expected)
+
+
+@pytest.fixture
+def example_graph():
+ G = nx.Graph()
+ G.add_weighted_edges_from([(1, 2, 3.0), (2, 3, 27.0), (3, 4, 3.0)])
+ return G
+
+
+def test_parse_edgelist_no_data(example_graph):
+ G = example_graph
+ H = nx.parse_edgelist(["1 2", "2 3", "3 4"], nodetype=int)
+ assert nodes_equal(G.nodes, H.nodes)
+ assert edges_equal(G.edges, H.edges)
+
+
+def test_parse_edgelist_with_data_dict(example_graph):
+ G = example_graph
+ H = nx.parse_edgelist(
+ ["1 2 {'weight': 3}", "2 3 {'weight': 27}", "3 4 {'weight': 3.0}"], nodetype=int
+ )
+ assert nodes_equal(G.nodes, H.nodes)
+ assert edges_equal(G.edges(data=True), H.edges(data=True))
+
+
+def test_parse_edgelist_with_data_list(example_graph):
+ G = example_graph
+ H = nx.parse_edgelist(
+ ["1 2 3", "2 3 27", "3 4 3.0"], nodetype=int, data=(("weight", float),)
+ )
+ assert nodes_equal(G.nodes, H.nodes)
+ assert edges_equal(G.edges(data=True), H.edges(data=True))
+
+
+def test_parse_edgelist():
+ # ignore lines with less than 2 nodes
+ lines = ["1;2", "2 3", "3 4"]
+ G = nx.parse_edgelist(lines, nodetype=int)
+ assert list(G.edges()) == [(2, 3), (3, 4)]
+ # unknown nodetype
+ with pytest.raises(TypeError, match="Failed to convert nodes"):
+ lines = ["1 2", "2 3", "3 4"]
+ nx.parse_edgelist(lines, nodetype="nope")
+ # lines have invalid edge format
+ with pytest.raises(TypeError, match="Failed to convert edge data"):
+ lines = ["1 2 3", "2 3", "3 4"]
+ nx.parse_edgelist(lines, nodetype=int)
+ # edge data and data_keys not the same length
+ with pytest.raises(IndexError, match="not the same length"):
+ lines = ["1 2 3", "2 3 27", "3 4 3.0"]
+ nx.parse_edgelist(
+ lines, nodetype=int, data=(("weight", float), ("capacity", int))
+ )
+ # edge data can't be converted to edge type
+ with pytest.raises(TypeError, match="Failed to convert"):
+ lines = ["1 2 't1'", "2 3 't3'", "3 4 't3'"]
+ nx.parse_edgelist(lines, nodetype=int, data=(("weight", float),))
+
+
+def test_comments_None():
+ edgelist = ["node#1 node#2", "node#2 node#3"]
+ # comments=None supported to ignore all comment characters
+ G = nx.parse_edgelist(edgelist, comments=None)
+ H = nx.Graph([e.split(" ") for e in edgelist])
+ assert edges_equal(G.edges, H.edges)
+
+
+class TestEdgelist:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.Graph(name="test")
+ e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")]
+ cls.G.add_edges_from(e)
+ cls.G.add_node("g")
+ cls.DG = nx.DiGraph(cls.G)
+ cls.XG = nx.MultiGraph()
+ cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)])
+ cls.XDG = nx.MultiDiGraph(cls.XG)
+
+ def test_write_edgelist_1(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edges_from([(1, 2), (2, 3)])
+ nx.write_edgelist(G, fh, data=False)
+ fh.seek(0)
+ assert fh.read() == b"1 2\n2 3\n"
+
+ def test_write_edgelist_2(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edges_from([(1, 2), (2, 3)])
+ nx.write_edgelist(G, fh, data=True)
+ fh.seek(0)
+ assert fh.read() == b"1 2 {}\n2 3 {}\n"
+
+ def test_write_edgelist_3(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edge(1, 2, weight=2.0)
+ G.add_edge(2, 3, weight=3.0)
+ nx.write_edgelist(G, fh, data=True)
+ fh.seek(0)
+ assert fh.read() == b"1 2 {'weight': 2.0}\n2 3 {'weight': 3.0}\n"
+
+ def test_write_edgelist_4(self):
+ fh = io.BytesIO()
+ G = nx.Graph()
+ G.add_edge(1, 2, weight=2.0)
+ G.add_edge(2, 3, weight=3.0)
+ nx.write_edgelist(G, fh, data=[("weight")])
+ fh.seek(0)
+ assert fh.read() == b"1 2 2.0\n2 3 3.0\n"
+
+ def test_unicode(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ fname = tmp_path / "el.txt"
+ nx.write_edgelist(G, fname)
+ H = nx.read_edgelist(fname)
+ assert graphs_equal(G, H)
+
+ def test_latin1_issue(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ fname = tmp_path / "el.txt"
+ with pytest.raises(UnicodeEncodeError):
+ nx.write_edgelist(G, fname, encoding="latin-1")
+
+ def test_latin1(self, tmp_path):
+ G = nx.Graph()
+ name1 = "Bj" + chr(246) + "rk"
+ name2 = chr(220) + "ber"
+ G.add_edge(name1, "Radiohead", **{name2: 3})
+ fname = tmp_path / "el.txt"
+
+ nx.write_edgelist(G, fname, encoding="latin-1")
+ H = nx.read_edgelist(fname, encoding="latin-1")
+ assert graphs_equal(G, H)
+
+ def test_edgelist_graph(self, tmp_path):
+ G = self.G
+ fname = tmp_path / "el.txt"
+ nx.write_edgelist(G, fname)
+ H = nx.read_edgelist(fname)
+ H2 = nx.read_edgelist(fname)
+ assert H is not H2 # they should be different graphs
+ G.remove_node("g") # isolated nodes are not written in edgelist
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_edgelist_digraph(self, tmp_path):
+ G = self.DG
+ fname = tmp_path / "el.txt"
+ nx.write_edgelist(G, fname)
+ H = nx.read_edgelist(fname, create_using=nx.DiGraph())
+ H2 = nx.read_edgelist(fname, create_using=nx.DiGraph())
+ assert H is not H2 # they should be different graphs
+ G.remove_node("g") # isolated nodes are not written in edgelist
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_edgelist_integers(self, tmp_path):
+ G = nx.convert_node_labels_to_integers(self.G)
+ fname = tmp_path / "el.txt"
+ nx.write_edgelist(G, fname)
+ H = nx.read_edgelist(fname, nodetype=int)
+ # isolated nodes are not written in edgelist
+ G.remove_nodes_from(list(nx.isolates(G)))
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_edgelist_multigraph(self, tmp_path):
+ G = self.XG
+ fname = tmp_path / "el.txt"
+ nx.write_edgelist(G, fname)
+ H = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph())
+ H2 = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+ def test_edgelist_multidigraph(self, tmp_path):
+ G = self.XDG
+ fname = tmp_path / "el.txt"
+ nx.write_edgelist(G, fname)
+ H = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiDiGraph())
+ H2 = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiDiGraph())
+ assert H is not H2 # they should be different graphs
+ assert nodes_equal(list(H), list(G))
+ assert edges_equal(list(H.edges()), list(G.edges()))
+
+
+def test_edgelist_consistent_strip_handling():
+ """See gh-7462
+
+ Input when printed looks like::
+
+ 1 2 3
+ 2 3
+ 3 4 3.0
+
+ Note the trailing \\t after the `3` in the second row, indicating an empty
+ data value.
+ """
+ s = io.StringIO("1\t2\t3\n2\t3\t\n3\t4\t3.0")
+ G = nx.parse_edgelist(s, delimiter="\t", nodetype=int, data=[("value", str)])
+ assert sorted(G.edges(data="value")) == [(1, 2, "3"), (2, 3, ""), (3, 4, "3.0")]
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py
new file mode 100644
index 00000000..6ff14c99
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py
@@ -0,0 +1,557 @@
+import io
+import time
+
+import pytest
+
+import networkx as nx
+
+
+class TestGEXF:
+ @classmethod
+ def setup_class(cls):
+ cls.simple_directed_data = """<?xml version="1.0" encoding="UTF-8"?>
+<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2">
+ <graph mode="static" defaultedgetype="directed">
+ <nodes>
+ <node id="0" label="Hello" />
+ <node id="1" label="Word" />
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1" />
+ </edges>
+ </graph>
+</gexf>
+"""
+ cls.simple_directed_graph = nx.DiGraph()
+ cls.simple_directed_graph.add_node("0", label="Hello")
+ cls.simple_directed_graph.add_node("1", label="World")
+ cls.simple_directed_graph.add_edge("0", "1", id="0")
+
+ cls.simple_directed_fh = io.BytesIO(cls.simple_directed_data.encode("UTF-8"))
+
+ cls.attribute_data = """<?xml version="1.0" encoding="UTF-8"?>\
+<gexf xmlns="http://www.gexf.net/1.2draft" xmlns:xsi="http://www.w3.\
+org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/\
+1.2draft http://www.gexf.net/1.2draft/gexf.xsd" version="1.2">
+ <meta lastmodifieddate="2009-03-20">
+ <creator>Gephi.org</creator>
+ <description>A Web network</description>
+ </meta>
+ <graph defaultedgetype="directed">
+ <attributes class="node">
+ <attribute id="0" title="url" type="string"/>
+ <attribute id="1" title="indegree" type="integer"/>
+ <attribute id="2" title="frog" type="boolean">
+ <default>true</default>
+ </attribute>
+ </attributes>
+ <nodes>
+ <node id="0" label="Gephi">
+ <attvalues>
+ <attvalue for="0" value="https://gephi.org"/>
+ <attvalue for="1" value="1"/>
+ <attvalue for="2" value="false"/>
+ </attvalues>
+ </node>
+ <node id="1" label="Webatlas">
+ <attvalues>
+ <attvalue for="0" value="http://webatlas.fr"/>
+ <attvalue for="1" value="2"/>
+ <attvalue for="2" value="false"/>
+ </attvalues>
+ </node>
+ <node id="2" label="RTGI">
+ <attvalues>
+ <attvalue for="0" value="http://rtgi.fr"/>
+ <attvalue for="1" value="1"/>
+ <attvalue for="2" value="true"/>
+ </attvalues>
+ </node>
+ <node id="3" label="BarabasiLab">
+ <attvalues>
+ <attvalue for="0" value="http://barabasilab.com"/>
+ <attvalue for="1" value="1"/>
+ <attvalue for="2" value="true"/>
+ </attvalues>
+ </node>
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1" label="foo"/>
+ <edge id="1" source="0" target="2"/>
+ <edge id="2" source="1" target="0"/>
+ <edge id="3" source="2" target="1"/>
+ <edge id="4" source="0" target="3"/>
+ </edges>
+ </graph>
+</gexf>
+"""
+ cls.attribute_graph = nx.DiGraph()
+ cls.attribute_graph.graph["node_default"] = {"frog": True}
+ cls.attribute_graph.add_node(
+ "0", label="Gephi", url="https://gephi.org", indegree=1, frog=False
+ )
+ cls.attribute_graph.add_node(
+ "1", label="Webatlas", url="http://webatlas.fr", indegree=2, frog=False
+ )
+ cls.attribute_graph.add_node(
+ "2", label="RTGI", url="http://rtgi.fr", indegree=1, frog=True
+ )
+ cls.attribute_graph.add_node(
+ "3",
+ label="BarabasiLab",
+ url="http://barabasilab.com",
+ indegree=1,
+ frog=True,
+ )
+ cls.attribute_graph.add_edge("0", "1", id="0", label="foo")
+ cls.attribute_graph.add_edge("0", "2", id="1")
+ cls.attribute_graph.add_edge("1", "0", id="2")
+ cls.attribute_graph.add_edge("2", "1", id="3")
+ cls.attribute_graph.add_edge("0", "3", id="4")
+ cls.attribute_fh = io.BytesIO(cls.attribute_data.encode("UTF-8"))
+
+ cls.simple_undirected_data = """<?xml version="1.0" encoding="UTF-8"?>
+<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2">
+ <graph mode="static" defaultedgetype="undirected">
+ <nodes>
+ <node id="0" label="Hello" />
+ <node id="1" label="Word" />
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1" />
+ </edges>
+ </graph>
+</gexf>
+"""
+ cls.simple_undirected_graph = nx.Graph()
+ cls.simple_undirected_graph.add_node("0", label="Hello")
+ cls.simple_undirected_graph.add_node("1", label="World")
+ cls.simple_undirected_graph.add_edge("0", "1", id="0")
+
+ cls.simple_undirected_fh = io.BytesIO(
+ cls.simple_undirected_data.encode("UTF-8")
+ )
+
+ def test_read_simple_directed_graphml(self):
+ G = self.simple_directed_graph
+ H = nx.read_gexf(self.simple_directed_fh)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(G.edges()) == sorted(H.edges())
+ assert sorted(G.edges(data=True)) == sorted(H.edges(data=True))
+ self.simple_directed_fh.seek(0)
+
+ def test_write_read_simple_directed_graphml(self):
+ G = self.simple_directed_graph
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(G.edges()) == sorted(H.edges())
+ assert sorted(G.edges(data=True)) == sorted(H.edges(data=True))
+ self.simple_directed_fh.seek(0)
+
+ def test_read_simple_undirected_graphml(self):
+ G = self.simple_undirected_graph
+ H = nx.read_gexf(self.simple_undirected_fh)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+ self.simple_undirected_fh.seek(0)
+
+ def test_read_attribute_graphml(self):
+ G = self.attribute_graph
+ H = nx.read_gexf(self.attribute_fh)
+ assert sorted(G.nodes(True)) == sorted(H.nodes(data=True))
+ ge = sorted(G.edges(data=True))
+ he = sorted(H.edges(data=True))
+ for a, b in zip(ge, he):
+ assert a == b
+ self.attribute_fh.seek(0)
+
+ def test_directed_edge_in_undirected(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<gexf xmlns="http://www.gexf.net/1.2draft" version='1.2'>
+ <graph mode="static" defaultedgetype="undirected" name="">
+ <nodes>
+ <node id="0" label="Hello" />
+ <node id="1" label="Word" />
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1" type="directed"/>
+ </edges>
+ </graph>
+</gexf>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_gexf, fh)
+
+ def test_undirected_edge_in_directed(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<gexf xmlns="http://www.gexf.net/1.2draft" version='1.2'>
+ <graph mode="static" defaultedgetype="directed" name="">
+ <nodes>
+ <node id="0" label="Hello" />
+ <node id="1" label="Word" />
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1" type="undirected"/>
+ </edges>
+ </graph>
+</gexf>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_gexf, fh)
+
+ def test_key_raises(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<gexf xmlns="http://www.gexf.net/1.2draft" version='1.2'>
+ <graph mode="static" defaultedgetype="directed" name="">
+ <nodes>
+ <node id="0" label="Hello">
+ <attvalues>
+ <attvalue for='0' value='1'/>
+ </attvalues>
+ </node>
+ <node id="1" label="Word" />
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1" type="undirected"/>
+ </edges>
+ </graph>
+</gexf>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_gexf, fh)
+
+ def test_relabel(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<gexf xmlns="http://www.gexf.net/1.2draft" version='1.2'>
+ <graph mode="static" defaultedgetype="directed" name="">
+ <nodes>
+ <node id="0" label="Hello" />
+ <node id="1" label="Word" />
+ </nodes>
+ <edges>
+ <edge id="0" source="0" target="1"/>
+ </edges>
+ </graph>
+</gexf>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ G = nx.read_gexf(fh, relabel=True)
+ assert sorted(G.nodes()) == ["Hello", "Word"]
+
+ def test_default_attribute(self):
+ G = nx.Graph()
+ G.add_node(1, label="1", color="green")
+ nx.add_path(G, [0, 1, 2, 3])
+ G.add_edge(1, 2, foo=3)
+ G.graph["node_default"] = {"color": "yellow"}
+ G.graph["edge_default"] = {"foo": 7}
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+ # Reading a gexf graph always sets mode attribute to either
+ # 'static' or 'dynamic'. Remove the mode attribute from the
+ # read graph for the sake of comparing remaining attributes.
+ del H.graph["mode"]
+ assert G.graph == H.graph
+
+ def test_serialize_ints_to_strings(self):
+ G = nx.Graph()
+ G.add_node(1, id=7, label=77)
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert list(H) == [7]
+ assert H.nodes[7]["label"] == "77"
+
+ def test_write_with_node_attributes(self):
+ # Addresses #673.
+ G = nx.Graph()
+ G.add_edges_from([(0, 1), (1, 2), (2, 3)])
+ for i in range(4):
+ G.nodes[i]["id"] = i
+ G.nodes[i]["label"] = i
+ G.nodes[i]["pid"] = i
+ G.nodes[i]["start"] = i
+ G.nodes[i]["end"] = i + 1
+
+ expected = f"""<gexf xmlns="http://www.gexf.net/1.2draft" xmlns:xsi\
+="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=\
+"http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/\
+gexf.xsd" version="1.2">
+ <meta lastmodifieddate="{time.strftime('%Y-%m-%d')}">
+ <creator>NetworkX {nx.__version__}</creator>
+ </meta>
+ <graph defaultedgetype="undirected" mode="dynamic" name="" timeformat="long">
+ <nodes>
+ <node id="0" label="0" pid="0" start="0" end="1" />
+ <node id="1" label="1" pid="1" start="1" end="2" />
+ <node id="2" label="2" pid="2" start="2" end="3" />
+ <node id="3" label="3" pid="3" start="3" end="4" />
+ </nodes>
+ <edges>
+ <edge source="0" target="1" id="0" />
+ <edge source="1" target="2" id="1" />
+ <edge source="2" target="3" id="2" />
+ </edges>
+ </graph>
+</gexf>"""
+ obtained = "\n".join(nx.generate_gexf(G))
+ assert expected == obtained
+
+ def test_edge_id_construct(self):
+ G = nx.Graph()
+ G.add_edges_from([(0, 1, {"id": 0}), (1, 2, {"id": 2}), (2, 3)])
+
+ expected = f"""<gexf xmlns="http://www.gexf.net/1.2draft" xmlns:xsi\
+="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.\
+gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd" version="1.2">
+ <meta lastmodifieddate="{time.strftime('%Y-%m-%d')}">
+ <creator>NetworkX {nx.__version__}</creator>
+ </meta>
+ <graph defaultedgetype="undirected" mode="static" name="">
+ <nodes>
+ <node id="0" label="0" />
+ <node id="1" label="1" />
+ <node id="2" label="2" />
+ <node id="3" label="3" />
+ </nodes>
+ <edges>
+ <edge source="0" target="1" id="0" />
+ <edge source="1" target="2" id="2" />
+ <edge source="2" target="3" id="1" />
+ </edges>
+ </graph>
+</gexf>"""
+
+ obtained = "\n".join(nx.generate_gexf(G))
+ assert expected == obtained
+
+ def test_numpy_type(self):
+ np = pytest.importorskip("numpy")
+ G = nx.path_graph(4)
+ nx.set_node_attributes(G, {n: n for n in np.arange(4)}, "number")
+ G[0][1]["edge-number"] = np.float64(1.1)
+
+ expected = f"""<gexf xmlns="http://www.gexf.net/1.2draft"\
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation\
+="http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd"\
+ version="1.2">
+ <meta lastmodifieddate="{time.strftime('%Y-%m-%d')}">
+ <creator>NetworkX {nx.__version__}</creator>
+ </meta>
+ <graph defaultedgetype="undirected" mode="static" name="">
+ <attributes mode="static" class="edge">
+ <attribute id="1" title="edge-number" type="float" />
+ </attributes>
+ <attributes mode="static" class="node">
+ <attribute id="0" title="number" type="int" />
+ </attributes>
+ <nodes>
+ <node id="0" label="0">
+ <attvalues>
+ <attvalue for="0" value="0" />
+ </attvalues>
+ </node>
+ <node id="1" label="1">
+ <attvalues>
+ <attvalue for="0" value="1" />
+ </attvalues>
+ </node>
+ <node id="2" label="2">
+ <attvalues>
+ <attvalue for="0" value="2" />
+ </attvalues>
+ </node>
+ <node id="3" label="3">
+ <attvalues>
+ <attvalue for="0" value="3" />
+ </attvalues>
+ </node>
+ </nodes>
+ <edges>
+ <edge source="0" target="1" id="0">
+ <attvalues>
+ <attvalue for="1" value="1.1" />
+ </attvalues>
+ </edge>
+ <edge source="1" target="2" id="1" />
+ <edge source="2" target="3" id="2" />
+ </edges>
+ </graph>
+</gexf>"""
+ obtained = "\n".join(nx.generate_gexf(G))
+ assert expected == obtained
+
+ def test_bool(self):
+ G = nx.Graph()
+ G.add_node(1, testattr=True)
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert H.nodes[1]["testattr"]
+
+ # Test for NaN, INF and -INF
+ def test_specials(self):
+ from math import isnan
+
+ inf, nan = float("inf"), float("nan")
+ G = nx.Graph()
+ G.add_node(1, testattr=inf, strdata="inf", key="a")
+ G.add_node(2, testattr=nan, strdata="nan", key="b")
+ G.add_node(3, testattr=-inf, strdata="-inf", key="c")
+
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ filetext = fh.read()
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+
+ assert b"INF" in filetext
+ assert b"NaN" in filetext
+ assert b"-INF" in filetext
+
+ assert H.nodes[1]["testattr"] == inf
+ assert isnan(H.nodes[2]["testattr"])
+ assert H.nodes[3]["testattr"] == -inf
+
+ assert H.nodes[1]["strdata"] == "inf"
+ assert H.nodes[2]["strdata"] == "nan"
+ assert H.nodes[3]["strdata"] == "-inf"
+
+ assert H.nodes[1]["networkx_key"] == "a"
+ assert H.nodes[2]["networkx_key"] == "b"
+ assert H.nodes[3]["networkx_key"] == "c"
+
+ def test_simple_list(self):
+ G = nx.Graph()
+ list_value = [(1, 2, 3), (9, 1, 2)]
+ G.add_node(1, key=list_value)
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert H.nodes[1]["networkx_key"] == list_value
+
+ def test_dynamic_mode(self):
+ G = nx.Graph()
+ G.add_node(1, label="1", color="green")
+ G.graph["mode"] = "dynamic"
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+
+ def test_multigraph_with_missing_attributes(self):
+ G = nx.MultiGraph()
+ G.add_node(0, label="1", color="green")
+ G.add_node(1, label="2", color="green")
+ G.add_edge(0, 1, id="0", weight=3, type="undirected", start=0, end=1)
+ G.add_edge(0, 1, id="1", label="foo", start=0, end=1)
+ G.add_edge(0, 1)
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+
+ def test_missing_viz_attributes(self):
+ G = nx.Graph()
+ G.add_node(0, label="1", color="green")
+ G.nodes[0]["viz"] = {"size": 54}
+ G.nodes[0]["viz"]["position"] = {"x": 0, "y": 1, "z": 0}
+ G.nodes[0]["viz"]["color"] = {"r": 0, "g": 0, "b": 256}
+ G.nodes[0]["viz"]["shape"] = "http://random.url"
+ G.nodes[0]["viz"]["thickness"] = 2
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh, version="1.1draft")
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+
+ # Test missing alpha value for version >draft1.1 - set default alpha value
+ # to 1.0 instead of `None` when writing for better general compatibility
+ fh = io.BytesIO()
+ # G.nodes[0]["viz"]["color"] does not have an alpha value explicitly defined
+ # so the default is used instead
+ nx.write_gexf(G, fh, version="1.2draft")
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert H.nodes[0]["viz"]["color"]["a"] == 1.0
+
+ # Second graph for the other branch
+ G = nx.Graph()
+ G.add_node(0, label="1", color="green")
+ G.nodes[0]["viz"] = {"size": 54}
+ G.nodes[0]["viz"]["position"] = {"x": 0, "y": 1, "z": 0}
+ G.nodes[0]["viz"]["color"] = {"r": 0, "g": 0, "b": 256, "a": 0.5}
+ G.nodes[0]["viz"]["shape"] = "ftp://random.url"
+ G.nodes[0]["viz"]["thickness"] = 2
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+
+ def test_slice_and_spell(self):
+ # Test spell first, so version = 1.2
+ G = nx.Graph()
+ G.add_node(0, label="1", color="green")
+ G.nodes[0]["spells"] = [(1, 2)]
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+
+ G = nx.Graph()
+ G.add_node(0, label="1", color="green")
+ G.nodes[0]["slices"] = [(1, 2)]
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh, version="1.1draft")
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
+
+ def test_add_parent(self):
+ G = nx.Graph()
+ G.add_node(0, label="1", color="green", parents=[1, 2])
+ fh = io.BytesIO()
+ nx.write_gexf(G, fh)
+ fh.seek(0)
+ H = nx.read_gexf(fh, node_type=int)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(sorted(e) for e in G.edges()) == sorted(
+ sorted(e) for e in H.edges()
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py
new file mode 100644
index 00000000..f575ad26
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py
@@ -0,0 +1,744 @@
+import codecs
+import io
+import math
+from ast import literal_eval
+from contextlib import contextmanager
+from textwrap import dedent
+
+import pytest
+
+import networkx as nx
+from networkx.readwrite.gml import literal_destringizer, literal_stringizer
+
+
+class TestGraph:
+ @classmethod
+ def setup_class(cls):
+ cls.simple_data = """Creator "me"
+Version "xx"
+graph [
+ comment "This is a sample graph"
+ directed 1
+ IsPlanar 1
+ pos [ x 0 y 1 ]
+ node [
+ id 1
+ label "Node 1"
+ pos [ x 1 y 1 ]
+ ]
+ node [
+ id 2
+ pos [ x 1 y 2 ]
+ label "Node 2"
+ ]
+ node [
+ id 3
+ label "Node 3"
+ pos [ x 1 y 3 ]
+ ]
+ edge [
+ source 1
+ target 2
+ label "Edge from node 1 to node 2"
+ color [line "blue" thickness 3]
+
+ ]
+ edge [
+ source 2
+ target 3
+ label "Edge from node 2 to node 3"
+ ]
+ edge [
+ source 3
+ target 1
+ label "Edge from node 3 to node 1"
+ ]
+]
+"""
+
+ def test_parse_gml_cytoscape_bug(self):
+ # example from issue #321, originally #324 in trac
+ cytoscape_example = """
+Creator "Cytoscape"
+Version 1.0
+graph [
+ node [
+ root_index -3
+ id -3
+ graphics [
+ x -96.0
+ y -67.0
+ w 40.0
+ h 40.0
+ fill "#ff9999"
+ type "ellipse"
+ outline "#666666"
+ outline_width 1.5
+ ]
+ label "node2"
+ ]
+ node [
+ root_index -2
+ id -2
+ graphics [
+ x 63.0
+ y 37.0
+ w 40.0
+ h 40.0
+ fill "#ff9999"
+ type "ellipse"
+ outline "#666666"
+ outline_width 1.5
+ ]
+ label "node1"
+ ]
+ node [
+ root_index -1
+ id -1
+ graphics [
+ x -31.0
+ y -17.0
+ w 40.0
+ h 40.0
+ fill "#ff9999"
+ type "ellipse"
+ outline "#666666"
+ outline_width 1.5
+ ]
+ label "node0"
+ ]
+ edge [
+ root_index -2
+ target -2
+ source -1
+ graphics [
+ width 1.5
+ fill "#0000ff"
+ type "line"
+ Line [
+ ]
+ source_arrow 0
+ target_arrow 3
+ ]
+ label "DirectedEdge"
+ ]
+ edge [
+ root_index -1
+ target -1
+ source -3
+ graphics [
+ width 1.5
+ fill "#0000ff"
+ type "line"
+ Line [
+ ]
+ source_arrow 0
+ target_arrow 3
+ ]
+ label "DirectedEdge"
+ ]
+]
+"""
+ nx.parse_gml(cytoscape_example)
+
+ def test_parse_gml(self):
+ G = nx.parse_gml(self.simple_data, label="label")
+ assert sorted(G.nodes()) == ["Node 1", "Node 2", "Node 3"]
+ assert sorted(G.edges()) == [
+ ("Node 1", "Node 2"),
+ ("Node 2", "Node 3"),
+ ("Node 3", "Node 1"),
+ ]
+
+ assert sorted(G.edges(data=True)) == [
+ (
+ "Node 1",
+ "Node 2",
+ {
+ "color": {"line": "blue", "thickness": 3},
+ "label": "Edge from node 1 to node 2",
+ },
+ ),
+ ("Node 2", "Node 3", {"label": "Edge from node 2 to node 3"}),
+ ("Node 3", "Node 1", {"label": "Edge from node 3 to node 1"}),
+ ]
+
+ def test_read_gml(self, tmp_path):
+ fname = tmp_path / "test.gml"
+ with open(fname, "w") as fh:
+ fh.write(self.simple_data)
+ Gin = nx.read_gml(fname, label="label")
+ G = nx.parse_gml(self.simple_data, label="label")
+ assert sorted(G.nodes(data=True)) == sorted(Gin.nodes(data=True))
+ assert sorted(G.edges(data=True)) == sorted(Gin.edges(data=True))
+
+ def test_labels_are_strings(self):
+ # GML requires labels to be strings (i.e., in quotes)
+ answer = """graph [
+ node [
+ id 0
+ label "1203"
+ ]
+]"""
+ G = nx.Graph()
+ G.add_node(1203)
+ data = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer))
+ assert data == answer
+
+ def test_relabel_duplicate(self):
+ data = """
+graph
+[
+ label ""
+ directed 1
+ node
+ [
+ id 0
+ label "same"
+ ]
+ node
+ [
+ id 1
+ label "same"
+ ]
+]
+"""
+ fh = io.BytesIO(data.encode("UTF-8"))
+ fh.seek(0)
+ pytest.raises(nx.NetworkXError, nx.read_gml, fh, label="label")
+
+ @pytest.mark.parametrize("stringizer", (None, literal_stringizer))
+ def test_tuplelabels(self, stringizer):
+ # https://github.com/networkx/networkx/pull/1048
+ # Writing tuple labels to GML failed.
+ G = nx.Graph()
+ G.add_edge((0, 1), (1, 0))
+ data = "\n".join(nx.generate_gml(G, stringizer=stringizer))
+ answer = """graph [
+ node [
+ id 0
+ label "(0,1)"
+ ]
+ node [
+ id 1
+ label "(1,0)"
+ ]
+ edge [
+ source 0
+ target 1
+ ]
+]"""
+ assert data == answer
+
+ def test_quotes(self, tmp_path):
+ # https://github.com/networkx/networkx/issues/1061
+ # Encoding quotes as HTML entities.
+ G = nx.path_graph(1)
+ G.name = "path_graph(1)"
+ attr = 'This is "quoted" and this is a copyright: ' + chr(169)
+ G.nodes[0]["demo"] = attr
+ with open(tmp_path / "test.gml", "w+b") as fobj:
+ nx.write_gml(G, fobj)
+ fobj.seek(0)
+ # Should be bytes in 2.x and 3.x
+ data = fobj.read().strip().decode("ascii")
+ answer = """graph [
+ name "path_graph(1)"
+ node [
+ id 0
+ label "0"
+ demo "This is &#34;quoted&#34; and this is a copyright: &#169;"
+ ]
+]"""
+ assert data == answer
+
+ def test_unicode_node(self, tmp_path):
+ node = "node" + chr(169)
+ G = nx.Graph()
+ G.add_node(node)
+ with open(tmp_path / "test.gml", "w+b") as fobj:
+ nx.write_gml(G, fobj)
+ fobj.seek(0)
+ # Should be bytes in 2.x and 3.x
+ data = fobj.read().strip().decode("ascii")
+ answer = """graph [
+ node [
+ id 0
+ label "node&#169;"
+ ]
+]"""
+ assert data == answer
+
+ def test_float_label(self, tmp_path):
+ node = 1.0
+ G = nx.Graph()
+ G.add_node(node)
+ with open(tmp_path / "test.gml", "w+b") as fobj:
+ nx.write_gml(G, fobj)
+ fobj.seek(0)
+ # Should be bytes in 2.x and 3.x
+ data = fobj.read().strip().decode("ascii")
+ answer = """graph [
+ node [
+ id 0
+ label "1.0"
+ ]
+]"""
+ assert data == answer
+
+ def test_special_float_label(self, tmp_path):
+ special_floats = [float("nan"), float("+inf"), float("-inf")]
+ try:
+ import numpy as np
+
+ special_floats += [np.nan, np.inf, np.inf * -1]
+ except ImportError:
+ special_floats += special_floats
+
+ G = nx.cycle_graph(len(special_floats))
+ attrs = dict(enumerate(special_floats))
+ nx.set_node_attributes(G, attrs, "nodefloat")
+ edges = list(G.edges)
+ attrs = {edges[i]: value for i, value in enumerate(special_floats)}
+ nx.set_edge_attributes(G, attrs, "edgefloat")
+
+ with open(tmp_path / "test.gml", "w+b") as fobj:
+ nx.write_gml(G, fobj)
+ fobj.seek(0)
+ # Should be bytes in 2.x and 3.x
+ data = fobj.read().strip().decode("ascii")
+ answer = """graph [
+ node [
+ id 0
+ label "0"
+ nodefloat NAN
+ ]
+ node [
+ id 1
+ label "1"
+ nodefloat +INF
+ ]
+ node [
+ id 2
+ label "2"
+ nodefloat -INF
+ ]
+ node [
+ id 3
+ label "3"
+ nodefloat NAN
+ ]
+ node [
+ id 4
+ label "4"
+ nodefloat +INF
+ ]
+ node [
+ id 5
+ label "5"
+ nodefloat -INF
+ ]
+ edge [
+ source 0
+ target 1
+ edgefloat NAN
+ ]
+ edge [
+ source 0
+ target 5
+ edgefloat +INF
+ ]
+ edge [
+ source 1
+ target 2
+ edgefloat -INF
+ ]
+ edge [
+ source 2
+ target 3
+ edgefloat NAN
+ ]
+ edge [
+ source 3
+ target 4
+ edgefloat +INF
+ ]
+ edge [
+ source 4
+ target 5
+ edgefloat -INF
+ ]
+]"""
+ assert data == answer
+
+ fobj.seek(0)
+ graph = nx.read_gml(fobj)
+ for indx, value in enumerate(special_floats):
+ node_value = graph.nodes[str(indx)]["nodefloat"]
+ if math.isnan(value):
+ assert math.isnan(node_value)
+ else:
+ assert node_value == value
+
+ edge = edges[indx]
+ string_edge = (str(edge[0]), str(edge[1]))
+ edge_value = graph.edges[string_edge]["edgefloat"]
+ if math.isnan(value):
+ assert math.isnan(edge_value)
+ else:
+ assert edge_value == value
+
+ def test_name(self):
+ G = nx.parse_gml('graph [ name "x" node [ id 0 label "x" ] ]')
+ assert "x" == G.graph["name"]
+ G = nx.parse_gml('graph [ node [ id 0 label "x" ] ]')
+ assert "" == G.name
+ assert "name" not in G.graph
+
+ def test_graph_types(self):
+ for directed in [None, False, True]:
+ for multigraph in [None, False, True]:
+ gml = "graph ["
+ if directed is not None:
+ gml += " directed " + str(int(directed))
+ if multigraph is not None:
+ gml += " multigraph " + str(int(multigraph))
+ gml += ' node [ id 0 label "0" ]'
+ gml += " edge [ source 0 target 0 ]"
+ gml += " ]"
+ G = nx.parse_gml(gml)
+ assert bool(directed) == G.is_directed()
+ assert bool(multigraph) == G.is_multigraph()
+ gml = "graph [\n"
+ if directed is True:
+ gml += " directed 1\n"
+ if multigraph is True:
+ gml += " multigraph 1\n"
+ gml += """ node [
+ id 0
+ label "0"
+ ]
+ edge [
+ source 0
+ target 0
+"""
+ if multigraph:
+ gml += " key 0\n"
+ gml += " ]\n]"
+ assert gml == "\n".join(nx.generate_gml(G))
+
+ def test_data_types(self):
+ data = [
+ True,
+ False,
+ 10**20,
+ -2e33,
+ "'",
+ '"&&amp;&&#34;"',
+ [{(b"\xfd",): "\x7f", chr(0x4444): (1, 2)}, (2, "3")],
+ ]
+ data.append(chr(0x14444))
+ data.append(literal_eval("{2.3j, 1 - 2.3j, ()}"))
+ G = nx.Graph()
+ G.name = data
+ G.graph["data"] = data
+ G.add_node(0, int=-1, data={"data": data})
+ G.add_edge(0, 0, float=-2.5, data=data)
+ gml = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer))
+ G = nx.parse_gml(gml, destringizer=literal_destringizer)
+ assert data == G.name
+ assert {"name": data, "data": data} == G.graph
+ assert list(G.nodes(data=True)) == [(0, {"int": -1, "data": {"data": data}})]
+ assert list(G.edges(data=True)) == [(0, 0, {"float": -2.5, "data": data})]
+ G = nx.Graph()
+ G.graph["data"] = "frozenset([1, 2, 3])"
+ G = nx.parse_gml(nx.generate_gml(G), destringizer=literal_eval)
+ assert G.graph["data"] == "frozenset([1, 2, 3])"
+
+ def test_escape_unescape(self):
+ gml = """graph [
+ name "&amp;&#34;&#xf;&#x4444;&#1234567890;&#x1234567890abcdef;&unknown;"
+]"""
+ G = nx.parse_gml(gml)
+ assert (
+ '&"\x0f' + chr(0x4444) + "&#1234567890;&#x1234567890abcdef;&unknown;"
+ == G.name
+ )
+ gml = "\n".join(nx.generate_gml(G))
+ alnu = "#1234567890;&#38;#x1234567890abcdef"
+ answer = (
+ """graph [
+ name "&#38;&#34;&#15;&#17476;&#38;"""
+ + alnu
+ + """;&#38;unknown;"
+]"""
+ )
+ assert answer == gml
+
+ def test_exceptions(self, tmp_path):
+ pytest.raises(ValueError, literal_destringizer, "(")
+ pytest.raises(ValueError, literal_destringizer, "frozenset([1, 2, 3])")
+ pytest.raises(ValueError, literal_destringizer, literal_destringizer)
+ pytest.raises(ValueError, literal_stringizer, frozenset([1, 2, 3]))
+ pytest.raises(ValueError, literal_stringizer, literal_stringizer)
+ with open(tmp_path / "test.gml", "w+b") as f:
+ f.write(codecs.BOM_UTF8 + b"graph[]")
+ f.seek(0)
+ pytest.raises(nx.NetworkXError, nx.read_gml, f)
+
+ def assert_parse_error(gml):
+ pytest.raises(nx.NetworkXError, nx.parse_gml, gml)
+
+ assert_parse_error(["graph [\n\n", "]"])
+ assert_parse_error("")
+ assert_parse_error('Creator ""')
+ assert_parse_error("0")
+ assert_parse_error("graph ]")
+ assert_parse_error("graph [ 1 ]")
+ assert_parse_error("graph [ 1.E+2 ]")
+ assert_parse_error('graph [ "A" ]')
+ assert_parse_error("graph [ ] graph ]")
+ assert_parse_error("graph [ ] graph [ ]")
+ assert_parse_error("graph [ data [1, 2, 3] ]")
+ assert_parse_error("graph [ node [ ] ]")
+ assert_parse_error("graph [ node [ id 0 ] ]")
+ nx.parse_gml('graph [ node [ id "a" ] ]', label="id")
+ assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 0 label 1 ] ]")
+ assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 1 label 0 ] ]")
+ assert_parse_error("graph [ node [ id 0 label 0 ] edge [ ] ]")
+ assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 ] ]")
+ nx.parse_gml("graph [edge [ source 0 target 0 ] node [ id 0 label 0 ] ]")
+ assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 1 target 0 ] ]")
+ assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 target 1 ] ]")
+ assert_parse_error(
+ "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
+ "edge [ source 0 target 1 ] edge [ source 1 target 0 ] ]"
+ )
+ nx.parse_gml(
+ "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
+ "edge [ source 0 target 1 ] edge [ source 1 target 0 ] "
+ "directed 1 ]"
+ )
+ nx.parse_gml(
+ "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
+ "edge [ source 0 target 1 ] edge [ source 0 target 1 ]"
+ "multigraph 1 ]"
+ )
+ nx.parse_gml(
+ "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
+ "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 ]"
+ "multigraph 1 ]"
+ )
+ assert_parse_error(
+ "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
+ "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 key 0 ]"
+ "multigraph 1 ]"
+ )
+ nx.parse_gml(
+ "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
+ "edge [ source 0 target 1 key 0 ] edge [ source 1 target 0 key 0 ]"
+ "directed 1 multigraph 1 ]"
+ )
+
+ # Tests for string convertible alphanumeric id and label values
+ nx.parse_gml("graph [edge [ source a target a ] node [ id a label b ] ]")
+ nx.parse_gml(
+ "graph [ node [ id n42 label 0 ] node [ id x43 label 1 ]"
+ "edge [ source n42 target x43 key 0 ]"
+ "edge [ source x43 target n42 key 0 ]"
+ "directed 1 multigraph 1 ]"
+ )
+ assert_parse_error(
+ "graph [edge [ source '\u4200' target '\u4200' ] "
+ + "node [ id '\u4200' label b ] ]"
+ )
+
+ def assert_generate_error(*args, **kwargs):
+ pytest.raises(
+ nx.NetworkXError, lambda: list(nx.generate_gml(*args, **kwargs))
+ )
+
+ G = nx.Graph()
+ G.graph[3] = 3
+ assert_generate_error(G)
+ G = nx.Graph()
+ G.graph["3"] = 3
+ assert_generate_error(G)
+ G = nx.Graph()
+ G.graph["data"] = frozenset([1, 2, 3])
+ assert_generate_error(G, stringizer=literal_stringizer)
+
+ def test_label_kwarg(self):
+ G = nx.parse_gml(self.simple_data, label="id")
+ assert sorted(G.nodes) == [1, 2, 3]
+ labels = [G.nodes[n]["label"] for n in sorted(G.nodes)]
+ assert labels == ["Node 1", "Node 2", "Node 3"]
+
+ G = nx.parse_gml(self.simple_data, label=None)
+ assert sorted(G.nodes) == [1, 2, 3]
+ labels = [G.nodes[n]["label"] for n in sorted(G.nodes)]
+ assert labels == ["Node 1", "Node 2", "Node 3"]
+
+ def test_outofrange_integers(self, tmp_path):
+ # GML restricts integers to 32 signed bits.
+ # Check that we honor this restriction on export
+ G = nx.Graph()
+ # Test export for numbers that barely fit or don't fit into 32 bits,
+ # and 3 numbers in the middle
+ numbers = {
+ "toosmall": (-(2**31)) - 1,
+ "small": -(2**31),
+ "med1": -4,
+ "med2": 0,
+ "med3": 17,
+ "big": (2**31) - 1,
+ "toobig": 2**31,
+ }
+ G.add_node("Node", **numbers)
+
+ fname = tmp_path / "test.gml"
+ nx.write_gml(G, fname)
+ # Check that the export wrote the nonfitting numbers as strings
+ G2 = nx.read_gml(fname)
+ for attr, value in G2.nodes["Node"].items():
+ if attr == "toosmall" or attr == "toobig":
+ assert type(value) == str
+ else:
+ assert type(value) == int
+
+ def test_multiline(self):
+ # example from issue #6836
+ multiline_example = """
+graph
+[
+ node
+ [
+ id 0
+ label "multiline node"
+ label2 "multiline1
+ multiline2
+ multiline3"
+ alt_name "id 0"
+ ]
+]
+"""
+ G = nx.parse_gml(multiline_example)
+ assert G.nodes["multiline node"] == {
+ "label2": "multiline1 multiline2 multiline3",
+ "alt_name": "id 0",
+ }
+
+
+@contextmanager
+def byte_file():
+ _file_handle = io.BytesIO()
+ yield _file_handle
+ _file_handle.seek(0)
+
+
+class TestPropertyLists:
+ def test_writing_graph_with_multi_element_property_list(self):
+ g = nx.Graph()
+ g.add_node("n1", properties=["element", 0, 1, 2.5, True, False])
+ with byte_file() as f:
+ nx.write_gml(g, f)
+ result = f.read().decode()
+
+ assert result == dedent(
+ """\
+ graph [
+ node [
+ id 0
+ label "n1"
+ properties "element"
+ properties 0
+ properties 1
+ properties 2.5
+ properties 1
+ properties 0
+ ]
+ ]
+ """
+ )
+
+ def test_writing_graph_with_one_element_property_list(self):
+ g = nx.Graph()
+ g.add_node("n1", properties=["element"])
+ with byte_file() as f:
+ nx.write_gml(g, f)
+ result = f.read().decode()
+
+ assert result == dedent(
+ """\
+ graph [
+ node [
+ id 0
+ label "n1"
+ properties "_networkx_list_start"
+ properties "element"
+ ]
+ ]
+ """
+ )
+
+ def test_reading_graph_with_list_property(self):
+ with byte_file() as f:
+ f.write(
+ dedent(
+ """
+ graph [
+ node [
+ id 0
+ label "n1"
+ properties "element"
+ properties 0
+ properties 1
+ properties 2.5
+ ]
+ ]
+ """
+ ).encode("ascii")
+ )
+ f.seek(0)
+ graph = nx.read_gml(f)
+ assert graph.nodes(data=True)["n1"] == {"properties": ["element", 0, 1, 2.5]}
+
+ def test_reading_graph_with_single_element_list_property(self):
+ with byte_file() as f:
+ f.write(
+ dedent(
+ """
+ graph [
+ node [
+ id 0
+ label "n1"
+ properties "_networkx_list_start"
+ properties "element"
+ ]
+ ]
+ """
+ ).encode("ascii")
+ )
+ f.seek(0)
+ graph = nx.read_gml(f)
+ assert graph.nodes(data=True)["n1"] == {"properties": ["element"]}
+
+
+@pytest.mark.parametrize("coll", ([], ()))
+def test_stringize_empty_list_tuple(coll):
+ G = nx.path_graph(2)
+ G.nodes[0]["test"] = coll # test serializing an empty collection
+ f = io.BytesIO()
+ nx.write_gml(G, f) # Smoke test - should not raise
+ f.seek(0)
+ H = nx.read_gml(f)
+ assert H.nodes["0"]["test"] == coll # Check empty list round-trips properly
+ # Check full round-tripping. Note that nodes are loaded as strings by
+ # default, so there needs to be some remapping prior to comparison
+ H = nx.relabel_nodes(H, {"0": 0, "1": 1})
+ assert nx.utils.graphs_equal(G, H)
+ # Same as above, but use destringizer for node remapping. Should have no
+ # effect on node attr
+ f.seek(0)
+ H = nx.read_gml(f, destringizer=int)
+ assert nx.utils.graphs_equal(G, H)
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py
new file mode 100644
index 00000000..a8032694
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py
@@ -0,0 +1,168 @@
+from io import BytesIO
+
+import pytest
+
+import networkx as nx
+import networkx.readwrite.graph6 as g6
+from networkx.utils import edges_equal, nodes_equal
+
+
+class TestGraph6Utils:
+ def test_n_data_n_conversion(self):
+ for i in [0, 1, 42, 62, 63, 64, 258047, 258048, 7744773, 68719476735]:
+ assert g6.data_to_n(g6.n_to_data(i))[0] == i
+ assert g6.data_to_n(g6.n_to_data(i))[1] == []
+ assert g6.data_to_n(g6.n_to_data(i) + [42, 43])[1] == [42, 43]
+
+
+class TestFromGraph6Bytes:
+ def test_from_graph6_bytes(self):
+ data = b"DF{"
+ G = nx.from_graph6_bytes(data)
+ assert nodes_equal(G.nodes(), [0, 1, 2, 3, 4])
+ assert edges_equal(
+ G.edges(), [(0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
+ )
+
+ def test_read_equals_from_bytes(self):
+ data = b"DF{"
+ G = nx.from_graph6_bytes(data)
+ fh = BytesIO(data)
+ Gin = nx.read_graph6(fh)
+ assert nodes_equal(G.nodes(), Gin.nodes())
+ assert edges_equal(G.edges(), Gin.edges())
+
+
+class TestReadGraph6:
+ def test_read_many_graph6(self):
+ """Test for reading many graphs from a file into a list."""
+ data = b"DF{\nD`{\nDqK\nD~{\n"
+ fh = BytesIO(data)
+ glist = nx.read_graph6(fh)
+ assert len(glist) == 4
+ for G in glist:
+ assert sorted(G) == list(range(5))
+
+
+class TestWriteGraph6:
+ """Unit tests for writing a graph to a file in graph6 format."""
+
+ def test_null_graph(self):
+ result = BytesIO()
+ nx.write_graph6(nx.null_graph(), result)
+ assert result.getvalue() == b">>graph6<<?\n"
+
+ def test_trivial_graph(self):
+ result = BytesIO()
+ nx.write_graph6(nx.trivial_graph(), result)
+ assert result.getvalue() == b">>graph6<<@\n"
+
+ def test_complete_graph(self):
+ result = BytesIO()
+ nx.write_graph6(nx.complete_graph(4), result)
+ assert result.getvalue() == b">>graph6<<C~\n"
+
+ def test_large_complete_graph(self):
+ result = BytesIO()
+ nx.write_graph6(nx.complete_graph(67), result, header=False)
+ assert result.getvalue() == b"~?@B" + b"~" * 368 + b"w\n"
+
+ def test_no_header(self):
+ result = BytesIO()
+ nx.write_graph6(nx.complete_graph(4), result, header=False)
+ assert result.getvalue() == b"C~\n"
+
+ def test_complete_bipartite_graph(self):
+ result = BytesIO()
+ G = nx.complete_bipartite_graph(6, 9)
+ nx.write_graph6(G, result, header=False)
+ # The expected encoding here was verified by Sage.
+ assert result.getvalue() == b"N??F~z{~Fw^_~?~?^_?\n"
+
+ @pytest.mark.parametrize("G", (nx.MultiGraph(), nx.DiGraph()))
+ def test_no_directed_or_multi_graphs(self, G):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.write_graph6(G, BytesIO())
+
+ def test_length(self):
+ for i in list(range(13)) + [31, 47, 62, 63, 64, 72]:
+ g = nx.random_graphs.gnm_random_graph(i, i * i // 4, seed=i)
+ gstr = BytesIO()
+ nx.write_graph6(g, gstr, header=False)
+ # Strip the trailing newline.
+ gstr = gstr.getvalue().rstrip()
+ assert len(gstr) == ((i - 1) * i // 2 + 5) // 6 + (1 if i < 63 else 4)
+
+ def test_roundtrip(self):
+ for i in list(range(13)) + [31, 47, 62, 63, 64, 72]:
+ G = nx.random_graphs.gnm_random_graph(i, i * i // 4, seed=i)
+ f = BytesIO()
+ nx.write_graph6(G, f)
+ f.seek(0)
+ H = nx.read_graph6(f)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+
+ def test_write_path(self, tmp_path):
+ with open(tmp_path / "test.g6", "w+b") as f:
+ g6.write_graph6_file(nx.null_graph(), f)
+ f.seek(0)
+ assert f.read() == b">>graph6<<?\n"
+
+ @pytest.mark.parametrize("edge", ((0, 1), (1, 2), (1, 42)))
+ def test_relabeling(self, edge):
+ G = nx.Graph([edge])
+ f = BytesIO()
+ nx.write_graph6(G, f)
+ f.seek(0)
+ assert f.read() == b">>graph6<<A_\n"
+
+
+class TestToGraph6Bytes:
+ def test_null_graph(self):
+ G = nx.null_graph()
+ assert g6.to_graph6_bytes(G) == b">>graph6<<?\n"
+
+ def test_trivial_graph(self):
+ G = nx.trivial_graph()
+ assert g6.to_graph6_bytes(G) == b">>graph6<<@\n"
+
+ def test_complete_graph(self):
+ assert g6.to_graph6_bytes(nx.complete_graph(4)) == b">>graph6<<C~\n"
+
+ def test_large_complete_graph(self):
+ G = nx.complete_graph(67)
+ assert g6.to_graph6_bytes(G, header=False) == b"~?@B" + b"~" * 368 + b"w\n"
+
+ def test_no_header(self):
+ G = nx.complete_graph(4)
+ assert g6.to_graph6_bytes(G, header=False) == b"C~\n"
+
+ def test_complete_bipartite_graph(self):
+ G = nx.complete_bipartite_graph(6, 9)
+ assert g6.to_graph6_bytes(G, header=False) == b"N??F~z{~Fw^_~?~?^_?\n"
+
+ @pytest.mark.parametrize("G", (nx.MultiGraph(), nx.DiGraph()))
+ def test_no_directed_or_multi_graphs(self, G):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ g6.to_graph6_bytes(G)
+
+ def test_length(self):
+ for i in list(range(13)) + [31, 47, 62, 63, 64, 72]:
+ G = nx.random_graphs.gnm_random_graph(i, i * i // 4, seed=i)
+ # Strip the trailing newline.
+ gstr = g6.to_graph6_bytes(G, header=False).rstrip()
+ assert len(gstr) == ((i - 1) * i // 2 + 5) // 6 + (1 if i < 63 else 4)
+
+ def test_roundtrip(self):
+ for i in list(range(13)) + [31, 47, 62, 63, 64, 72]:
+ G = nx.random_graphs.gnm_random_graph(i, i * i // 4, seed=i)
+ data = g6.to_graph6_bytes(G)
+ H = nx.from_graph6_bytes(data.rstrip())
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+
+ @pytest.mark.parametrize("edge", ((0, 1), (1, 2), (1, 42)))
+ def test_relabeling(self, edge):
+ G = nx.Graph([edge])
+ assert g6.to_graph6_bytes(G) == b">>graph6<<A_\n"
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graphml.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graphml.py
new file mode 100644
index 00000000..5ffa837e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_graphml.py
@@ -0,0 +1,1531 @@
+import io
+
+import pytest
+
+import networkx as nx
+from networkx.readwrite.graphml import GraphMLWriter
+from networkx.utils import edges_equal, nodes_equal
+
+
+class BaseGraphML:
+ @classmethod
+ def setup_class(cls):
+ cls.simple_directed_data = """<?xml version="1.0" encoding="UTF-8"?>
+<!-- This file was written by the JAVA GraphML Library.-->
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G" edgedefault="directed">
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <node id="n3"/>
+ <node id="n4"/>
+ <node id="n5"/>
+ <node id="n6"/>
+ <node id="n7"/>
+ <node id="n8"/>
+ <node id="n9"/>
+ <node id="n10"/>
+ <edge id="foo" source="n0" target="n2"/>
+ <edge source="n1" target="n2"/>
+ <edge source="n2" target="n3"/>
+ <edge source="n3" target="n5"/>
+ <edge source="n3" target="n4"/>
+ <edge source="n4" target="n6"/>
+ <edge source="n6" target="n5"/>
+ <edge source="n5" target="n7"/>
+ <edge source="n6" target="n8"/>
+ <edge source="n8" target="n7"/>
+ <edge source="n8" target="n9"/>
+ </graph>
+</graphml>"""
+ cls.simple_directed_graph = nx.DiGraph()
+ cls.simple_directed_graph.add_node("n10")
+ cls.simple_directed_graph.add_edge("n0", "n2", id="foo")
+ cls.simple_directed_graph.add_edge("n0", "n2")
+ cls.simple_directed_graph.add_edges_from(
+ [
+ ("n1", "n2"),
+ ("n2", "n3"),
+ ("n3", "n5"),
+ ("n3", "n4"),
+ ("n4", "n6"),
+ ("n6", "n5"),
+ ("n5", "n7"),
+ ("n6", "n8"),
+ ("n8", "n7"),
+ ("n8", "n9"),
+ ]
+ )
+ cls.simple_directed_fh = io.BytesIO(cls.simple_directed_data.encode("UTF-8"))
+
+ cls.attribute_data = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="d0" for="node" attr.name="color" attr.type="string">
+ <default>yellow</default>
+ </key>
+ <key id="d1" for="edge" attr.name="weight" attr.type="double"/>
+ <graph id="G" edgedefault="directed">
+ <node id="n0">
+ <data key="d0">green</data>
+ </node>
+ <node id="n1"/>
+ <node id="n2">
+ <data key="d0">blue</data>
+ </node>
+ <node id="n3">
+ <data key="d0">red</data>
+ </node>
+ <node id="n4"/>
+ <node id="n5">
+ <data key="d0">turquoise</data>
+ </node>
+ <edge id="e0" source="n0" target="n2">
+ <data key="d1">1.0</data>
+ </edge>
+ <edge id="e1" source="n0" target="n1">
+ <data key="d1">1.0</data>
+ </edge>
+ <edge id="e2" source="n1" target="n3">
+ <data key="d1">2.0</data>
+ </edge>
+ <edge id="e3" source="n3" target="n2"/>
+ <edge id="e4" source="n2" target="n4"/>
+ <edge id="e5" source="n3" target="n5"/>
+ <edge id="e6" source="n5" target="n4">
+ <data key="d1">1.1</data>
+ </edge>
+ </graph>
+</graphml>
+"""
+ cls.attribute_graph = nx.DiGraph(id="G")
+ cls.attribute_graph.graph["node_default"] = {"color": "yellow"}
+ cls.attribute_graph.add_node("n0", color="green")
+ cls.attribute_graph.add_node("n2", color="blue")
+ cls.attribute_graph.add_node("n3", color="red")
+ cls.attribute_graph.add_node("n4")
+ cls.attribute_graph.add_node("n5", color="turquoise")
+ cls.attribute_graph.add_edge("n0", "n2", id="e0", weight=1.0)
+ cls.attribute_graph.add_edge("n0", "n1", id="e1", weight=1.0)
+ cls.attribute_graph.add_edge("n1", "n3", id="e2", weight=2.0)
+ cls.attribute_graph.add_edge("n3", "n2", id="e3")
+ cls.attribute_graph.add_edge("n2", "n4", id="e4")
+ cls.attribute_graph.add_edge("n3", "n5", id="e5")
+ cls.attribute_graph.add_edge("n5", "n4", id="e6", weight=1.1)
+ cls.attribute_fh = io.BytesIO(cls.attribute_data.encode("UTF-8"))
+
+ cls.node_attribute_default_data = """<?xml version="1.0" encoding="UTF-8"?>
+ <graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="d0" for="node" attr.name="boolean_attribute" attr.type="boolean"><default>false</default></key>
+ <key id="d1" for="node" attr.name="int_attribute" attr.type="int"><default>0</default></key>
+ <key id="d2" for="node" attr.name="long_attribute" attr.type="long"><default>0</default></key>
+ <key id="d3" for="node" attr.name="float_attribute" attr.type="float"><default>0.0</default></key>
+ <key id="d4" for="node" attr.name="double_attribute" attr.type="double"><default>0.0</default></key>
+ <key id="d5" for="node" attr.name="string_attribute" attr.type="string"><default>Foo</default></key>
+ <graph id="G" edgedefault="directed">
+ <node id="n0"/>
+ <node id="n1"/>
+ <edge id="e0" source="n0" target="n1"/>
+ </graph>
+ </graphml>
+ """
+ cls.node_attribute_default_graph = nx.DiGraph(id="G")
+ cls.node_attribute_default_graph.graph["node_default"] = {
+ "boolean_attribute": False,
+ "int_attribute": 0,
+ "long_attribute": 0,
+ "float_attribute": 0.0,
+ "double_attribute": 0.0,
+ "string_attribute": "Foo",
+ }
+ cls.node_attribute_default_graph.add_node("n0")
+ cls.node_attribute_default_graph.add_node("n1")
+ cls.node_attribute_default_graph.add_edge("n0", "n1", id="e0")
+ cls.node_attribute_default_fh = io.BytesIO(
+ cls.node_attribute_default_data.encode("UTF-8")
+ )
+
+ cls.attribute_named_key_ids_data = """<?xml version='1.0' encoding='utf-8'?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="edge_prop" for="edge" attr.name="edge_prop" attr.type="string"/>
+ <key id="prop2" for="node" attr.name="prop2" attr.type="string"/>
+ <key id="prop1" for="node" attr.name="prop1" attr.type="string"/>
+ <graph edgedefault="directed">
+ <node id="0">
+ <data key="prop1">val1</data>
+ <data key="prop2">val2</data>
+ </node>
+ <node id="1">
+ <data key="prop1">val_one</data>
+ <data key="prop2">val2</data>
+ </node>
+ <edge source="0" target="1">
+ <data key="edge_prop">edge_value</data>
+ </edge>
+ </graph>
+</graphml>
+"""
+ cls.attribute_named_key_ids_graph = nx.DiGraph()
+ cls.attribute_named_key_ids_graph.add_node("0", prop1="val1", prop2="val2")
+ cls.attribute_named_key_ids_graph.add_node("1", prop1="val_one", prop2="val2")
+ cls.attribute_named_key_ids_graph.add_edge("0", "1", edge_prop="edge_value")
+ fh = io.BytesIO(cls.attribute_named_key_ids_data.encode("UTF-8"))
+ cls.attribute_named_key_ids_fh = fh
+
+ cls.attribute_numeric_type_data = """<?xml version='1.0' encoding='utf-8'?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key attr.name="weight" attr.type="double" for="node" id="d1" />
+ <key attr.name="weight" attr.type="double" for="edge" id="d0" />
+ <graph edgedefault="directed">
+ <node id="n0">
+ <data key="d1">1</data>
+ </node>
+ <node id="n1">
+ <data key="d1">2.0</data>
+ </node>
+ <edge source="n0" target="n1">
+ <data key="d0">1</data>
+ </edge>
+ <edge source="n1" target="n0">
+ <data key="d0">k</data>
+ </edge>
+ <edge source="n1" target="n1">
+ <data key="d0">1.0</data>
+ </edge>
+ </graph>
+</graphml>
+"""
+ cls.attribute_numeric_type_graph = nx.DiGraph()
+ cls.attribute_numeric_type_graph.add_node("n0", weight=1)
+ cls.attribute_numeric_type_graph.add_node("n1", weight=2.0)
+ cls.attribute_numeric_type_graph.add_edge("n0", "n1", weight=1)
+ cls.attribute_numeric_type_graph.add_edge("n1", "n1", weight=1.0)
+ fh = io.BytesIO(cls.attribute_numeric_type_data.encode("UTF-8"))
+ cls.attribute_numeric_type_fh = fh
+
+ cls.simple_undirected_data = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G">
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <node id="n10"/>
+ <edge id="foo" source="n0" target="n2"/>
+ <edge source="n1" target="n2"/>
+ <edge source="n2" target="n3"/>
+ </graph>
+</graphml>"""
+ # <edge source="n8" target="n10" directed="false"/>
+ cls.simple_undirected_graph = nx.Graph()
+ cls.simple_undirected_graph.add_node("n10")
+ cls.simple_undirected_graph.add_edge("n0", "n2", id="foo")
+ cls.simple_undirected_graph.add_edges_from([("n1", "n2"), ("n2", "n3")])
+ fh = io.BytesIO(cls.simple_undirected_data.encode("UTF-8"))
+ cls.simple_undirected_fh = fh
+
+ cls.undirected_multigraph_data = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G">
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <node id="n10"/>
+ <edge id="e0" source="n0" target="n2"/>
+ <edge id="e1" source="n1" target="n2"/>
+ <edge id="e2" source="n2" target="n1"/>
+ </graph>
+</graphml>"""
+ cls.undirected_multigraph = nx.MultiGraph()
+ cls.undirected_multigraph.add_node("n10")
+ cls.undirected_multigraph.add_edge("n0", "n2", id="e0")
+ cls.undirected_multigraph.add_edge("n1", "n2", id="e1")
+ cls.undirected_multigraph.add_edge("n2", "n1", id="e2")
+ fh = io.BytesIO(cls.undirected_multigraph_data.encode("UTF-8"))
+ cls.undirected_multigraph_fh = fh
+
+ cls.undirected_multigraph_no_multiedge_data = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G">
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <node id="n10"/>
+ <edge id="e0" source="n0" target="n2"/>
+ <edge id="e1" source="n1" target="n2"/>
+ <edge id="e2" source="n2" target="n3"/>
+ </graph>
+</graphml>"""
+ cls.undirected_multigraph_no_multiedge = nx.MultiGraph()
+ cls.undirected_multigraph_no_multiedge.add_node("n10")
+ cls.undirected_multigraph_no_multiedge.add_edge("n0", "n2", id="e0")
+ cls.undirected_multigraph_no_multiedge.add_edge("n1", "n2", id="e1")
+ cls.undirected_multigraph_no_multiedge.add_edge("n2", "n3", id="e2")
+ fh = io.BytesIO(cls.undirected_multigraph_no_multiedge_data.encode("UTF-8"))
+ cls.undirected_multigraph_no_multiedge_fh = fh
+
+ cls.multigraph_only_ids_for_multiedges_data = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G">
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <node id="n10"/>
+ <edge source="n0" target="n2"/>
+ <edge id="e1" source="n1" target="n2"/>
+ <edge id="e2" source="n2" target="n1"/>
+ </graph>
+</graphml>"""
+ cls.multigraph_only_ids_for_multiedges = nx.MultiGraph()
+ cls.multigraph_only_ids_for_multiedges.add_node("n10")
+ cls.multigraph_only_ids_for_multiedges.add_edge("n0", "n2")
+ cls.multigraph_only_ids_for_multiedges.add_edge("n1", "n2", id="e1")
+ cls.multigraph_only_ids_for_multiedges.add_edge("n2", "n1", id="e2")
+ fh = io.BytesIO(cls.multigraph_only_ids_for_multiedges_data.encode("UTF-8"))
+ cls.multigraph_only_ids_for_multiedges_fh = fh
+
+
+class TestReadGraphML(BaseGraphML):
+ def test_read_simple_directed_graphml(self):
+ G = self.simple_directed_graph
+ H = nx.read_graphml(self.simple_directed_fh)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(G.edges()) == sorted(H.edges())
+ assert sorted(G.edges(data=True)) == sorted(H.edges(data=True))
+ self.simple_directed_fh.seek(0)
+
+ PG = nx.parse_graphml(self.simple_directed_data)
+ assert sorted(G.nodes()) == sorted(PG.nodes())
+ assert sorted(G.edges()) == sorted(PG.edges())
+ assert sorted(G.edges(data=True)) == sorted(PG.edges(data=True))
+
+ def test_read_simple_undirected_graphml(self):
+ G = self.simple_undirected_graph
+ H = nx.read_graphml(self.simple_undirected_fh)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ self.simple_undirected_fh.seek(0)
+
+ PG = nx.parse_graphml(self.simple_undirected_data)
+ assert nodes_equal(G.nodes(), PG.nodes())
+ assert edges_equal(G.edges(), PG.edges())
+
+ def test_read_undirected_multigraph_graphml(self):
+ G = self.undirected_multigraph
+ H = nx.read_graphml(self.undirected_multigraph_fh)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ self.undirected_multigraph_fh.seek(0)
+
+ PG = nx.parse_graphml(self.undirected_multigraph_data)
+ assert nodes_equal(G.nodes(), PG.nodes())
+ assert edges_equal(G.edges(), PG.edges())
+
+ def test_read_undirected_multigraph_no_multiedge_graphml(self):
+ G = self.undirected_multigraph_no_multiedge
+ H = nx.read_graphml(self.undirected_multigraph_no_multiedge_fh)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ self.undirected_multigraph_no_multiedge_fh.seek(0)
+
+ PG = nx.parse_graphml(self.undirected_multigraph_no_multiedge_data)
+ assert nodes_equal(G.nodes(), PG.nodes())
+ assert edges_equal(G.edges(), PG.edges())
+
+ def test_read_undirected_multigraph_only_ids_for_multiedges_graphml(self):
+ G = self.multigraph_only_ids_for_multiedges
+ H = nx.read_graphml(self.multigraph_only_ids_for_multiedges_fh)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ self.multigraph_only_ids_for_multiedges_fh.seek(0)
+
+ PG = nx.parse_graphml(self.multigraph_only_ids_for_multiedges_data)
+ assert nodes_equal(G.nodes(), PG.nodes())
+ assert edges_equal(G.edges(), PG.edges())
+
+ def test_read_attribute_graphml(self):
+ G = self.attribute_graph
+ H = nx.read_graphml(self.attribute_fh)
+ assert nodes_equal(G.nodes(True), sorted(H.nodes(data=True)))
+ ge = sorted(G.edges(data=True))
+ he = sorted(H.edges(data=True))
+ for a, b in zip(ge, he):
+ assert a == b
+ self.attribute_fh.seek(0)
+
+ PG = nx.parse_graphml(self.attribute_data)
+ assert sorted(G.nodes(True)) == sorted(PG.nodes(data=True))
+ ge = sorted(G.edges(data=True))
+ he = sorted(PG.edges(data=True))
+ for a, b in zip(ge, he):
+ assert a == b
+
+ def test_node_default_attribute_graphml(self):
+ G = self.node_attribute_default_graph
+ H = nx.read_graphml(self.node_attribute_default_fh)
+ assert G.graph["node_default"] == H.graph["node_default"]
+
+ def test_directed_edge_in_undirected(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G">
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <edge source="n0" target="n1"/>
+ <edge source="n1" target="n2" directed='true'/>
+ </graph>
+</graphml>"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_graphml, fh)
+ pytest.raises(nx.NetworkXError, nx.parse_graphml, s)
+
+ def test_undirected_edge_in_directed(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G" edgedefault='directed'>
+ <node id="n0"/>
+ <node id="n1"/>
+ <node id="n2"/>
+ <edge source="n0" target="n1"/>
+ <edge source="n1" target="n2" directed='false'/>
+ </graph>
+</graphml>"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_graphml, fh)
+ pytest.raises(nx.NetworkXError, nx.parse_graphml, s)
+
+ def test_key_raise(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="d0" for="node" attr.name="color" attr.type="string">
+ <default>yellow</default>
+ </key>
+ <key id="d1" for="edge" attr.name="weight" attr.type="double"/>
+ <graph id="G" edgedefault="directed">
+ <node id="n0">
+ <data key="d0">green</data>
+ </node>
+ <node id="n1"/>
+ <node id="n2">
+ <data key="d0">blue</data>
+ </node>
+ <edge id="e0" source="n0" target="n2">
+ <data key="d2">1.0</data>
+ </edge>
+ </graph>
+</graphml>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_graphml, fh)
+ pytest.raises(nx.NetworkXError, nx.parse_graphml, s)
+
+ def test_hyperedge_raise(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="d0" for="node" attr.name="color" attr.type="string">
+ <default>yellow</default>
+ </key>
+ <key id="d1" for="edge" attr.name="weight" attr.type="double"/>
+ <graph id="G" edgedefault="directed">
+ <node id="n0">
+ <data key="d0">green</data>
+ </node>
+ <node id="n1"/>
+ <node id="n2">
+ <data key="d0">blue</data>
+ </node>
+ <hyperedge id="e0" source="n0" target="n2">
+ <endpoint node="n0"/>
+ <endpoint node="n1"/>
+ <endpoint node="n2"/>
+ </hyperedge>
+ </graph>
+</graphml>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_graphml, fh)
+ pytest.raises(nx.NetworkXError, nx.parse_graphml, s)
+
+ def test_multigraph_keys(self):
+ # Test that reading multigraphs uses edge id attributes as keys
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <graph id="G" edgedefault="directed">
+ <node id="n0"/>
+ <node id="n1"/>
+ <edge id="e0" source="n0" target="n1"/>
+ <edge id="e1" source="n0" target="n1"/>
+ </graph>
+</graphml>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ G = nx.read_graphml(fh)
+ expected = [("n0", "n1", "e0"), ("n0", "n1", "e1")]
+ assert sorted(G.edges(keys=True)) == expected
+ fh.seek(0)
+ H = nx.parse_graphml(s)
+ assert sorted(H.edges(keys=True)) == expected
+
+ def test_preserve_multi_edge_data(self):
+ """
+ Test that data and keys of edges are preserved on consequent
+ write and reads
+ """
+ G = nx.MultiGraph()
+ G.add_node(1)
+ G.add_node(2)
+ G.add_edges_from(
+ [
+ # edges with no data, no keys:
+ (1, 2),
+ # edges with only data:
+ (1, 2, {"key": "data_key1"}),
+ (1, 2, {"id": "data_id2"}),
+ (1, 2, {"key": "data_key3", "id": "data_id3"}),
+ # edges with both data and keys:
+ (1, 2, 103, {"key": "data_key4"}),
+ (1, 2, 104, {"id": "data_id5"}),
+ (1, 2, 105, {"key": "data_key6", "id": "data_id7"}),
+ ]
+ )
+ fh = io.BytesIO()
+ nx.write_graphml(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh, node_type=int)
+ assert edges_equal(G.edges(data=True, keys=True), H.edges(data=True, keys=True))
+ assert G._adj == H._adj
+
+ Gadj = {
+ str(node): {
+ str(nbr): {str(ekey): dd for ekey, dd in key_dict.items()}
+ for nbr, key_dict in nbr_dict.items()
+ }
+ for node, nbr_dict in G._adj.items()
+ }
+ fh.seek(0)
+ HH = nx.read_graphml(fh, node_type=str, edge_key_type=str)
+ assert Gadj == HH._adj
+
+ fh.seek(0)
+ string_fh = fh.read()
+ HH = nx.parse_graphml(string_fh, node_type=str, edge_key_type=str)
+ assert Gadj == HH._adj
+
+ def test_yfiles_extension(self):
+ data = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:y="http://www.yworks.com/xml/graphml"
+ xmlns:yed="http://www.yworks.com/xml/yed/3"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <!--Created by yFiles for Java 2.7-->
+ <key for="graphml" id="d0" yfiles.type="resources"/>
+ <key attr.name="url" attr.type="string" for="node" id="d1"/>
+ <key attr.name="description" attr.type="string" for="node" id="d2"/>
+ <key for="node" id="d3" yfiles.type="nodegraphics"/>
+ <key attr.name="Description" attr.type="string" for="graph" id="d4">
+ <default/>
+ </key>
+ <key attr.name="url" attr.type="string" for="edge" id="d5"/>
+ <key attr.name="description" attr.type="string" for="edge" id="d6"/>
+ <key for="edge" id="d7" yfiles.type="edgegraphics"/>
+ <graph edgedefault="directed" id="G">
+ <node id="n0">
+ <data key="d3">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="125.0" y="100.0"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content"
+ borderDistance="0.0" fontFamily="Dialog" fontSize="13"
+ fontStyle="plain" hasBackgroundColor="false" hasLineColor="false"
+ height="19.1328125" modelName="internal" modelPosition="c"
+ textColor="#000000" visible="true" width="12.27099609375"
+ x="8.864501953125" y="5.43359375">1</y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ <node id="n1">
+ <data key="d3">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="183.0" y="205.0"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content"
+ borderDistance="0.0" fontFamily="Dialog" fontSize="13"
+ fontStyle="plain" hasBackgroundColor="false" hasLineColor="false"
+ height="19.1328125" modelName="internal" modelPosition="c"
+ textColor="#000000" visible="true" width="12.27099609375"
+ x="8.864501953125" y="5.43359375">2</y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ <node id="n2">
+ <data key="d6" xml:space="preserve"><![CDATA[description
+line1
+line2]]></data>
+ <data key="d3">
+ <y:GenericNode configuration="com.yworks.flowchart.terminator">
+ <y:Geometry height="40.0" width="80.0" x="950.0" y="286.0"/>
+ <y:Fill color="#E8EEF7" color2="#B7C9E3" transparent="false"/>
+ <y:BorderStyle color="#000000" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content"
+ fontFamily="Dialog" fontSize="12" fontStyle="plain"
+ hasBackgroundColor="false" hasLineColor="false" height="17.96875"
+ horizontalTextPosition="center" iconTextGap="4" modelName="custom"
+ textColor="#000000" verticalTextPosition="bottom" visible="true"
+ width="67.984375" x="6.0078125" xml:space="preserve"
+ y="11.015625">3<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/></y:LabelModel>
+ <y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0"
+ labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0"
+ offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
+ </y:GenericNode>
+ </data>
+ </node>
+ <edge id="e0" source="n0" target="n1">
+ <data key="d7">
+ <y:PolyLineEdge>
+ <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
+ <y:LineStyle color="#000000" type="line" width="1.0"/>
+ <y:Arrows source="none" target="standard"/>
+ <y:BendStyle smoothed="false"/>
+ </y:PolyLineEdge>
+ </data>
+ </edge>
+ </graph>
+ <data key="d0">
+ <y:Resources/>
+ </data>
+</graphml>
+"""
+ fh = io.BytesIO(data.encode("UTF-8"))
+ G = nx.read_graphml(fh, force_multigraph=True)
+ assert list(G.edges()) == [("n0", "n1")]
+ assert G.has_edge("n0", "n1", key="e0")
+ assert G.nodes["n0"]["label"] == "1"
+ assert G.nodes["n1"]["label"] == "2"
+ assert G.nodes["n2"]["label"] == "3"
+ assert G.nodes["n0"]["shape_type"] == "rectangle"
+ assert G.nodes["n1"]["shape_type"] == "rectangle"
+ assert G.nodes["n2"]["shape_type"] == "com.yworks.flowchart.terminator"
+ assert G.nodes["n2"]["description"] == "description\nline1\nline2"
+ fh.seek(0)
+ G = nx.read_graphml(fh)
+ assert list(G.edges()) == [("n0", "n1")]
+ assert G["n0"]["n1"]["id"] == "e0"
+ assert G.nodes["n0"]["label"] == "1"
+ assert G.nodes["n1"]["label"] == "2"
+ assert G.nodes["n2"]["label"] == "3"
+ assert G.nodes["n0"]["shape_type"] == "rectangle"
+ assert G.nodes["n1"]["shape_type"] == "rectangle"
+ assert G.nodes["n2"]["shape_type"] == "com.yworks.flowchart.terminator"
+ assert G.nodes["n2"]["description"] == "description\nline1\nline2"
+
+ H = nx.parse_graphml(data, force_multigraph=True)
+ assert list(H.edges()) == [("n0", "n1")]
+ assert H.has_edge("n0", "n1", key="e0")
+ assert H.nodes["n0"]["label"] == "1"
+ assert H.nodes["n1"]["label"] == "2"
+ assert H.nodes["n2"]["label"] == "3"
+
+ H = nx.parse_graphml(data)
+ assert list(H.edges()) == [("n0", "n1")]
+ assert H["n0"]["n1"]["id"] == "e0"
+ assert H.nodes["n0"]["label"] == "1"
+ assert H.nodes["n1"]["label"] == "2"
+ assert H.nodes["n2"]["label"] == "3"
+
+ def test_bool(self):
+ s = """<?xml version="1.0" encoding="UTF-8"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="d0" for="node" attr.name="test" attr.type="boolean">
+ <default>false</default>
+ </key>
+ <graph id="G" edgedefault="directed">
+ <node id="n0">
+ <data key="d0">true</data>
+ </node>
+ <node id="n1"/>
+ <node id="n2">
+ <data key="d0">false</data>
+ </node>
+ <node id="n3">
+ <data key="d0">FaLsE</data>
+ </node>
+ <node id="n4">
+ <data key="d0">True</data>
+ </node>
+ <node id="n5">
+ <data key="d0">0</data>
+ </node>
+ <node id="n6">
+ <data key="d0">1</data>
+ </node>
+ </graph>
+</graphml>
+"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ G = nx.read_graphml(fh)
+ H = nx.parse_graphml(s)
+ for graph in [G, H]:
+ assert graph.nodes["n0"]["test"]
+ assert not graph.nodes["n2"]["test"]
+ assert not graph.nodes["n3"]["test"]
+ assert graph.nodes["n4"]["test"]
+ assert not graph.nodes["n5"]["test"]
+ assert graph.nodes["n6"]["test"]
+
+ def test_graphml_header_line(self):
+ good = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key id="d0" for="node" attr.name="test" attr.type="boolean">
+ <default>false</default>
+ </key>
+ <graph id="G">
+ <node id="n0">
+ <data key="d0">true</data>
+ </node>
+ </graph>
+</graphml>
+"""
+ bad = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml>
+ <key id="d0" for="node" attr.name="test" attr.type="boolean">
+ <default>false</default>
+ </key>
+ <graph id="G">
+ <node id="n0">
+ <data key="d0">true</data>
+ </node>
+ </graph>
+</graphml>
+"""
+ ugly = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="https://ghghgh">
+ <key id="d0" for="node" attr.name="test" attr.type="boolean">
+ <default>false</default>
+ </key>
+ <graph id="G">
+ <node id="n0">
+ <data key="d0">true</data>
+ </node>
+ </graph>
+</graphml>
+"""
+ for s in (good, bad):
+ fh = io.BytesIO(s.encode("UTF-8"))
+ G = nx.read_graphml(fh)
+ H = nx.parse_graphml(s)
+ for graph in [G, H]:
+ assert graph.nodes["n0"]["test"]
+
+ fh = io.BytesIO(ugly.encode("UTF-8"))
+ pytest.raises(nx.NetworkXError, nx.read_graphml, fh)
+ pytest.raises(nx.NetworkXError, nx.parse_graphml, ugly)
+
+ def test_read_attributes_with_groups(self):
+ data = """\
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
+ <!--Created by yEd 3.17-->
+ <key attr.name="Description" attr.type="string" for="graph" id="d0"/>
+ <key for="port" id="d1" yfiles.type="portgraphics"/>
+ <key for="port" id="d2" yfiles.type="portgeometry"/>
+ <key for="port" id="d3" yfiles.type="portuserdata"/>
+ <key attr.name="CustomProperty" attr.type="string" for="node" id="d4">
+ <default/>
+ </key>
+ <key attr.name="url" attr.type="string" for="node" id="d5"/>
+ <key attr.name="description" attr.type="string" for="node" id="d6"/>
+ <key for="node" id="d7" yfiles.type="nodegraphics"/>
+ <key for="graphml" id="d8" yfiles.type="resources"/>
+ <key attr.name="url" attr.type="string" for="edge" id="d9"/>
+ <key attr.name="description" attr.type="string" for="edge" id="d10"/>
+ <key for="edge" id="d11" yfiles.type="edgegraphics"/>
+ <graph edgedefault="directed" id="G">
+ <data key="d0"/>
+ <node id="n0">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="125.0" y="-255.4611111111111"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="11.634765625" x="9.1826171875" y="6.015625">2<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/>
+ </y:LabelModel>
+ <y:ModelParameter>
+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
+ </y:ModelParameter>
+ </y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ <node id="n1" yfiles.foldertype="group">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d5"/>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ProxyAutoBoundsNode>
+ <y:Realizers active="0">
+ <y:GroupNode>
+ <y:Geometry height="250.38333333333333" width="140.0" x="-30.0" y="-330.3833333333333"/>
+ <y:Fill color="#F5F5F5" transparent="false"/>
+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="140.0" x="0.0" y="0.0">Group 3</y:NodeLabel>
+ <y:Shape type="roundrectangle"/>
+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
+ <y:BorderInsets bottom="1" bottomF="1.0" left="0" leftF="0.0" right="0" rightF="0.0" top="1" topF="1.0001736111111086"/>
+ </y:GroupNode>
+ <y:GroupNode>
+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
+ <y:Fill color="#F5F5F5" transparent="false"/>
+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" y="0.0">Folder 3</y:NodeLabel>
+ <y:Shape type="roundrectangle"/>
+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
+ </y:GroupNode>
+ </y:Realizers>
+ </y:ProxyAutoBoundsNode>
+ </data>
+ <graph edgedefault="directed" id="n1:">
+ <node id="n1::n0" yfiles.foldertype="group">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d5"/>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ProxyAutoBoundsNode>
+ <y:Realizers active="0">
+ <y:GroupNode>
+ <y:Geometry height="83.46111111111111" width="110.0" x="-15.0" y="-292.9222222222222"/>
+ <y:Fill color="#F5F5F5" transparent="false"/>
+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.0" x="0.0" y="0.0">Group 1</y:NodeLabel>
+ <y:Shape type="roundrectangle"/>
+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
+ <y:BorderInsets bottom="1" bottomF="1.0" left="0" leftF="0.0" right="0" rightF="0.0" top="1" topF="1.0001736111111086"/>
+ </y:GroupNode>
+ <y:GroupNode>
+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
+ <y:Fill color="#F5F5F5" transparent="false"/>
+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" y="0.0">Folder 1</y:NodeLabel>
+ <y:Shape type="roundrectangle"/>
+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
+ </y:GroupNode>
+ </y:Realizers>
+ </y:ProxyAutoBoundsNode>
+ </data>
+ <graph edgedefault="directed" id="n1::n0:">
+ <node id="n1::n0::n0">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="50.0" y="-255.4611111111111"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="11.634765625" x="9.1826171875" y="6.015625">1<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/>
+ </y:LabelModel>
+ <y:ModelParameter>
+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
+ </y:ModelParameter>
+ </y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ <node id="n1::n0::n1">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="0.0" y="-255.4611111111111"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="11.634765625" x="9.1826171875" y="6.015625">3<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/>
+ </y:LabelModel>
+ <y:ModelParameter>
+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
+ </y:ModelParameter>
+ </y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ </graph>
+ </node>
+ <node id="n1::n1" yfiles.foldertype="group">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d5"/>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ProxyAutoBoundsNode>
+ <y:Realizers active="0">
+ <y:GroupNode>
+ <y:Geometry height="83.46111111111111" width="110.0" x="-15.0" y="-179.4611111111111"/>
+ <y:Fill color="#F5F5F5" transparent="false"/>
+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.0" x="0.0" y="0.0">Group 2</y:NodeLabel>
+ <y:Shape type="roundrectangle"/>
+ <y:State closed="false" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
+ <y:Insets bottom="15" bottomF="15.0" left="15" leftF="15.0" right="15" rightF="15.0" top="15" topF="15.0"/>
+ <y:BorderInsets bottom="1" bottomF="1.0" left="0" leftF="0.0" right="0" rightF="0.0" top="1" topF="1.0001736111111086"/>
+ </y:GroupNode>
+ <y:GroupNode>
+ <y:Geometry height="50.0" width="50.0" x="0.0" y="60.0"/>
+ <y:Fill color="#F5F5F5" transparent="false"/>
+ <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
+ <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="21.4609375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.201171875" x="-7.6005859375" y="0.0">Folder 2</y:NodeLabel>
+ <y:Shape type="roundrectangle"/>
+ <y:State closed="true" closedHeight="50.0" closedWidth="50.0" innerGraphDisplayEnabled="false"/>
+ <y:Insets bottom="5" bottomF="5.0" left="5" leftF="5.0" right="5" rightF="5.0" top="5" topF="5.0"/>
+ <y:BorderInsets bottom="0" bottomF="0.0" left="0" leftF="0.0" right="0" rightF="0.0" top="0" topF="0.0"/>
+ </y:GroupNode>
+ </y:Realizers>
+ </y:ProxyAutoBoundsNode>
+ </data>
+ <graph edgedefault="directed" id="n1::n1:">
+ <node id="n1::n1::n0">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="0.0" y="-142.0"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="11.634765625" x="9.1826171875" y="6.015625">5<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/>
+ </y:LabelModel>
+ <y:ModelParameter>
+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
+ </y:ModelParameter>
+ </y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ <node id="n1::n1::n1">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="50.0" y="-142.0"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="11.634765625" x="9.1826171875" y="6.015625">6<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/>
+ </y:LabelModel>
+ <y:ModelParameter>
+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
+ </y:ModelParameter>
+ </y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ </graph>
+ </node>
+ </graph>
+ </node>
+ <node id="n2">
+ <data key="d4"><![CDATA[CustomPropertyValue]]></data>
+ <data key="d6"/>
+ <data key="d7">
+ <y:ShapeNode>
+ <y:Geometry height="30.0" width="30.0" x="125.0" y="-142.0"/>
+ <y:Fill color="#FFCC00" transparent="false"/>
+ <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
+ <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="11.634765625" x="9.1826171875" y="6.015625">9<y:LabelModel>
+ <y:SmartNodeLabelModel distance="4.0"/>
+ </y:LabelModel>
+ <y:ModelParameter>
+ <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
+ </y:ModelParameter>
+ </y:NodeLabel>
+ <y:Shape type="rectangle"/>
+ </y:ShapeNode>
+ </data>
+ </node>
+ <edge id="n1::n1::e0" source="n1::n1::n0" target="n1::n1::n1">
+ <data key="d10"/>
+ <data key="d11">
+ <y:PolyLineEdge>
+ <y:Path sx="15.0" sy="-0.0" tx="-15.0" ty="-0.0"/>
+ <y:LineStyle color="#000000" type="line" width="1.0"/>
+ <y:Arrows source="none" target="standard"/>
+ <y:BendStyle smoothed="false"/>
+ </y:PolyLineEdge>
+ </data>
+ </edge>
+ <edge id="n1::n0::e0" source="n1::n0::n1" target="n1::n0::n0">
+ <data key="d10"/>
+ <data key="d11">
+ <y:PolyLineEdge>
+ <y:Path sx="15.0" sy="-0.0" tx="-15.0" ty="-0.0"/>
+ <y:LineStyle color="#000000" type="line" width="1.0"/>
+ <y:Arrows source="none" target="standard"/>
+ <y:BendStyle smoothed="false"/>
+ </y:PolyLineEdge>
+ </data>
+ </edge>
+ <edge id="e0" source="n1::n0::n0" target="n0">
+ <data key="d10"/>
+ <data key="d11">
+ <y:PolyLineEdge>
+ <y:Path sx="15.0" sy="-0.0" tx="-15.0" ty="-0.0"/>
+ <y:LineStyle color="#000000" type="line" width="1.0"/>
+ <y:Arrows source="none" target="standard"/>
+ <y:BendStyle smoothed="false"/>
+ </y:PolyLineEdge>
+ </data>
+ </edge>
+ <edge id="e1" source="n1::n1::n1" target="n2">
+ <data key="d10"/>
+ <data key="d11">
+ <y:PolyLineEdge>
+ <y:Path sx="15.0" sy="-0.0" tx="-15.0" ty="-0.0"/>
+ <y:LineStyle color="#000000" type="line" width="1.0"/>
+ <y:Arrows source="none" target="standard"/>
+ <y:BendStyle smoothed="false"/>
+ </y:PolyLineEdge>
+ </data>
+ </edge>
+ </graph>
+ <data key="d8">
+ <y:Resources/>
+ </data>
+</graphml>
+"""
+ # verify that nodes / attributes are correctly read when part of a group
+ fh = io.BytesIO(data.encode("UTF-8"))
+ G = nx.read_graphml(fh)
+ data = [x for _, x in G.nodes(data=True)]
+ assert len(data) == 9
+ for node_data in data:
+ assert node_data["CustomProperty"] != ""
+
+ def test_long_attribute_type(self):
+ # test that graphs with attr.type="long" (as produced by botch and
+ # dose3) can be parsed
+ s = """<?xml version='1.0' encoding='utf-8'?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns
+ http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+ <key attr.name="cudfversion" attr.type="long" for="node" id="d6" />
+ <graph edgedefault="directed">
+ <node id="n1">
+ <data key="d6">4284</data>
+ </node>
+ </graph>
+</graphml>"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ G = nx.read_graphml(fh)
+ expected = [("n1", {"cudfversion": 4284})]
+ assert sorted(G.nodes(data=True)) == expected
+ fh.seek(0)
+ H = nx.parse_graphml(s)
+ assert sorted(H.nodes(data=True)) == expected
+
+
+class TestWriteGraphML(BaseGraphML):
+ writer = staticmethod(nx.write_graphml_lxml)
+
+ @classmethod
+ def setup_class(cls):
+ BaseGraphML.setup_class()
+ _ = pytest.importorskip("lxml.etree")
+
+ def test_write_interface(self):
+ try:
+ import lxml.etree
+
+ assert nx.write_graphml == nx.write_graphml_lxml
+ except ImportError:
+ assert nx.write_graphml == nx.write_graphml_xml
+
+ def test_write_read_simple_directed_graphml(self):
+ G = self.simple_directed_graph
+ G.graph["hi"] = "there"
+ fh = io.BytesIO()
+ self.writer(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(G.edges()) == sorted(H.edges())
+ assert sorted(G.edges(data=True)) == sorted(H.edges(data=True))
+ self.simple_directed_fh.seek(0)
+
+ def test_GraphMLWriter_add_graphs(self):
+ gmlw = GraphMLWriter()
+ G = self.simple_directed_graph
+ H = G.copy()
+ gmlw.add_graphs([G, H])
+
+ def test_write_read_simple_no_prettyprint(self):
+ G = self.simple_directed_graph
+ G.graph["hi"] = "there"
+ G.graph["id"] = "1"
+ fh = io.BytesIO()
+ self.writer(G, fh, prettyprint=False)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ assert sorted(G.nodes()) == sorted(H.nodes())
+ assert sorted(G.edges()) == sorted(H.edges())
+ assert sorted(G.edges(data=True)) == sorted(H.edges(data=True))
+ self.simple_directed_fh.seek(0)
+
+ def test_write_read_attribute_named_key_ids_graphml(self):
+ from xml.etree.ElementTree import parse
+
+ G = self.attribute_named_key_ids_graph
+ fh = io.BytesIO()
+ self.writer(G, fh, named_key_ids=True)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ fh.seek(0)
+
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ assert edges_equal(G.edges(data=True), H.edges(data=True))
+ self.attribute_named_key_ids_fh.seek(0)
+
+ xml = parse(fh)
+ # Children are the key elements, and the graph element
+ children = list(xml.getroot())
+ assert len(children) == 4
+
+ keys = [child.items() for child in children[:3]]
+
+ assert len(keys) == 3
+ assert ("id", "edge_prop") in keys[0]
+ assert ("attr.name", "edge_prop") in keys[0]
+ assert ("id", "prop2") in keys[1]
+ assert ("attr.name", "prop2") in keys[1]
+ assert ("id", "prop1") in keys[2]
+ assert ("attr.name", "prop1") in keys[2]
+
+ # Confirm the read graph nodes/edge are identical when compared to
+ # default writing behavior.
+ default_behavior_fh = io.BytesIO()
+ nx.write_graphml(G, default_behavior_fh)
+ default_behavior_fh.seek(0)
+ H = nx.read_graphml(default_behavior_fh)
+
+ named_key_ids_behavior_fh = io.BytesIO()
+ nx.write_graphml(G, named_key_ids_behavior_fh, named_key_ids=True)
+ named_key_ids_behavior_fh.seek(0)
+ J = nx.read_graphml(named_key_ids_behavior_fh)
+
+ assert all(n1 == n2 for (n1, n2) in zip(H.nodes, J.nodes))
+ assert all(e1 == e2 for (e1, e2) in zip(H.edges, J.edges))
+
+ def test_write_read_attribute_numeric_type_graphml(self):
+ from xml.etree.ElementTree import parse
+
+ G = self.attribute_numeric_type_graph
+ fh = io.BytesIO()
+ self.writer(G, fh, infer_numeric_types=True)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ fh.seek(0)
+
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ assert edges_equal(G.edges(data=True), H.edges(data=True))
+ self.attribute_numeric_type_fh.seek(0)
+
+ xml = parse(fh)
+ # Children are the key elements, and the graph element
+ children = list(xml.getroot())
+ assert len(children) == 3
+
+ keys = [child.items() for child in children[:2]]
+
+ assert len(keys) == 2
+ assert ("attr.type", "double") in keys[0]
+ assert ("attr.type", "double") in keys[1]
+
+ def test_more_multigraph_keys(self, tmp_path):
+ """Writing keys as edge id attributes means keys become strings.
+ The original keys are stored as data, so read them back in
+ if `str(key) == edge_id`
+ This allows the adjacency to remain the same.
+ """
+ G = nx.MultiGraph()
+ G.add_edges_from([("a", "b", 2), ("a", "b", 3)])
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname)
+ H = nx.read_graphml(fname)
+ assert H.is_multigraph()
+ assert edges_equal(G.edges(keys=True), H.edges(keys=True))
+ assert G._adj == H._adj
+
+ def test_default_attribute(self):
+ G = nx.Graph(name="Fred")
+ G.add_node(1, label=1, color="green")
+ nx.add_path(G, [0, 1, 2, 3])
+ G.add_edge(1, 2, weight=3)
+ G.graph["node_default"] = {"color": "yellow"}
+ G.graph["edge_default"] = {"weight": 7}
+ fh = io.BytesIO()
+ self.writer(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh, node_type=int)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ assert G.graph == H.graph
+
+ def test_mixed_type_attributes(self):
+ G = nx.MultiGraph()
+ G.add_node("n0", special=False)
+ G.add_node("n1", special=0)
+ G.add_edge("n0", "n1", special=False)
+ G.add_edge("n0", "n1", special=0)
+ fh = io.BytesIO()
+ self.writer(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ assert not H.nodes["n0"]["special"]
+ assert H.nodes["n1"]["special"] == 0
+ assert not H.edges["n0", "n1", 0]["special"]
+ assert H.edges["n0", "n1", 1]["special"] == 0
+
+ def test_str_number_mixed_type_attributes(self):
+ G = nx.MultiGraph()
+ G.add_node("n0", special="hello")
+ G.add_node("n1", special=0)
+ G.add_edge("n0", "n1", special="hello")
+ G.add_edge("n0", "n1", special=0)
+ fh = io.BytesIO()
+ self.writer(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ assert H.nodes["n0"]["special"] == "hello"
+ assert H.nodes["n1"]["special"] == 0
+ assert H.edges["n0", "n1", 0]["special"] == "hello"
+ assert H.edges["n0", "n1", 1]["special"] == 0
+
+ def test_mixed_int_type_number_attributes(self):
+ np = pytest.importorskip("numpy")
+ G = nx.MultiGraph()
+ G.add_node("n0", special=np.int64(0))
+ G.add_node("n1", special=1)
+ G.add_edge("n0", "n1", special=np.int64(2))
+ G.add_edge("n0", "n1", special=3)
+ fh = io.BytesIO()
+ self.writer(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ assert H.nodes["n0"]["special"] == 0
+ assert H.nodes["n1"]["special"] == 1
+ assert H.edges["n0", "n1", 0]["special"] == 2
+ assert H.edges["n0", "n1", 1]["special"] == 3
+
+ def test_multigraph_to_graph(self, tmp_path):
+ # test converting multigraph to graph if no parallel edges found
+ G = nx.MultiGraph()
+ G.add_edges_from([("a", "b", 2), ("b", "c", 3)]) # no multiedges
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname)
+ H = nx.read_graphml(fname)
+ assert not H.is_multigraph()
+ H = nx.read_graphml(fname, force_multigraph=True)
+ assert H.is_multigraph()
+
+ # add a multiedge
+ G.add_edge("a", "b", "e-id")
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname)
+ H = nx.read_graphml(fname)
+ assert H.is_multigraph()
+ H = nx.read_graphml(fname, force_multigraph=True)
+ assert H.is_multigraph()
+
+ def test_write_generate_edge_id_from_attribute(self, tmp_path):
+ from xml.etree.ElementTree import parse
+
+ G = nx.Graph()
+ G.add_edges_from([("a", "b"), ("b", "c"), ("a", "c")])
+ edge_attributes = {e: str(e) for e in G.edges}
+ nx.set_edge_attributes(G, edge_attributes, "eid")
+ fname = tmp_path / "test.graphml"
+ # set edge_id_from_attribute e.g. "eid" for write_graphml()
+ self.writer(G, fname, edge_id_from_attribute="eid")
+ # set edge_id_from_attribute e.g. "eid" for generate_graphml()
+ generator = nx.generate_graphml(G, edge_id_from_attribute="eid")
+
+ H = nx.read_graphml(fname)
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ # NetworkX adds explicit edge "id" from file as attribute
+ nx.set_edge_attributes(G, edge_attributes, "id")
+ assert edges_equal(G.edges(data=True), H.edges(data=True))
+
+ tree = parse(fname)
+ children = list(tree.getroot())
+ assert len(children) == 2
+ edge_ids = [
+ edge.attrib["id"]
+ for edge in tree.getroot().findall(
+ ".//{http://graphml.graphdrawing.org/xmlns}edge"
+ )
+ ]
+ # verify edge id value is equal to specified attribute value
+ assert sorted(edge_ids) == sorted(edge_attributes.values())
+
+ # check graphml generated from generate_graphml()
+ data = "".join(generator)
+ J = nx.parse_graphml(data)
+ assert sorted(G.nodes()) == sorted(J.nodes())
+ assert sorted(G.edges()) == sorted(J.edges())
+ # NetworkX adds explicit edge "id" from file as attribute
+ nx.set_edge_attributes(G, edge_attributes, "id")
+ assert edges_equal(G.edges(data=True), J.edges(data=True))
+
+ def test_multigraph_write_generate_edge_id_from_attribute(self, tmp_path):
+ from xml.etree.ElementTree import parse
+
+ G = nx.MultiGraph()
+ G.add_edges_from([("a", "b"), ("b", "c"), ("a", "c"), ("a", "b")])
+ edge_attributes = {e: str(e) for e in G.edges}
+ nx.set_edge_attributes(G, edge_attributes, "eid")
+ fname = tmp_path / "test.graphml"
+ # set edge_id_from_attribute e.g. "eid" for write_graphml()
+ self.writer(G, fname, edge_id_from_attribute="eid")
+ # set edge_id_from_attribute e.g. "eid" for generate_graphml()
+ generator = nx.generate_graphml(G, edge_id_from_attribute="eid")
+
+ H = nx.read_graphml(fname)
+ assert H.is_multigraph()
+ H = nx.read_graphml(fname, force_multigraph=True)
+ assert H.is_multigraph()
+
+ assert nodes_equal(G.nodes(), H.nodes())
+ assert edges_equal(G.edges(), H.edges())
+ assert sorted(data.get("eid") for u, v, data in H.edges(data=True)) == sorted(
+ edge_attributes.values()
+ )
+ # NetworkX uses edge_ids as keys in multigraphs if no key
+ assert sorted(key for u, v, key in H.edges(keys=True)) == sorted(
+ edge_attributes.values()
+ )
+
+ tree = parse(fname)
+ children = list(tree.getroot())
+ assert len(children) == 2
+ edge_ids = [
+ edge.attrib["id"]
+ for edge in tree.getroot().findall(
+ ".//{http://graphml.graphdrawing.org/xmlns}edge"
+ )
+ ]
+ # verify edge id value is equal to specified attribute value
+ assert sorted(edge_ids) == sorted(edge_attributes.values())
+
+ # check graphml generated from generate_graphml()
+ graphml_data = "".join(generator)
+ J = nx.parse_graphml(graphml_data)
+ assert J.is_multigraph()
+
+ assert nodes_equal(G.nodes(), J.nodes())
+ assert edges_equal(G.edges(), J.edges())
+ assert sorted(data.get("eid") for u, v, data in J.edges(data=True)) == sorted(
+ edge_attributes.values()
+ )
+ # NetworkX uses edge_ids as keys in multigraphs if no key
+ assert sorted(key for u, v, key in J.edges(keys=True)) == sorted(
+ edge_attributes.values()
+ )
+
+ def test_numpy_float64(self, tmp_path):
+ np = pytest.importorskip("numpy")
+ wt = np.float64(3.4)
+ G = nx.Graph([(1, 2, {"weight": wt})])
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname)
+ H = nx.read_graphml(fname, node_type=int)
+ assert G.edges == H.edges
+ wtG = G[1][2]["weight"]
+ wtH = H[1][2]["weight"]
+ assert wtG == pytest.approx(wtH, abs=1e-6)
+ assert type(wtG) == np.float64
+ assert type(wtH) == float
+
+ def test_numpy_float32(self, tmp_path):
+ np = pytest.importorskip("numpy")
+ wt = np.float32(3.4)
+ G = nx.Graph([(1, 2, {"weight": wt})])
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname)
+ H = nx.read_graphml(fname, node_type=int)
+ assert G.edges == H.edges
+ wtG = G[1][2]["weight"]
+ wtH = H[1][2]["weight"]
+ assert wtG == pytest.approx(wtH, abs=1e-6)
+ assert type(wtG) == np.float32
+ assert type(wtH) == float
+
+ def test_numpy_float64_inference(self, tmp_path):
+ np = pytest.importorskip("numpy")
+ G = self.attribute_numeric_type_graph
+ G.edges[("n1", "n1")]["weight"] = np.float64(1.1)
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname, infer_numeric_types=True)
+ H = nx.read_graphml(fname)
+ assert G._adj == H._adj
+
+ def test_unicode_attributes(self, tmp_path):
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ node_type = str
+ G.add_edge(name1, "Radiohead", foo=name2)
+ fname = tmp_path / "test.graphml"
+ self.writer(G, fname)
+ H = nx.read_graphml(fname, node_type=node_type)
+ assert G._adj == H._adj
+
+ def test_unicode_escape(self):
+ # test for handling json escaped strings in python 2 Issue #1880
+ import json
+
+ a = {"a": '{"a": "123"}'} # an object with many chars to escape
+ sa = json.dumps(a)
+ G = nx.Graph()
+ G.graph["test"] = sa
+ fh = io.BytesIO()
+ self.writer(G, fh)
+ fh.seek(0)
+ H = nx.read_graphml(fh)
+ assert G.graph["test"] == H.graph["test"]
+
+
+class TestXMLGraphML(TestWriteGraphML):
+ writer = staticmethod(nx.write_graphml_xml)
+
+ @classmethod
+ def setup_class(cls):
+ TestWriteGraphML.setup_class()
+
+
+def test_exception_for_unsupported_datatype_node_attr():
+ """Test that a detailed exception is raised when an attribute is of a type
+ not supported by GraphML, e.g. a list"""
+ pytest.importorskip("lxml.etree")
+ # node attribute
+ G = nx.Graph()
+ G.add_node(0, my_list_attribute=[0, 1, 2])
+ fh = io.BytesIO()
+ with pytest.raises(TypeError, match="GraphML does not support"):
+ nx.write_graphml(G, fh)
+
+
+def test_exception_for_unsupported_datatype_edge_attr():
+ """Test that a detailed exception is raised when an attribute is of a type
+ not supported by GraphML, e.g. a list"""
+ pytest.importorskip("lxml.etree")
+ # edge attribute
+ G = nx.Graph()
+ G.add_edge(0, 1, my_list_attribute=[0, 1, 2])
+ fh = io.BytesIO()
+ with pytest.raises(TypeError, match="GraphML does not support"):
+ nx.write_graphml(G, fh)
+
+
+def test_exception_for_unsupported_datatype_graph_attr():
+ """Test that a detailed exception is raised when an attribute is of a type
+ not supported by GraphML, e.g. a list"""
+ pytest.importorskip("lxml.etree")
+ # graph attribute
+ G = nx.Graph()
+ G.graph["my_list_attribute"] = [0, 1, 2]
+ fh = io.BytesIO()
+ with pytest.raises(TypeError, match="GraphML does not support"):
+ nx.write_graphml(G, fh)
+
+
+def test_empty_attribute():
+ """Tests that a GraphML string with an empty attribute can be parsed
+ correctly."""
+ s = """<?xml version='1.0' encoding='utf-8'?>
+ <graphml>
+ <key id="d1" for="node" attr.name="foo" attr.type="string"/>
+ <key id="d2" for="node" attr.name="bar" attr.type="string"/>
+ <graph>
+ <node id="0">
+ <data key="d1">aaa</data>
+ <data key="d2">bbb</data>
+ </node>
+ <node id="1">
+ <data key="d1">ccc</data>
+ <data key="d2"></data>
+ </node>
+ </graph>
+ </graphml>"""
+ fh = io.BytesIO(s.encode("UTF-8"))
+ G = nx.read_graphml(fh)
+ assert G.nodes["0"] == {"foo": "aaa", "bar": "bbb"}
+ assert G.nodes["1"] == {"foo": "ccc", "bar": ""}
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py
new file mode 100644
index 00000000..8ac5ecc3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py
@@ -0,0 +1,30 @@
+import io
+
+import networkx as nx
+
+
+class TestLEDA:
+ def test_parse_leda(self):
+ data = """#header section \nLEDA.GRAPH \nstring\nint\n-1\n#nodes section\n5 \n|{v1}| \n|{v2}| \n|{v3}| \n|{v4}| \n|{v5}| \n\n#edges section\n7 \n1 2 0 |{4}| \n1 3 0 |{3}| \n2 3 0 |{2}| \n3 4 0 |{3}| \n3 5 0 |{7}| \n4 5 0 |{6}| \n5 1 0 |{foo}|"""
+ G = nx.parse_leda(data)
+ G = nx.parse_leda(data.split("\n"))
+ assert sorted(G.nodes()) == ["v1", "v2", "v3", "v4", "v5"]
+ assert sorted(G.edges(data=True)) == [
+ ("v1", "v2", {"label": "4"}),
+ ("v1", "v3", {"label": "3"}),
+ ("v2", "v3", {"label": "2"}),
+ ("v3", "v4", {"label": "3"}),
+ ("v3", "v5", {"label": "7"}),
+ ("v4", "v5", {"label": "6"}),
+ ("v5", "v1", {"label": "foo"}),
+ ]
+
+ def test_read_LEDA(self):
+ fh = io.BytesIO()
+ data = """#header section \nLEDA.GRAPH \nstring\nint\n-1\n#nodes section\n5 \n|{v1}| \n|{v2}| \n|{v3}| \n|{v4}| \n|{v5}| \n\n#edges section\n7 \n1 2 0 |{4}| \n1 3 0 |{3}| \n2 3 0 |{2}| \n3 4 0 |{3}| \n3 5 0 |{7}| \n4 5 0 |{6}| \n5 1 0 |{foo}|"""
+ G = nx.parse_leda(data)
+ fh.write(data.encode("UTF-8"))
+ fh.seek(0)
+ Gin = nx.read_leda(fh)
+ assert sorted(G.nodes()) == sorted(Gin.nodes())
+ assert sorted(G.edges()) == sorted(Gin.edges())
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py
new file mode 100644
index 00000000..e4c50de7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py
@@ -0,0 +1,62 @@
+import io
+
+import networkx as nx
+from networkx.readwrite.p2g import read_p2g, write_p2g
+from networkx.utils import edges_equal
+
+
+class TestP2G:
+ @classmethod
+ def setup_class(cls):
+ cls.G = nx.Graph(name="test")
+ e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")]
+ cls.G.add_edges_from(e)
+ cls.G.add_node("g")
+ cls.DG = nx.DiGraph(cls.G)
+
+ def test_read_p2g(self):
+ s = b"""\
+name
+3 4
+a
+1 2
+b
+
+c
+0 2
+"""
+ bytesIO = io.BytesIO(s)
+ G = read_p2g(bytesIO)
+ assert G.name == "name"
+ assert sorted(G) == ["a", "b", "c"]
+ edges = [(str(u), str(v)) for u, v in G.edges()]
+ assert edges_equal(G.edges(), [("a", "c"), ("a", "b"), ("c", "a"), ("c", "c")])
+
+ def test_write_p2g(self):
+ s = b"""foo
+3 2
+1
+1
+2
+2
+3
+
+"""
+ fh = io.BytesIO()
+ G = nx.DiGraph()
+ G.name = "foo"
+ G.add_edges_from([(1, 2), (2, 3)])
+ write_p2g(G, fh)
+ fh.seek(0)
+ r = fh.read()
+ assert r == s
+
+ def test_write_read_p2g(self):
+ fh = io.BytesIO()
+ G = nx.DiGraph()
+ G.name = "foo"
+ G.add_edges_from([("a", "b"), ("b", "c")])
+ write_p2g(G, fh)
+ fh.seek(0)
+ H = read_p2g(fh)
+ assert edges_equal(G.edges(), H.edges())
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py
new file mode 100644
index 00000000..317ebe8e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py
@@ -0,0 +1,126 @@
+"""
+Pajek tests
+"""
+
+import networkx as nx
+from networkx.utils import edges_equal, nodes_equal
+
+
+class TestPajek:
+ @classmethod
+ def setup_class(cls):
+ cls.data = """*network Tralala\n*vertices 4\n 1 "A1" 0.0938 0.0896 ellipse x_fact 1 y_fact 1\n 2 "Bb" 0.8188 0.2458 ellipse x_fact 1 y_fact 1\n 3 "C" 0.3688 0.7792 ellipse x_fact 1\n 4 "D2" 0.9583 0.8563 ellipse x_fact 1\n*arcs\n1 1 1 h2 0 w 3 c Blue s 3 a1 -130 k1 0.6 a2 -130 k2 0.6 ap 0.5 l "Bezier loop" lc BlueViolet fos 20 lr 58 lp 0.3 la 360\n2 1 1 h2 0 a1 120 k1 1.3 a2 -120 k2 0.3 ap 25 l "Bezier arc" lphi 270 la 180 lr 19 lp 0.5\n1 2 1 h2 0 a1 40 k1 2.8 a2 30 k2 0.8 ap 25 l "Bezier arc" lphi 90 la 0 lp 0.65\n4 2 -1 h2 0 w 1 k1 -2 k2 250 ap 25 l "Circular arc" c Red lc OrangeRed\n3 4 1 p Dashed h2 0 w 2 c OliveGreen ap 25 l "Straight arc" lc PineGreen\n1 3 1 p Dashed h2 0 w 5 k1 -1 k2 -20 ap 25 l "Oval arc" c Brown lc Black\n3 3 -1 h1 6 w 1 h2 12 k1 -2 k2 -15 ap 0.5 l "Circular loop" c Red lc OrangeRed lphi 270 la 180"""
+ cls.G = nx.MultiDiGraph()
+ cls.G.add_nodes_from(["A1", "Bb", "C", "D2"])
+ cls.G.add_edges_from(
+ [
+ ("A1", "A1"),
+ ("A1", "Bb"),
+ ("A1", "C"),
+ ("Bb", "A1"),
+ ("C", "C"),
+ ("C", "D2"),
+ ("D2", "Bb"),
+ ]
+ )
+
+ cls.G.graph["name"] = "Tralala"
+
+ def test_parse_pajek_simple(self):
+ # Example without node positions or shape
+ data = """*Vertices 2\n1 "1"\n2 "2"\n*Edges\n1 2\n2 1"""
+ G = nx.parse_pajek(data)
+ assert sorted(G.nodes()) == ["1", "2"]
+ assert edges_equal(G.edges(), [("1", "2"), ("1", "2")])
+
+ def test_parse_pajek(self):
+ G = nx.parse_pajek(self.data)
+ assert sorted(G.nodes()) == ["A1", "Bb", "C", "D2"]
+ assert edges_equal(
+ G.edges(),
+ [
+ ("A1", "A1"),
+ ("A1", "Bb"),
+ ("A1", "C"),
+ ("Bb", "A1"),
+ ("C", "C"),
+ ("C", "D2"),
+ ("D2", "Bb"),
+ ],
+ )
+
+ def test_parse_pajet_mat(self):
+ data = """*Vertices 3\n1 "one"\n2 "two"\n3 "three"\n*Matrix\n1 1 0\n0 1 0\n0 1 0\n"""
+ G = nx.parse_pajek(data)
+ assert set(G.nodes()) == {"one", "two", "three"}
+ assert G.nodes["two"] == {"id": "2"}
+ assert edges_equal(
+ set(G.edges()),
+ {("one", "one"), ("two", "one"), ("two", "two"), ("two", "three")},
+ )
+
+ def test_read_pajek(self, tmp_path):
+ G = nx.parse_pajek(self.data)
+ # Read data from file
+ fname = tmp_path / "test.pjk"
+ with open(fname, "wb") as fh:
+ fh.write(self.data.encode("UTF-8"))
+
+ Gin = nx.read_pajek(fname)
+ assert sorted(G.nodes()) == sorted(Gin.nodes())
+ assert edges_equal(G.edges(), Gin.edges())
+ assert self.G.graph == Gin.graph
+ for n in G:
+ assert G.nodes[n] == Gin.nodes[n]
+
+ def test_write_pajek(self):
+ import io
+
+ G = nx.parse_pajek(self.data)
+ fh = io.BytesIO()
+ nx.write_pajek(G, fh)
+ fh.seek(0)
+ H = nx.read_pajek(fh)
+ assert nodes_equal(list(G), list(H))
+ assert edges_equal(list(G.edges()), list(H.edges()))
+ # Graph name is left out for now, therefore it is not tested.
+ # assert_equal(G.graph, H.graph)
+
+ def test_ignored_attribute(self):
+ import io
+
+ G = nx.Graph()
+ fh = io.BytesIO()
+ G.add_node(1, int_attr=1)
+ G.add_node(2, empty_attr=" ")
+ G.add_edge(1, 2, int_attr=2)
+ G.add_edge(2, 3, empty_attr=" ")
+
+ import warnings
+
+ with warnings.catch_warnings(record=True) as w:
+ nx.write_pajek(G, fh)
+ assert len(w) == 4
+
+ def test_noname(self):
+ # Make sure we can parse a line such as: *network
+ # Issue #952
+ line = "*network\n"
+ other_lines = self.data.split("\n")[1:]
+ data = line + "\n".join(other_lines)
+ G = nx.parse_pajek(data)
+
+ def test_unicode(self):
+ import io
+
+ G = nx.Graph()
+ name1 = chr(2344) + chr(123) + chr(6543)
+ name2 = chr(5543) + chr(1543) + chr(324)
+ G.add_edge(name1, "Radiohead", foo=name2)
+ fh = io.BytesIO()
+ nx.write_pajek(G, fh)
+ fh.seek(0)
+ H = nx.read_pajek(fh)
+ assert nodes_equal(list(G), list(H))
+ assert edges_equal(list(G.edges()), list(H.edges()))
+ assert G.graph == H.graph
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py
new file mode 100644
index 00000000..344ad0e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py
@@ -0,0 +1,166 @@
+from io import BytesIO
+
+import pytest
+
+import networkx as nx
+from networkx.utils import edges_equal, nodes_equal
+
+
+class TestSparseGraph6:
+ def test_from_sparse6_bytes(self):
+ data = b":Q___eDcdFcDeFcE`GaJ`IaHbKNbLM"
+ G = nx.from_sparse6_bytes(data)
+ assert nodes_equal(
+ sorted(G.nodes()),
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
+ )
+ assert edges_equal(
+ G.edges(),
+ [
+ (0, 1),
+ (0, 2),
+ (0, 3),
+ (1, 12),
+ (1, 14),
+ (2, 13),
+ (2, 15),
+ (3, 16),
+ (3, 17),
+ (4, 7),
+ (4, 9),
+ (4, 11),
+ (5, 6),
+ (5, 8),
+ (5, 9),
+ (6, 10),
+ (6, 11),
+ (7, 8),
+ (7, 10),
+ (8, 12),
+ (9, 15),
+ (10, 14),
+ (11, 13),
+ (12, 16),
+ (13, 17),
+ (14, 17),
+ (15, 16),
+ ],
+ )
+
+ def test_from_bytes_multigraph_graph(self):
+ graph_data = b":An"
+ G = nx.from_sparse6_bytes(graph_data)
+ assert type(G) == nx.Graph
+ multigraph_data = b":Ab"
+ M = nx.from_sparse6_bytes(multigraph_data)
+ assert type(M) == nx.MultiGraph
+
+ def test_read_sparse6(self):
+ data = b":Q___eDcdFcDeFcE`GaJ`IaHbKNbLM"
+ G = nx.from_sparse6_bytes(data)
+ fh = BytesIO(data)
+ Gin = nx.read_sparse6(fh)
+ assert nodes_equal(G.nodes(), Gin.nodes())
+ assert edges_equal(G.edges(), Gin.edges())
+
+ def test_read_many_graph6(self):
+ # Read many graphs into list
+ data = b":Q___eDcdFcDeFcE`GaJ`IaHbKNbLM\n" b":Q___dCfDEdcEgcbEGbFIaJ`JaHN`IM"
+ fh = BytesIO(data)
+ glist = nx.read_sparse6(fh)
+ assert len(glist) == 2
+ for G in glist:
+ assert nodes_equal(
+ G.nodes(),
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
+ )
+
+
+class TestWriteSparse6:
+ """Unit tests for writing graphs in the sparse6 format.
+
+ Most of the test cases were checked against the sparse6 encoder in Sage.
+
+ """
+
+ def test_null_graph(self):
+ G = nx.null_graph()
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ assert result.getvalue() == b">>sparse6<<:?\n"
+
+ def test_trivial_graph(self):
+ G = nx.trivial_graph()
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ assert result.getvalue() == b">>sparse6<<:@\n"
+
+ def test_empty_graph(self):
+ G = nx.empty_graph(5)
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ assert result.getvalue() == b">>sparse6<<:D\n"
+
+ def test_large_empty_graph(self):
+ G = nx.empty_graph(68)
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ assert result.getvalue() == b">>sparse6<<:~?@C\n"
+
+ def test_very_large_empty_graph(self):
+ G = nx.empty_graph(258049)
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ assert result.getvalue() == b">>sparse6<<:~~???~?@\n"
+
+ def test_complete_graph(self):
+ G = nx.complete_graph(4)
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ assert result.getvalue() == b">>sparse6<<:CcKI\n"
+
+ def test_no_header(self):
+ G = nx.complete_graph(4)
+ result = BytesIO()
+ nx.write_sparse6(G, result, header=False)
+ assert result.getvalue() == b":CcKI\n"
+
+ def test_padding(self):
+ codes = (b":Cdv", b":DaYn", b":EaYnN", b":FaYnL", b":GaYnLz")
+ for n, code in enumerate(codes, start=4):
+ G = nx.path_graph(n)
+ result = BytesIO()
+ nx.write_sparse6(G, result, header=False)
+ assert result.getvalue() == code + b"\n"
+
+ def test_complete_bipartite(self):
+ G = nx.complete_bipartite_graph(6, 9)
+ result = BytesIO()
+ nx.write_sparse6(G, result)
+ # Compared with sage
+ expected = b">>sparse6<<:Nk" + b"?G`cJ" * 9 + b"\n"
+ assert result.getvalue() == expected
+
+ def test_read_write_inverse(self):
+ for i in list(range(13)) + [31, 47, 62, 63, 64, 72]:
+ m = min(2 * i, i * i // 2)
+ g = nx.random_graphs.gnm_random_graph(i, m, seed=i)
+ gstr = BytesIO()
+ nx.write_sparse6(g, gstr, header=False)
+ # Strip the trailing newline.
+ gstr = gstr.getvalue().rstrip()
+ g2 = nx.from_sparse6_bytes(gstr)
+ assert g2.order() == g.order()
+ assert edges_equal(g2.edges(), g.edges())
+
+ def test_no_directed_graphs(self):
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.write_sparse6(nx.DiGraph(), BytesIO())
+
+ def test_write_path(self, tmp_path):
+ # Get a valid temporary file name
+ fullfilename = str(tmp_path / "test.s6")
+ # file should be closed now, so write_sparse6 can open it
+ nx.write_sparse6(nx.null_graph(), fullfilename)
+ with open(fullfilename, mode="rb") as fh:
+ assert fh.read() == b">>sparse6<<:?\n"
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py
new file mode 100644
index 00000000..b2b74482
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py
@@ -0,0 +1,1742 @@
+import random
+from itertools import product
+from textwrap import dedent
+
+import pytest
+
+import networkx as nx
+
+
+def test_generate_network_text_forest_directed():
+ # Create a directed forest with labels
+ graph = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph)
+ for node in graph.nodes:
+ graph.nodes[node]["label"] = "node_" + chr(ord("a") + node)
+
+ node_target = dedent(
+ """
+ ╙── 0
+ ├─╼ 1
+ │ ├─╼ 3
+ │ └─╼ 4
+ └─╼ 2
+ ├─╼ 5
+ └─╼ 6
+ """
+ ).strip()
+
+ label_target = dedent(
+ """
+ ╙── node_a
+ ├─╼ node_b
+ │ ├─╼ node_d
+ │ └─╼ node_e
+ └─╼ node_c
+ ├─╼ node_f
+ └─╼ node_g
+ """
+ ).strip()
+
+ # Basic node case
+ ret = nx.generate_network_text(graph, with_labels=False)
+ assert "\n".join(ret) == node_target
+
+ # Basic label case
+ ret = nx.generate_network_text(graph, with_labels=True)
+ assert "\n".join(ret) == label_target
+
+
+def test_write_network_text_empty_graph():
+ def _graph_str(g, **kw):
+ printbuf = []
+ nx.write_network_text(g, printbuf.append, end="", **kw)
+ return "\n".join(printbuf)
+
+ assert _graph_str(nx.DiGraph()) == "╙"
+ assert _graph_str(nx.Graph()) == "╙"
+ assert _graph_str(nx.DiGraph(), ascii_only=True) == "+"
+ assert _graph_str(nx.Graph(), ascii_only=True) == "+"
+
+
+def test_write_network_text_within_forest_glyph():
+ g = nx.DiGraph()
+ g.add_nodes_from([1, 2, 3, 4])
+ g.add_edge(2, 4)
+ lines = []
+ write = lines.append
+ nx.write_network_text(g, path=write, end="")
+ nx.write_network_text(g, path=write, ascii_only=True, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╟── 1
+ ╟── 2
+ ╎ └─╼ 4
+ ╙── 3
+ +-- 1
+ +-- 2
+ : L-> 4
+ +-- 3
+ """
+ ).strip()
+ assert text == target
+
+
+def test_generate_network_text_directed_multi_tree():
+ tree1 = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph)
+ tree2 = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph)
+ forest = nx.disjoint_union_all([tree1, tree2])
+ ret = "\n".join(nx.generate_network_text(forest))
+
+ target = dedent(
+ """
+ ╟── 0
+ ╎ ├─╼ 1
+ ╎ │ ├─╼ 3
+ ╎ │ └─╼ 4
+ ╎ └─╼ 2
+ ╎ ├─╼ 5
+ ╎ └─╼ 6
+ ╙── 7
+ ├─╼ 8
+ │ ├─╼ 10
+ │ └─╼ 11
+ └─╼ 9
+ ├─╼ 12
+ └─╼ 13
+ """
+ ).strip()
+ assert ret == target
+
+ tree3 = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph)
+ forest = nx.disjoint_union_all([tree1, tree2, tree3])
+ ret = "\n".join(nx.generate_network_text(forest, sources=[0, 14, 7]))
+
+ target = dedent(
+ """
+ ╟── 0
+ ╎ ├─╼ 1
+ ╎ │ ├─╼ 3
+ ╎ │ └─╼ 4
+ ╎ └─╼ 2
+ ╎ ├─╼ 5
+ ╎ └─╼ 6
+ ╟── 14
+ ╎ ├─╼ 15
+ ╎ │ ├─╼ 17
+ ╎ │ └─╼ 18
+ ╎ └─╼ 16
+ ╎ ├─╼ 19
+ ╎ └─╼ 20
+ ╙── 7
+ ├─╼ 8
+ │ ├─╼ 10
+ │ └─╼ 11
+ └─╼ 9
+ ├─╼ 12
+ └─╼ 13
+ """
+ ).strip()
+ assert ret == target
+
+ ret = "\n".join(
+ nx.generate_network_text(forest, sources=[0, 14, 7], ascii_only=True)
+ )
+
+ target = dedent(
+ """
+ +-- 0
+ : |-> 1
+ : | |-> 3
+ : | L-> 4
+ : L-> 2
+ : |-> 5
+ : L-> 6
+ +-- 14
+ : |-> 15
+ : | |-> 17
+ : | L-> 18
+ : L-> 16
+ : |-> 19
+ : L-> 20
+ +-- 7
+ |-> 8
+ | |-> 10
+ | L-> 11
+ L-> 9
+ |-> 12
+ L-> 13
+ """
+ ).strip()
+ assert ret == target
+
+
+def test_generate_network_text_undirected_multi_tree():
+ tree1 = nx.balanced_tree(r=2, h=2, create_using=nx.Graph)
+ tree2 = nx.balanced_tree(r=2, h=2, create_using=nx.Graph)
+ tree2 = nx.relabel_nodes(tree2, {n: n + len(tree1) for n in tree2.nodes})
+ forest = nx.union(tree1, tree2)
+ ret = "\n".join(nx.generate_network_text(forest, sources=[0, 7]))
+
+ target = dedent(
+ """
+ ╟── 0
+ ╎ ├── 1
+ ╎ │ ├── 3
+ ╎ │ └── 4
+ ╎ └── 2
+ ╎ ├── 5
+ ╎ └── 6
+ ╙── 7
+ ├── 8
+ │ ├── 10
+ │ └── 11
+ └── 9
+ ├── 12
+ └── 13
+ """
+ ).strip()
+ assert ret == target
+
+ ret = "\n".join(nx.generate_network_text(forest, sources=[0, 7], ascii_only=True))
+
+ target = dedent(
+ """
+ +-- 0
+ : |-- 1
+ : | |-- 3
+ : | L-- 4
+ : L-- 2
+ : |-- 5
+ : L-- 6
+ +-- 7
+ |-- 8
+ | |-- 10
+ | L-- 11
+ L-- 9
+ |-- 12
+ L-- 13
+ """
+ ).strip()
+ assert ret == target
+
+
+def test_generate_network_text_forest_undirected():
+ # Create a directed forest
+ graph = nx.balanced_tree(r=2, h=2, create_using=nx.Graph)
+
+ node_target0 = dedent(
+ """
+ ╙── 0
+ ├── 1
+ │ ├── 3
+ │ └── 4
+ └── 2
+ ├── 5
+ └── 6
+ """
+ ).strip()
+
+ # defined starting point
+ ret = "\n".join(nx.generate_network_text(graph, sources=[0]))
+ assert ret == node_target0
+
+ # defined starting point
+ node_target2 = dedent(
+ """
+ ╙── 2
+ ├── 0
+ │ └── 1
+ │ ├── 3
+ │ └── 4
+ ├── 5
+ └── 6
+ """
+ ).strip()
+ ret = "\n".join(nx.generate_network_text(graph, sources=[2]))
+ assert ret == node_target2
+
+
+def test_generate_network_text_overspecified_sources():
+ """
+ When sources are directly specified, we won't be able to determine when we
+ are in the last component, so there will always be a trailing, leftmost
+ pipe.
+ """
+ graph = nx.disjoint_union_all(
+ [
+ nx.balanced_tree(r=2, h=1, create_using=nx.DiGraph),
+ nx.balanced_tree(r=1, h=2, create_using=nx.DiGraph),
+ nx.balanced_tree(r=2, h=1, create_using=nx.DiGraph),
+ ]
+ )
+
+ # defined starting point
+ target1 = dedent(
+ """
+ ╟── 0
+ ╎ ├─╼ 1
+ ╎ └─╼ 2
+ ╟── 3
+ ╎ └─╼ 4
+ ╎ └─╼ 5
+ ╟── 6
+ ╎ ├─╼ 7
+ ╎ └─╼ 8
+ """
+ ).strip()
+
+ target2 = dedent(
+ """
+ ╟── 0
+ ╎ ├─╼ 1
+ ╎ └─╼ 2
+ ╟── 3
+ ╎ └─╼ 4
+ ╎ └─╼ 5
+ ╙── 6
+ ├─╼ 7
+ └─╼ 8
+ """
+ ).strip()
+
+ got1 = "\n".join(nx.generate_network_text(graph, sources=graph.nodes))
+ got2 = "\n".join(nx.generate_network_text(graph))
+ assert got1 == target1
+ assert got2 == target2
+
+
+def test_write_network_text_iterative_add_directed_edges():
+ """
+ Walk through the cases going from a disconnected to fully connected graph
+ """
+ graph = nx.DiGraph()
+ graph.add_nodes_from([1, 2, 3, 4])
+ lines = []
+ write = lines.append
+ write("--- initial state ---")
+ nx.write_network_text(graph, path=write, end="")
+ for i, j in product(graph.nodes, graph.nodes):
+ write(f"--- add_edge({i}, {j}) ---")
+ graph.add_edge(i, j)
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ # defined starting point
+ target = dedent(
+ """
+ --- initial state ---
+ ╟── 1
+ ╟── 2
+ ╟── 3
+ ╙── 4
+ --- add_edge(1, 1) ---
+ ╟── 1 ╾ 1
+ ╎ └─╼ ...
+ ╟── 2
+ ╟── 3
+ ╙── 4
+ --- add_edge(1, 2) ---
+ ╟── 1 ╾ 1
+ ╎ ├─╼ 2
+ ╎ └─╼ ...
+ ╟── 3
+ ╙── 4
+ --- add_edge(1, 3) ---
+ ╟── 1 ╾ 1
+ ╎ ├─╼ 2
+ ╎ ├─╼ 3
+ ╎ └─╼ ...
+ ╙── 4
+ --- add_edge(1, 4) ---
+ ╙── 1 ╾ 1
+ ├─╼ 2
+ ├─╼ 3
+ ├─╼ 4
+ └─╼ ...
+ --- add_edge(2, 1) ---
+ ╙── 2 ╾ 1
+ └─╼ 1 ╾ 1
+ ├─╼ 3
+ ├─╼ 4
+ └─╼ ...
+ --- add_edge(2, 2) ---
+ ╙── 1 ╾ 1, 2
+ ├─╼ 2 ╾ 2
+ │ └─╼ ...
+ ├─╼ 3
+ ├─╼ 4
+ └─╼ ...
+ --- add_edge(2, 3) ---
+ ╙── 1 ╾ 1, 2
+ ├─╼ 2 ╾ 2
+ │ ├─╼ 3 ╾ 1
+ │ └─╼ ...
+ ├─╼ 4
+ └─╼ ...
+ --- add_edge(2, 4) ---
+ ╙── 1 ╾ 1, 2
+ ├─╼ 2 ╾ 2
+ │ ├─╼ 3 ╾ 1
+ │ ├─╼ 4 ╾ 1
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(3, 1) ---
+ ╙── 2 ╾ 1, 2
+ ├─╼ 1 ╾ 1, 3
+ │ ├─╼ 3 ╾ 2
+ │ │ └─╼ ...
+ │ ├─╼ 4 ╾ 2
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(3, 2) ---
+ ╙── 3 ╾ 1, 2
+ ├─╼ 1 ╾ 1, 2
+ │ ├─╼ 2 ╾ 2, 3
+ │ │ ├─╼ 4 ╾ 1
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(3, 3) ---
+ ╙── 1 ╾ 1, 2, 3
+ ├─╼ 2 ╾ 2, 3
+ │ ├─╼ 3 ╾ 1, 3
+ │ │ └─╼ ...
+ │ ├─╼ 4 ╾ 1
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(3, 4) ---
+ ╙── 1 ╾ 1, 2, 3
+ ├─╼ 2 ╾ 2, 3
+ │ ├─╼ 3 ╾ 1, 3
+ │ │ ├─╼ 4 ╾ 1, 2
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(4, 1) ---
+ ╙── 2 ╾ 1, 2, 3
+ ├─╼ 1 ╾ 1, 3, 4
+ │ ├─╼ 3 ╾ 2, 3
+ │ │ ├─╼ 4 ╾ 1, 2
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(4, 2) ---
+ ╙── 3 ╾ 1, 2, 3
+ ├─╼ 1 ╾ 1, 2, 4
+ │ ├─╼ 2 ╾ 2, 3, 4
+ │ │ ├─╼ 4 ╾ 1, 3
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(4, 3) ---
+ ╙── 4 ╾ 1, 2, 3
+ ├─╼ 1 ╾ 1, 2, 3
+ │ ├─╼ 2 ╾ 2, 3, 4
+ │ │ ├─╼ 3 ╾ 1, 3, 4
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(4, 4) ---
+ ╙── 1 ╾ 1, 2, 3, 4
+ ├─╼ 2 ╾ 2, 3, 4
+ │ ├─╼ 3 ╾ 1, 3, 4
+ │ │ ├─╼ 4 ╾ 1, 2, 4
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_iterative_add_undirected_edges():
+ """
+ Walk through the cases going from a disconnected to fully connected graph
+ """
+ graph = nx.Graph()
+ graph.add_nodes_from([1, 2, 3, 4])
+ lines = []
+ write = lines.append
+ write("--- initial state ---")
+ nx.write_network_text(graph, path=write, end="")
+ for i, j in product(graph.nodes, graph.nodes):
+ if i == j:
+ continue
+ write(f"--- add_edge({i}, {j}) ---")
+ graph.add_edge(i, j)
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- initial state ---
+ ╟── 1
+ ╟── 2
+ ╟── 3
+ ╙── 4
+ --- add_edge(1, 2) ---
+ ╟── 3
+ ╟── 4
+ ╙── 1
+ └── 2
+ --- add_edge(1, 3) ---
+ ╟── 4
+ ╙── 2
+ └── 1
+ └── 3
+ --- add_edge(1, 4) ---
+ ╙── 2
+ └── 1
+ ├── 3
+ └── 4
+ --- add_edge(2, 1) ---
+ ╙── 2
+ └── 1
+ ├── 3
+ └── 4
+ --- add_edge(2, 3) ---
+ ╙── 4
+ └── 1
+ ├── 2
+ │ └── 3 ─ 1
+ └── ...
+ --- add_edge(2, 4) ---
+ ╙── 3
+ ├── 1
+ │ ├── 2 ─ 3
+ │ │ └── 4 ─ 1
+ │ └── ...
+ └── ...
+ --- add_edge(3, 1) ---
+ ╙── 3
+ ├── 1
+ │ ├── 2 ─ 3
+ │ │ └── 4 ─ 1
+ │ └── ...
+ └── ...
+ --- add_edge(3, 2) ---
+ ╙── 3
+ ├── 1
+ │ ├── 2 ─ 3
+ │ │ └── 4 ─ 1
+ │ └── ...
+ └── ...
+ --- add_edge(3, 4) ---
+ ╙── 1
+ ├── 2
+ │ ├── 3 ─ 1
+ │ │ └── 4 ─ 1, 2
+ │ └── ...
+ └── ...
+ --- add_edge(4, 1) ---
+ ╙── 1
+ ├── 2
+ │ ├── 3 ─ 1
+ │ │ └── 4 ─ 1, 2
+ │ └── ...
+ └── ...
+ --- add_edge(4, 2) ---
+ ╙── 1
+ ├── 2
+ │ ├── 3 ─ 1
+ │ │ └── 4 ─ 1, 2
+ │ └── ...
+ └── ...
+ --- add_edge(4, 3) ---
+ ╙── 1
+ ├── 2
+ │ ├── 3 ─ 1
+ │ │ └── 4 ─ 1, 2
+ │ └── ...
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_iterative_add_random_directed_edges():
+ """
+ Walk through the cases going from a disconnected to fully connected graph
+ """
+
+ rng = random.Random(724466096)
+ graph = nx.DiGraph()
+ graph.add_nodes_from([1, 2, 3, 4, 5])
+ possible_edges = list(product(graph.nodes, graph.nodes))
+ rng.shuffle(possible_edges)
+ graph.add_edges_from(possible_edges[0:8])
+ lines = []
+ write = lines.append
+ write("--- initial state ---")
+ nx.write_network_text(graph, path=write, end="")
+ for i, j in possible_edges[8:12]:
+ write(f"--- add_edge({i}, {j}) ---")
+ graph.add_edge(i, j)
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- initial state ---
+ ╙── 3 ╾ 5
+ └─╼ 2 ╾ 2
+ ├─╼ 4 ╾ 4
+ │ ├─╼ 5
+ │ │ ├─╼ 1 ╾ 1
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(4, 1) ---
+ ╙── 3 ╾ 5
+ └─╼ 2 ╾ 2
+ ├─╼ 4 ╾ 4
+ │ ├─╼ 5
+ │ │ ├─╼ 1 ╾ 1, 4
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(2, 1) ---
+ ╙── 3 ╾ 5
+ └─╼ 2 ╾ 2
+ ├─╼ 4 ╾ 4
+ │ ├─╼ 5
+ │ │ ├─╼ 1 ╾ 1, 4, 2
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(5, 2) ---
+ ╙── 3 ╾ 5
+ └─╼ 2 ╾ 2, 5
+ ├─╼ 4 ╾ 4
+ │ ├─╼ 5
+ │ │ ├─╼ 1 ╾ 1, 4, 2
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- add_edge(1, 5) ---
+ ╙── 3 ╾ 5
+ └─╼ 2 ╾ 2, 5
+ ├─╼ 4 ╾ 4
+ │ ├─╼ 5 ╾ 1
+ │ │ ├─╼ 1 ╾ 1, 4, 2
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_nearly_forest():
+ g = nx.DiGraph()
+ g.add_edge(1, 2)
+ g.add_edge(1, 5)
+ g.add_edge(2, 3)
+ g.add_edge(3, 4)
+ g.add_edge(5, 6)
+ g.add_edge(6, 7)
+ g.add_edge(6, 8)
+ orig = g.copy()
+ g.add_edge(1, 8) # forward edge
+ g.add_edge(4, 2) # back edge
+ g.add_edge(6, 3) # cross edge
+ lines = []
+ write = lines.append
+ write("--- directed case ---")
+ nx.write_network_text(orig, path=write, end="")
+ write("--- add (1, 8), (4, 2), (6, 3) ---")
+ nx.write_network_text(g, path=write, end="")
+ write("--- undirected case ---")
+ nx.write_network_text(orig.to_undirected(), path=write, sources=[1], end="")
+ write("--- add (1, 8), (4, 2), (6, 3) ---")
+ nx.write_network_text(g.to_undirected(), path=write, sources=[1], end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- directed case ---
+ ╙── 1
+ ├─╼ 2
+ │ └─╼ 3
+ │ └─╼ 4
+ └─╼ 5
+ └─╼ 6
+ ├─╼ 7
+ └─╼ 8
+ --- add (1, 8), (4, 2), (6, 3) ---
+ ╙── 1
+ ├─╼ 2 ╾ 4
+ │ └─╼ 3 ╾ 6
+ │ └─╼ 4
+ │ └─╼ ...
+ ├─╼ 5
+ │ └─╼ 6
+ │ ├─╼ 7
+ │ ├─╼ 8 ╾ 1
+ │ └─╼ ...
+ └─╼ ...
+ --- undirected case ---
+ ╙── 1
+ ├── 2
+ │ └── 3
+ │ └── 4
+ └── 5
+ └── 6
+ ├── 7
+ └── 8
+ --- add (1, 8), (4, 2), (6, 3) ---
+ ╙── 1
+ ├── 2
+ │ ├── 3
+ │ │ ├── 4 ─ 2
+ │ │ └── 6
+ │ │ ├── 5 ─ 1
+ │ │ ├── 7
+ │ │ └── 8 ─ 1
+ │ └── ...
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_complete_graph_ascii_only():
+ graph = nx.generators.complete_graph(5, create_using=nx.DiGraph)
+ lines = []
+ write = lines.append
+ write("--- directed case ---")
+ nx.write_network_text(graph, path=write, ascii_only=True, end="")
+ write("--- undirected case ---")
+ nx.write_network_text(graph.to_undirected(), path=write, ascii_only=True, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- directed case ---
+ +-- 0 <- 1, 2, 3, 4
+ |-> 1 <- 2, 3, 4
+ | |-> 2 <- 0, 3, 4
+ | | |-> 3 <- 0, 1, 4
+ | | | |-> 4 <- 0, 1, 2
+ | | | | L-> ...
+ | | | L-> ...
+ | | L-> ...
+ | L-> ...
+ L-> ...
+ --- undirected case ---
+ +-- 0
+ |-- 1
+ | |-- 2 - 0
+ | | |-- 3 - 0, 1
+ | | | L-- 4 - 0, 1, 2
+ | | L-- ...
+ | L-- ...
+ L-- ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_with_labels():
+ graph = nx.generators.complete_graph(5, create_using=nx.DiGraph)
+ for n in graph.nodes:
+ graph.nodes[n]["label"] = f"Node(n={n})"
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, with_labels=True, ascii_only=False, end="")
+ text = "\n".join(lines)
+ # Non trees with labels can get somewhat out of hand with network text
+ # because we need to immediately show every non-tree edge to the right
+ target = dedent(
+ """
+ ╙── Node(n=0) ╾ Node(n=1), Node(n=2), Node(n=3), Node(n=4)
+ ├─╼ Node(n=1) ╾ Node(n=2), Node(n=3), Node(n=4)
+ │ ├─╼ Node(n=2) ╾ Node(n=0), Node(n=3), Node(n=4)
+ │ │ ├─╼ Node(n=3) ╾ Node(n=0), Node(n=1), Node(n=4)
+ │ │ │ ├─╼ Node(n=4) ╾ Node(n=0), Node(n=1), Node(n=2)
+ │ │ │ │ └─╼ ...
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_complete_graphs():
+ lines = []
+ write = lines.append
+ for k in [0, 1, 2, 3, 4, 5]:
+ g = nx.generators.complete_graph(k)
+ write(f"--- undirected k={k} ---")
+ nx.write_network_text(g, path=write, end="")
+
+ for k in [0, 1, 2, 3, 4, 5]:
+ g = nx.generators.complete_graph(k, nx.DiGraph)
+ write(f"--- directed k={k} ---")
+ nx.write_network_text(g, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- undirected k=0 ---
+ ╙
+ --- undirected k=1 ---
+ ╙── 0
+ --- undirected k=2 ---
+ ╙── 0
+ └── 1
+ --- undirected k=3 ---
+ ╙── 0
+ ├── 1
+ │ └── 2 ─ 0
+ └── ...
+ --- undirected k=4 ---
+ ╙── 0
+ ├── 1
+ │ ├── 2 ─ 0
+ │ │ └── 3 ─ 0, 1
+ │ └── ...
+ └── ...
+ --- undirected k=5 ---
+ ╙── 0
+ ├── 1
+ │ ├── 2 ─ 0
+ │ │ ├── 3 ─ 0, 1
+ │ │ │ └── 4 ─ 0, 1, 2
+ │ │ └── ...
+ │ └── ...
+ └── ...
+ --- directed k=0 ---
+ ╙
+ --- directed k=1 ---
+ ╙── 0
+ --- directed k=2 ---
+ ╙── 0 ╾ 1
+ └─╼ 1
+ └─╼ ...
+ --- directed k=3 ---
+ ╙── 0 ╾ 1, 2
+ ├─╼ 1 ╾ 2
+ │ ├─╼ 2 ╾ 0
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- directed k=4 ---
+ ╙── 0 ╾ 1, 2, 3
+ ├─╼ 1 ╾ 2, 3
+ │ ├─╼ 2 ╾ 0, 3
+ │ │ ├─╼ 3 ╾ 0, 1
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- directed k=5 ---
+ ╙── 0 ╾ 1, 2, 3, 4
+ ├─╼ 1 ╾ 2, 3, 4
+ │ ├─╼ 2 ╾ 0, 3, 4
+ │ │ ├─╼ 3 ╾ 0, 1, 4
+ │ │ │ ├─╼ 4 ╾ 0, 1, 2
+ │ │ │ │ └─╼ ...
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_multiple_sources():
+ g = nx.DiGraph()
+ g.add_edge(1, 2)
+ g.add_edge(1, 3)
+ g.add_edge(2, 4)
+ g.add_edge(3, 5)
+ g.add_edge(3, 6)
+ g.add_edge(5, 4)
+ g.add_edge(4, 1)
+ g.add_edge(1, 5)
+ lines = []
+ write = lines.append
+ # Use each node as the starting point to demonstrate how the representation
+ # changes.
+ nodes = sorted(g.nodes())
+ for n in nodes:
+ write(f"--- source node: {n} ---")
+ nx.write_network_text(g, path=write, sources=[n], end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- source node: 1 ---
+ ╙── 1 ╾ 4
+ ├─╼ 2
+ │ └─╼ 4 ╾ 5
+ │ └─╼ ...
+ ├─╼ 3
+ │ ├─╼ 5 ╾ 1
+ │ │ └─╼ ...
+ │ └─╼ 6
+ └─╼ ...
+ --- source node: 2 ---
+ ╙── 2 ╾ 1
+ └─╼ 4 ╾ 5
+ └─╼ 1
+ ├─╼ 3
+ │ ├─╼ 5 ╾ 1
+ │ │ └─╼ ...
+ │ └─╼ 6
+ └─╼ ...
+ --- source node: 3 ---
+ ╙── 3 ╾ 1
+ ├─╼ 5 ╾ 1
+ │ └─╼ 4 ╾ 2
+ │ └─╼ 1
+ │ ├─╼ 2
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ 6
+ --- source node: 4 ---
+ ╙── 4 ╾ 2, 5
+ └─╼ 1
+ ├─╼ 2
+ │ └─╼ ...
+ ├─╼ 3
+ │ ├─╼ 5 ╾ 1
+ │ │ └─╼ ...
+ │ └─╼ 6
+ └─╼ ...
+ --- source node: 5 ---
+ ╙── 5 ╾ 3, 1
+ └─╼ 4 ╾ 2
+ └─╼ 1
+ ├─╼ 2
+ │ └─╼ ...
+ ├─╼ 3
+ │ ├─╼ 6
+ │ └─╼ ...
+ └─╼ ...
+ --- source node: 6 ---
+ ╙── 6 ╾ 3
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_star_graph():
+ graph = nx.star_graph(5, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╙── 1
+ └── 0
+ ├── 2
+ ├── 3
+ ├── 4
+ └── 5
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_path_graph():
+ graph = nx.path_graph(3, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╙── 0
+ └── 1
+ └── 2
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_lollipop_graph():
+ graph = nx.lollipop_graph(4, 2, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╙── 5
+ └── 4
+ └── 3
+ ├── 0
+ │ ├── 1 ─ 3
+ │ │ └── 2 ─ 0, 3
+ │ └── ...
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_wheel_graph():
+ graph = nx.wheel_graph(7, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╙── 1
+ ├── 0
+ │ ├── 2 ─ 1
+ │ │ └── 3 ─ 0
+ │ │ └── 4 ─ 0
+ │ │ └── 5 ─ 0
+ │ │ └── 6 ─ 0, 1
+ │ └── ...
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_circular_ladder_graph():
+ graph = nx.circular_ladder_graph(4, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╙── 0
+ ├── 1
+ │ ├── 2
+ │ │ ├── 3 ─ 0
+ │ │ │ └── 7
+ │ │ │ ├── 6 ─ 2
+ │ │ │ │ └── 5 ─ 1
+ │ │ │ │ └── 4 ─ 0, 7
+ │ │ │ └── ...
+ │ │ └── ...
+ │ └── ...
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_dorogovtsev_goltsev_mendes_graph():
+ graph = nx.dorogovtsev_goltsev_mendes_graph(4, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ ╙── 15
+ ├── 0
+ │ ├── 1 ─ 15
+ │ │ ├── 2 ─ 0
+ │ │ │ ├── 4 ─ 0
+ │ │ │ │ ├── 9 ─ 0
+ │ │ │ │ │ ├── 22 ─ 0
+ │ │ │ │ │ └── 38 ─ 4
+ │ │ │ │ ├── 13 ─ 2
+ │ │ │ │ │ ├── 34 ─ 2
+ │ │ │ │ │ └── 39 ─ 4
+ │ │ │ │ ├── 18 ─ 0
+ │ │ │ │ ├── 30 ─ 2
+ │ │ │ │ └── ...
+ │ │ │ ├── 5 ─ 1
+ │ │ │ │ ├── 12 ─ 1
+ │ │ │ │ │ ├── 29 ─ 1
+ │ │ │ │ │ └── 40 ─ 5
+ │ │ │ │ ├── 14 ─ 2
+ │ │ │ │ │ ├── 35 ─ 2
+ │ │ │ │ │ └── 41 ─ 5
+ │ │ │ │ ├── 25 ─ 1
+ │ │ │ │ ├── 31 ─ 2
+ │ │ │ │ └── ...
+ │ │ │ ├── 7 ─ 0
+ │ │ │ │ ├── 20 ─ 0
+ │ │ │ │ └── 32 ─ 2
+ │ │ │ ├── 10 ─ 1
+ │ │ │ │ ├── 27 ─ 1
+ │ │ │ │ └── 33 ─ 2
+ │ │ │ ├── 16 ─ 0
+ │ │ │ ├── 23 ─ 1
+ │ │ │ └── ...
+ │ │ ├── 3 ─ 0
+ │ │ │ ├── 8 ─ 0
+ │ │ │ │ ├── 21 ─ 0
+ │ │ │ │ └── 36 ─ 3
+ │ │ │ ├── 11 ─ 1
+ │ │ │ │ ├── 28 ─ 1
+ │ │ │ │ └── 37 ─ 3
+ │ │ │ ├── 17 ─ 0
+ │ │ │ ├── 24 ─ 1
+ │ │ │ └── ...
+ │ │ ├── 6 ─ 0
+ │ │ │ ├── 19 ─ 0
+ │ │ │ └── 26 ─ 1
+ │ │ └── ...
+ │ └── ...
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_tree_max_depth():
+ orig = nx.balanced_tree(r=1, h=3, create_using=nx.DiGraph)
+ lines = []
+ write = lines.append
+ write("--- directed case, max_depth=0 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=0)
+ write("--- directed case, max_depth=1 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=1)
+ write("--- directed case, max_depth=2 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=2)
+ write("--- directed case, max_depth=3 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=3)
+ write("--- directed case, max_depth=4 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=4)
+ write("--- undirected case, max_depth=0 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=0)
+ write("--- undirected case, max_depth=1 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=1)
+ write("--- undirected case, max_depth=2 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=2)
+ write("--- undirected case, max_depth=3 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=3)
+ write("--- undirected case, max_depth=4 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=4)
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- directed case, max_depth=0 ---
+ ╙ ...
+ --- directed case, max_depth=1 ---
+ ╙── 0
+ └─╼ ...
+ --- directed case, max_depth=2 ---
+ ╙── 0
+ └─╼ 1
+ └─╼ ...
+ --- directed case, max_depth=3 ---
+ ╙── 0
+ └─╼ 1
+ └─╼ 2
+ └─╼ ...
+ --- directed case, max_depth=4 ---
+ ╙── 0
+ └─╼ 1
+ └─╼ 2
+ └─╼ 3
+ --- undirected case, max_depth=0 ---
+ ╙ ...
+ --- undirected case, max_depth=1 ---
+ ╙── 0 ─ 1
+ └── ...
+ --- undirected case, max_depth=2 ---
+ ╙── 0
+ └── 1 ─ 2
+ └── ...
+ --- undirected case, max_depth=3 ---
+ ╙── 0
+ └── 1
+ └── 2 ─ 3
+ └── ...
+ --- undirected case, max_depth=4 ---
+ ╙── 0
+ └── 1
+ └── 2
+ └── 3
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_graph_max_depth():
+ orig = nx.erdos_renyi_graph(10, 0.15, directed=True, seed=40392)
+ lines = []
+ write = lines.append
+ write("--- directed case, max_depth=None ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=None)
+ write("--- directed case, max_depth=0 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=0)
+ write("--- directed case, max_depth=1 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=1)
+ write("--- directed case, max_depth=2 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=2)
+ write("--- directed case, max_depth=3 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=3)
+ write("--- undirected case, max_depth=None ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=None)
+ write("--- undirected case, max_depth=0 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=0)
+ write("--- undirected case, max_depth=1 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=1)
+ write("--- undirected case, max_depth=2 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=2)
+ write("--- undirected case, max_depth=3 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=3)
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- directed case, max_depth=None ---
+ ╟── 4
+ ╎ ├─╼ 0 ╾ 3
+ ╎ ├─╼ 5 ╾ 7
+ ╎ │ └─╼ 3
+ ╎ │ ├─╼ 1 ╾ 9
+ ╎ │ │ └─╼ 9 ╾ 6
+ ╎ │ │ ├─╼ 6
+ ╎ │ │ │ └─╼ ...
+ ╎ │ │ ├─╼ 7 ╾ 4
+ ╎ │ │ │ ├─╼ 2
+ ╎ │ │ │ └─╼ ...
+ ╎ │ │ └─╼ ...
+ ╎ │ └─╼ ...
+ ╎ └─╼ ...
+ ╙── 8
+ --- directed case, max_depth=0 ---
+ ╙ ...
+ --- directed case, max_depth=1 ---
+ ╟── 4
+ ╎ └─╼ ...
+ ╙── 8
+ --- directed case, max_depth=2 ---
+ ╟── 4
+ ╎ ├─╼ 0 ╾ 3
+ ╎ ├─╼ 5 ╾ 7
+ ╎ │ └─╼ ...
+ ╎ └─╼ 7 ╾ 9
+ ╎ └─╼ ...
+ ╙── 8
+ --- directed case, max_depth=3 ---
+ ╟── 4
+ ╎ ├─╼ 0 ╾ 3
+ ╎ ├─╼ 5 ╾ 7
+ ╎ │ └─╼ 3
+ ╎ │ └─╼ ...
+ ╎ └─╼ 7 ╾ 9
+ ╎ ├─╼ 2
+ ╎ └─╼ ...
+ ╙── 8
+ --- undirected case, max_depth=None ---
+ ╟── 8
+ ╙── 2
+ └── 7
+ ├── 4
+ │ ├── 0
+ │ │ └── 3
+ │ │ ├── 1
+ │ │ │ └── 9 ─ 7
+ │ │ │ └── 6
+ │ │ └── 5 ─ 4, 7
+ │ └── ...
+ └── ...
+ --- undirected case, max_depth=0 ---
+ ╙ ...
+ --- undirected case, max_depth=1 ---
+ ╟── 8
+ ╙── 2 ─ 7
+ └── ...
+ --- undirected case, max_depth=2 ---
+ ╟── 8
+ ╙── 2
+ └── 7 ─ 4, 5, 9
+ └── ...
+ --- undirected case, max_depth=3 ---
+ ╟── 8
+ ╙── 2
+ └── 7
+ ├── 4 ─ 0, 5
+ │ └── ...
+ ├── 5 ─ 4, 3
+ │ └── ...
+ └── 9 ─ 1, 6
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_clique_max_depth():
+ orig = nx.complete_graph(5, nx.DiGraph)
+ lines = []
+ write = lines.append
+ write("--- directed case, max_depth=None ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=None)
+ write("--- directed case, max_depth=0 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=0)
+ write("--- directed case, max_depth=1 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=1)
+ write("--- directed case, max_depth=2 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=2)
+ write("--- directed case, max_depth=3 ---")
+ nx.write_network_text(orig, path=write, end="", max_depth=3)
+ write("--- undirected case, max_depth=None ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=None)
+ write("--- undirected case, max_depth=0 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=0)
+ write("--- undirected case, max_depth=1 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=1)
+ write("--- undirected case, max_depth=2 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=2)
+ write("--- undirected case, max_depth=3 ---")
+ nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=3)
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- directed case, max_depth=None ---
+ ╙── 0 ╾ 1, 2, 3, 4
+ ├─╼ 1 ╾ 2, 3, 4
+ │ ├─╼ 2 ╾ 0, 3, 4
+ │ │ ├─╼ 3 ╾ 0, 1, 4
+ │ │ │ ├─╼ 4 ╾ 0, 1, 2
+ │ │ │ │ └─╼ ...
+ │ │ │ └─╼ ...
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- directed case, max_depth=0 ---
+ ╙ ...
+ --- directed case, max_depth=1 ---
+ ╙── 0 ╾ 1, 2, 3, 4
+ └─╼ ...
+ --- directed case, max_depth=2 ---
+ ╙── 0 ╾ 1, 2, 3, 4
+ ├─╼ 1 ╾ 2, 3, 4
+ │ └─╼ ...
+ ├─╼ 2 ╾ 1, 3, 4
+ │ └─╼ ...
+ ├─╼ 3 ╾ 1, 2, 4
+ │ └─╼ ...
+ └─╼ 4 ╾ 1, 2, 3
+ └─╼ ...
+ --- directed case, max_depth=3 ---
+ ╙── 0 ╾ 1, 2, 3, 4
+ ├─╼ 1 ╾ 2, 3, 4
+ │ ├─╼ 2 ╾ 0, 3, 4
+ │ │ └─╼ ...
+ │ ├─╼ 3 ╾ 0, 2, 4
+ │ │ └─╼ ...
+ │ ├─╼ 4 ╾ 0, 2, 3
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ --- undirected case, max_depth=None ---
+ ╙── 0
+ ├── 1
+ │ ├── 2 ─ 0
+ │ │ ├── 3 ─ 0, 1
+ │ │ │ └── 4 ─ 0, 1, 2
+ │ │ └── ...
+ │ └── ...
+ └── ...
+ --- undirected case, max_depth=0 ---
+ ╙ ...
+ --- undirected case, max_depth=1 ---
+ ╙── 0 ─ 1, 2, 3, 4
+ └── ...
+ --- undirected case, max_depth=2 ---
+ ╙── 0
+ ├── 1 ─ 2, 3, 4
+ │ └── ...
+ ├── 2 ─ 1, 3, 4
+ │ └── ...
+ ├── 3 ─ 1, 2, 4
+ │ └── ...
+ └── 4 ─ 1, 2, 3
+ --- undirected case, max_depth=3 ---
+ ╙── 0
+ ├── 1
+ │ ├── 2 ─ 0, 3, 4
+ │ │ └── ...
+ │ ├── 3 ─ 0, 2, 4
+ │ │ └── ...
+ │ └── 4 ─ 0, 2, 3
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_custom_label():
+ # Create a directed forest with labels
+ graph = nx.erdos_renyi_graph(5, 0.4, directed=True, seed=359222358)
+ for node in graph.nodes:
+ graph.nodes[node]["label"] = f"Node({node})"
+ graph.nodes[node]["chr"] = chr(node + ord("a") - 1)
+ if node % 2 == 0:
+ graph.nodes[node]["part"] = chr(node + ord("a"))
+
+ lines = []
+ write = lines.append
+ write("--- when with_labels=True, uses the 'label' attr ---")
+ nx.write_network_text(graph, path=write, with_labels=True, end="", max_depth=None)
+ write("--- when with_labels=False, uses str(node) value ---")
+ nx.write_network_text(graph, path=write, with_labels=False, end="", max_depth=None)
+ write("--- when with_labels is a string, use that attr ---")
+ nx.write_network_text(graph, path=write, with_labels="chr", end="", max_depth=None)
+ write("--- fallback to str(node) when the attr does not exist ---")
+ nx.write_network_text(graph, path=write, with_labels="part", end="", max_depth=None)
+
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- when with_labels=True, uses the 'label' attr ---
+ ╙── Node(1)
+ └─╼ Node(3) ╾ Node(2)
+ ├─╼ Node(0)
+ │ ├─╼ Node(2) ╾ Node(3), Node(4)
+ │ │ └─╼ ...
+ │ └─╼ Node(4)
+ │ └─╼ ...
+ └─╼ ...
+ --- when with_labels=False, uses str(node) value ---
+ ╙── 1
+ └─╼ 3 ╾ 2
+ ├─╼ 0
+ │ ├─╼ 2 ╾ 3, 4
+ │ │ └─╼ ...
+ │ └─╼ 4
+ │ └─╼ ...
+ └─╼ ...
+ --- when with_labels is a string, use that attr ---
+ ╙── a
+ └─╼ c ╾ b
+ ├─╼ `
+ │ ├─╼ b ╾ c, d
+ │ │ └─╼ ...
+ │ └─╼ d
+ │ └─╼ ...
+ └─╼ ...
+ --- fallback to str(node) when the attr does not exist ---
+ ╙── 1
+ └─╼ 3 ╾ c
+ ├─╼ a
+ │ ├─╼ c ╾ 3, e
+ │ │ └─╼ ...
+ │ └─╼ e
+ │ └─╼ ...
+ └─╼ ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_write_network_text_vertical_chains():
+ graph1 = nx.lollipop_graph(4, 2, create_using=nx.Graph)
+ graph1.add_edge(0, -1)
+ graph1.add_edge(-1, -2)
+ graph1.add_edge(-2, -3)
+
+ graph2 = graph1.to_directed()
+ graph2.remove_edges_from([(u, v) for u, v in graph2.edges if v > u])
+
+ lines = []
+ write = lines.append
+ write("--- Undirected UTF ---")
+ nx.write_network_text(graph1, path=write, end="", vertical_chains=True)
+ write("--- Undirected ASCI ---")
+ nx.write_network_text(
+ graph1, path=write, end="", vertical_chains=True, ascii_only=True
+ )
+ write("--- Directed UTF ---")
+ nx.write_network_text(graph2, path=write, end="", vertical_chains=True)
+ write("--- Directed ASCI ---")
+ nx.write_network_text(
+ graph2, path=write, end="", vertical_chains=True, ascii_only=True
+ )
+
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- Undirected UTF ---
+ ╙── 5
+ │
+ 4
+ │
+ 3
+ ├── 0
+ │ ├── 1 ─ 3
+ │ │ │
+ │ │ 2 ─ 0, 3
+ │ ├── -1
+ │ │ │
+ │ │ -2
+ │ │ │
+ │ │ -3
+ │ └── ...
+ └── ...
+ --- Undirected ASCI ---
+ +-- 5
+ |
+ 4
+ |
+ 3
+ |-- 0
+ | |-- 1 - 3
+ | | |
+ | | 2 - 0, 3
+ | |-- -1
+ | | |
+ | | -2
+ | | |
+ | | -3
+ | L-- ...
+ L-- ...
+ --- Directed UTF ---
+ ╙── 5
+ ╽
+ 4
+ ╽
+ 3
+ ├─╼ 0 ╾ 1, 2
+ │ ╽
+ │ -1
+ │ ╽
+ │ -2
+ │ ╽
+ │ -3
+ ├─╼ 1 ╾ 2
+ │ └─╼ ...
+ └─╼ 2
+ └─╼ ...
+ --- Directed ASCI ---
+ +-- 5
+ !
+ 4
+ !
+ 3
+ |-> 0 <- 1, 2
+ | !
+ | -1
+ | !
+ | -2
+ | !
+ | -3
+ |-> 1 <- 2
+ | L-> ...
+ L-> 2
+ L-> ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_collapse_directed():
+ graph = nx.balanced_tree(r=2, h=3, create_using=nx.DiGraph)
+ lines = []
+ write = lines.append
+ write("--- Original ---")
+ nx.write_network_text(graph, path=write, end="")
+ graph.nodes[1]["collapse"] = True
+ write("--- Collapse Node 1 ---")
+ nx.write_network_text(graph, path=write, end="")
+ write("--- Add alternate path (5, 3) to collapsed zone")
+ graph.add_edge(5, 3)
+ nx.write_network_text(graph, path=write, end="")
+ write("--- Collapse Node 0 ---")
+ graph.nodes[0]["collapse"] = True
+ nx.write_network_text(graph, path=write, end="")
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- Original ---
+ ╙── 0
+ ├─╼ 1
+ │ ├─╼ 3
+ │ │ ├─╼ 7
+ │ │ └─╼ 8
+ │ └─╼ 4
+ │ ├─╼ 9
+ │ └─╼ 10
+ └─╼ 2
+ ├─╼ 5
+ │ ├─╼ 11
+ │ └─╼ 12
+ └─╼ 6
+ ├─╼ 13
+ └─╼ 14
+ --- Collapse Node 1 ---
+ ╙── 0
+ ├─╼ 1
+ │ └─╼ ...
+ └─╼ 2
+ ├─╼ 5
+ │ ├─╼ 11
+ │ └─╼ 12
+ └─╼ 6
+ ├─╼ 13
+ └─╼ 14
+ --- Add alternate path (5, 3) to collapsed zone
+ ╙── 0
+ ├─╼ 1
+ │ └─╼ ...
+ └─╼ 2
+ ├─╼ 5
+ │ ├─╼ 11
+ │ ├─╼ 12
+ │ └─╼ 3 ╾ 1
+ │ ├─╼ 7
+ │ └─╼ 8
+ └─╼ 6
+ ├─╼ 13
+ └─╼ 14
+ --- Collapse Node 0 ---
+ ╙── 0
+ └─╼ ...
+ """
+ ).strip()
+ assert target == text
+
+
+def test_collapse_undirected():
+ graph = nx.balanced_tree(r=2, h=3, create_using=nx.Graph)
+ lines = []
+ write = lines.append
+ write("--- Original ---")
+ nx.write_network_text(graph, path=write, end="", sources=[0])
+ graph.nodes[1]["collapse"] = True
+ write("--- Collapse Node 1 ---")
+ nx.write_network_text(graph, path=write, end="", sources=[0])
+ write("--- Add alternate path (5, 3) to collapsed zone")
+ graph.add_edge(5, 3)
+ nx.write_network_text(graph, path=write, end="", sources=[0])
+ write("--- Collapse Node 0 ---")
+ graph.nodes[0]["collapse"] = True
+ nx.write_network_text(graph, path=write, end="", sources=[0])
+ text = "\n".join(lines)
+ target = dedent(
+ """
+ --- Original ---
+ ╙── 0
+ ├── 1
+ │ ├── 3
+ │ │ ├── 7
+ │ │ └── 8
+ │ └── 4
+ │ ├── 9
+ │ └── 10
+ └── 2
+ ├── 5
+ │ ├── 11
+ │ └── 12
+ └── 6
+ ├── 13
+ └── 14
+ --- Collapse Node 1 ---
+ ╙── 0
+ ├── 1 ─ 3, 4
+ │ └── ...
+ └── 2
+ ├── 5
+ │ ├── 11
+ │ └── 12
+ └── 6
+ ├── 13
+ └── 14
+ --- Add alternate path (5, 3) to collapsed zone
+ ╙── 0
+ ├── 1 ─ 3, 4
+ │ └── ...
+ └── 2
+ ├── 5
+ │ ├── 11
+ │ ├── 12
+ │ └── 3 ─ 1
+ │ ├── 7
+ │ └── 8
+ └── 6
+ ├── 13
+ └── 14
+ --- Collapse Node 0 ---
+ ╙── 0 ─ 1, 2
+ └── ...
+ """
+ ).strip()
+ assert target == text
+
+
+def generate_test_graphs():
+ """
+ Generate a gauntlet of different test graphs with different properties
+ """
+ import random
+
+ rng = random.Random(976689776)
+ num_randomized = 3
+
+ for directed in [0, 1]:
+ cls = nx.DiGraph if directed else nx.Graph
+
+ for num_nodes in range(17):
+ # Disconnected graph
+ graph = cls()
+ graph.add_nodes_from(range(num_nodes))
+ yield graph
+
+ # Randomize graphs
+ if num_nodes > 0:
+ for p in [0.1, 0.3, 0.5, 0.7, 0.9]:
+ for seed in range(num_randomized):
+ graph = nx.erdos_renyi_graph(
+ num_nodes, p, directed=directed, seed=rng
+ )
+ yield graph
+
+ yield nx.complete_graph(num_nodes, cls)
+
+ yield nx.path_graph(3, create_using=cls)
+ yield nx.balanced_tree(r=1, h=3, create_using=cls)
+ if not directed:
+ yield nx.circular_ladder_graph(4, create_using=cls)
+ yield nx.star_graph(5, create_using=cls)
+ yield nx.lollipop_graph(4, 2, create_using=cls)
+ yield nx.wheel_graph(7, create_using=cls)
+ yield nx.dorogovtsev_goltsev_mendes_graph(4, create_using=cls)
+
+
+@pytest.mark.parametrize(
+ ("vertical_chains", "ascii_only"),
+ tuple(
+ [
+ (vertical_chains, ascii_only)
+ for vertical_chains in [0, 1]
+ for ascii_only in [0, 1]
+ ]
+ ),
+)
+def test_network_text_round_trip(vertical_chains, ascii_only):
+ """
+ Write the graph to network text format, then parse it back in, assert it is
+ the same as the original graph. Passing this test is strong validation of
+ both the format generator and parser.
+ """
+ from networkx.readwrite.text import _parse_network_text
+
+ for graph in generate_test_graphs():
+ graph = nx.relabel_nodes(graph, {n: str(n) for n in graph.nodes})
+ lines = list(
+ nx.generate_network_text(
+ graph, vertical_chains=vertical_chains, ascii_only=ascii_only
+ )
+ )
+ new = _parse_network_text(lines)
+ try:
+ assert new.nodes == graph.nodes
+ assert new.edges == graph.edges
+ except Exception:
+ nx.write_network_text(graph)
+ raise
diff --git a/.venv/lib/python3.12/site-packages/networkx/readwrite/text.py b/.venv/lib/python3.12/site-packages/networkx/readwrite/text.py
new file mode 100644
index 00000000..c54901d1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/readwrite/text.py
@@ -0,0 +1,852 @@
+"""
+Text-based visual representations of graphs
+"""
+
+import sys
+import warnings
+from collections import defaultdict
+
+import networkx as nx
+from networkx.utils import open_file
+
+__all__ = ["generate_network_text", "write_network_text"]
+
+
+class BaseGlyphs:
+ @classmethod
+ def as_dict(cls):
+ return {
+ a: getattr(cls, a)
+ for a in dir(cls)
+ if not a.startswith("_") and a != "as_dict"
+ }
+
+
+class AsciiBaseGlyphs(BaseGlyphs):
+ empty: str = "+"
+ newtree_last: str = "+-- "
+ newtree_mid: str = "+-- "
+ endof_forest: str = " "
+ within_forest: str = ": "
+ within_tree: str = "| "
+
+
+class AsciiDirectedGlyphs(AsciiBaseGlyphs):
+ last: str = "L-> "
+ mid: str = "|-> "
+ backedge: str = "<-"
+ vertical_edge: str = "!"
+
+
+class AsciiUndirectedGlyphs(AsciiBaseGlyphs):
+ last: str = "L-- "
+ mid: str = "|-- "
+ backedge: str = "-"
+ vertical_edge: str = "|"
+
+
+class UtfBaseGlyphs(BaseGlyphs):
+ # Notes on available box and arrow characters
+ # https://en.wikipedia.org/wiki/Box-drawing_character
+ # https://stackoverflow.com/questions/2701192/triangle-arrow
+ empty: str = "╙"
+ newtree_last: str = "╙── "
+ newtree_mid: str = "╟── "
+ endof_forest: str = " "
+ within_forest: str = "╎ "
+ within_tree: str = "│ "
+
+
+class UtfDirectedGlyphs(UtfBaseGlyphs):
+ last: str = "└─╼ "
+ mid: str = "├─╼ "
+ backedge: str = "╾"
+ vertical_edge: str = "╽"
+
+
+class UtfUndirectedGlyphs(UtfBaseGlyphs):
+ last: str = "└── "
+ mid: str = "├── "
+ backedge: str = "─"
+ vertical_edge: str = "│"
+
+
+def generate_network_text(
+ graph,
+ with_labels=True,
+ sources=None,
+ max_depth=None,
+ ascii_only=False,
+ vertical_chains=False,
+):
+ """Generate lines in the "network text" format
+
+ This works via a depth-first traversal of the graph and writing a line for
+ each unique node encountered. Non-tree edges are written to the right of
+ each node, and connection to a non-tree edge is indicated with an ellipsis.
+ This representation works best when the input graph is a forest, but any
+ graph can be represented.
+
+ This notation is original to networkx, although it is simple enough that it
+ may be known in existing literature. See #5602 for details. The procedure
+ is summarized as follows:
+
+ 1. Given a set of source nodes (which can be specified, or automatically
+ discovered via finding the (strongly) connected components and choosing one
+ node with minimum degree from each), we traverse the graph in depth first
+ order.
+
+ 2. Each reachable node will be printed exactly once on it's own line.
+
+ 3. Edges are indicated in one of four ways:
+
+ a. a parent "L-style" connection on the upper left. This corresponds to
+ a traversal in the directed DFS tree.
+
+ b. a backref "<-style" connection shown directly on the right. For
+ directed graphs, these are drawn for any incoming edges to a node that
+ is not a parent edge. For undirected graphs, these are drawn for only
+ the non-parent edges that have already been represented (The edges that
+ have not been represented will be handled in the recursive case).
+
+ c. a child "L-style" connection on the lower right. Drawing of the
+ children are handled recursively.
+
+ d. if ``vertical_chains`` is true, and a parent node only has one child
+ a "vertical-style" edge is drawn between them.
+
+ 4. The children of each node (wrt the directed DFS tree) are drawn
+ underneath and to the right of it. In the case that a child node has already
+ been drawn the connection is replaced with an ellipsis ("...") to indicate
+ that there is one or more connections represented elsewhere.
+
+ 5. If a maximum depth is specified, an edge to nodes past this maximum
+ depth will be represented by an ellipsis.
+
+ 6. If a node has a truthy "collapse" value, then we do not traverse past
+ that node.
+
+ Parameters
+ ----------
+ graph : nx.DiGraph | nx.Graph
+ Graph to represent
+
+ with_labels : bool | str
+ If True will use the "label" attribute of a node to display if it
+ exists otherwise it will use the node value itself. If given as a
+ string, then that attribute name will be used instead of "label".
+ Defaults to True.
+
+ sources : List
+ Specifies which nodes to start traversal from. Note: nodes that are not
+ reachable from one of these sources may not be shown. If unspecified,
+ the minimal set of nodes needed to reach all others will be used.
+
+ max_depth : int | None
+ The maximum depth to traverse before stopping. Defaults to None.
+
+ ascii_only : Boolean
+ If True only ASCII characters are used to construct the visualization
+
+ vertical_chains : Boolean
+ If True, chains of nodes will be drawn vertically when possible.
+
+ Yields
+ ------
+ str : a line of generated text
+
+ Examples
+ --------
+ >>> graph = nx.path_graph(10)
+ >>> graph.add_node("A")
+ >>> graph.add_node("B")
+ >>> graph.add_node("C")
+ >>> graph.add_node("D")
+ >>> graph.add_edge(9, "A")
+ >>> graph.add_edge(9, "B")
+ >>> graph.add_edge(9, "C")
+ >>> graph.add_edge("C", "D")
+ >>> graph.add_edge("C", "E")
+ >>> graph.add_edge("C", "F")
+ >>> nx.write_network_text(graph)
+ ╙── 0
+ └── 1
+ └── 2
+ └── 3
+ └── 4
+ └── 5
+ └── 6
+ └── 7
+ └── 8
+ └── 9
+ ├── A
+ ├── B
+ └── C
+ ├── D
+ ├── E
+ └── F
+ >>> nx.write_network_text(graph, vertical_chains=True)
+ ╙── 0
+ │
+ 1
+ │
+ 2
+ │
+ 3
+ │
+ 4
+ │
+ 5
+ │
+ 6
+ │
+ 7
+ │
+ 8
+ │
+ 9
+ ├── A
+ ├── B
+ └── C
+ ├── D
+ ├── E
+ └── F
+ """
+ from typing import Any, NamedTuple
+
+ class StackFrame(NamedTuple):
+ parent: Any
+ node: Any
+ indents: list
+ this_islast: bool
+ this_vertical: bool
+
+ collapse_attr = "collapse"
+
+ is_directed = graph.is_directed()
+
+ if is_directed:
+ glyphs = AsciiDirectedGlyphs if ascii_only else UtfDirectedGlyphs
+ succ = graph.succ
+ pred = graph.pred
+ else:
+ glyphs = AsciiUndirectedGlyphs if ascii_only else UtfUndirectedGlyphs
+ succ = graph.adj
+ pred = graph.adj
+
+ if isinstance(with_labels, str):
+ label_attr = with_labels
+ elif with_labels:
+ label_attr = "label"
+ else:
+ label_attr = None
+
+ if max_depth == 0:
+ yield glyphs.empty + " ..."
+ elif len(graph.nodes) == 0:
+ yield glyphs.empty
+ else:
+ # If the nodes to traverse are unspecified, find the minimal set of
+ # nodes that will reach the entire graph
+ if sources is None:
+ sources = _find_sources(graph)
+
+ # Populate the stack with each:
+ # 1. parent node in the DFS tree (or None for root nodes),
+ # 2. the current node in the DFS tree
+ # 2. a list of indentations indicating depth
+ # 3. a flag indicating if the node is the final one to be written.
+ # Reverse the stack so sources are popped in the correct order.
+ last_idx = len(sources) - 1
+ stack = [
+ StackFrame(None, node, [], (idx == last_idx), False)
+ for idx, node in enumerate(sources)
+ ][::-1]
+
+ num_skipped_children = defaultdict(lambda: 0)
+ seen_nodes = set()
+ while stack:
+ parent, node, indents, this_islast, this_vertical = stack.pop()
+
+ if node is not Ellipsis:
+ skip = node in seen_nodes
+ if skip:
+ # Mark that we skipped a parent's child
+ num_skipped_children[parent] += 1
+
+ if this_islast:
+ # If we reached the last child of a parent, and we skipped
+ # any of that parents children, then we should emit an
+ # ellipsis at the end after this.
+ if num_skipped_children[parent] and parent is not None:
+ # Append the ellipsis to be emitted last
+ next_islast = True
+ try_frame = StackFrame(
+ node, Ellipsis, indents, next_islast, False
+ )
+ stack.append(try_frame)
+
+ # Redo this frame, but not as a last object
+ next_islast = False
+ try_frame = StackFrame(
+ parent, node, indents, next_islast, this_vertical
+ )
+ stack.append(try_frame)
+ continue
+
+ if skip:
+ continue
+ seen_nodes.add(node)
+
+ if not indents:
+ # Top level items (i.e. trees in the forest) get different
+ # glyphs to indicate they are not actually connected
+ if this_islast:
+ this_vertical = False
+ this_prefix = indents + [glyphs.newtree_last]
+ next_prefix = indents + [glyphs.endof_forest]
+ else:
+ this_prefix = indents + [glyphs.newtree_mid]
+ next_prefix = indents + [glyphs.within_forest]
+
+ else:
+ # Non-top-level items
+ if this_vertical:
+ this_prefix = indents
+ next_prefix = indents
+ else:
+ if this_islast:
+ this_prefix = indents + [glyphs.last]
+ next_prefix = indents + [glyphs.endof_forest]
+ else:
+ this_prefix = indents + [glyphs.mid]
+ next_prefix = indents + [glyphs.within_tree]
+
+ if node is Ellipsis:
+ label = " ..."
+ suffix = ""
+ children = []
+ else:
+ if label_attr is not None:
+ label = str(graph.nodes[node].get(label_attr, node))
+ else:
+ label = str(node)
+
+ # Determine if we want to show the children of this node.
+ if collapse_attr is not None:
+ collapse = graph.nodes[node].get(collapse_attr, False)
+ else:
+ collapse = False
+
+ # Determine:
+ # (1) children to traverse into after showing this node.
+ # (2) parents to immediately show to the right of this node.
+ if is_directed:
+ # In the directed case we must show every successor node
+ # note: it may be skipped later, but we don't have that
+ # information here.
+ children = list(succ[node])
+ # In the directed case we must show every predecessor
+ # except for parent we directly traversed from.
+ handled_parents = {parent}
+ else:
+ # Showing only the unseen children results in a more
+ # concise representation for the undirected case.
+ children = [
+ child for child in succ[node] if child not in seen_nodes
+ ]
+
+ # In the undirected case, parents are also children, so we
+ # only need to immediately show the ones we can no longer
+ # traverse
+ handled_parents = {*children, parent}
+
+ if max_depth is not None and len(indents) == max_depth - 1:
+ # Use ellipsis to indicate we have reached maximum depth
+ if children:
+ children = [Ellipsis]
+ handled_parents = {parent}
+
+ if collapse:
+ # Collapsing a node is the same as reaching maximum depth
+ if children:
+ children = [Ellipsis]
+ handled_parents = {parent}
+
+ # The other parents are other predecessors of this node that
+ # are not handled elsewhere.
+ other_parents = [p for p in pred[node] if p not in handled_parents]
+ if other_parents:
+ if label_attr is not None:
+ other_parents_labels = ", ".join(
+ [
+ str(graph.nodes[p].get(label_attr, p))
+ for p in other_parents
+ ]
+ )
+ else:
+ other_parents_labels = ", ".join(
+ [str(p) for p in other_parents]
+ )
+ suffix = " ".join(["", glyphs.backedge, other_parents_labels])
+ else:
+ suffix = ""
+
+ # Emit the line for this node, this will be called for each node
+ # exactly once.
+ if this_vertical:
+ yield "".join(this_prefix + [glyphs.vertical_edge])
+
+ yield "".join(this_prefix + [label, suffix])
+
+ if vertical_chains:
+ if is_directed:
+ num_children = len(set(children))
+ else:
+ num_children = len(set(children) - {parent})
+ # The next node can be drawn vertically if it is the only
+ # remaining child of this node.
+ next_is_vertical = num_children == 1
+ else:
+ next_is_vertical = False
+
+ # Push children on the stack in reverse order so they are popped in
+ # the original order.
+ for idx, child in enumerate(children[::-1]):
+ next_islast = idx == 0
+ try_frame = StackFrame(
+ node, child, next_prefix, next_islast, next_is_vertical
+ )
+ stack.append(try_frame)
+
+
+@open_file(1, "w")
+def write_network_text(
+ graph,
+ path=None,
+ with_labels=True,
+ sources=None,
+ max_depth=None,
+ ascii_only=False,
+ end="\n",
+ vertical_chains=False,
+):
+ """Creates a nice text representation of a graph
+
+ This works via a depth-first traversal of the graph and writing a line for
+ each unique node encountered. Non-tree edges are written to the right of
+ each node, and connection to a non-tree edge is indicated with an ellipsis.
+ This representation works best when the input graph is a forest, but any
+ graph can be represented.
+
+ Parameters
+ ----------
+ graph : nx.DiGraph | nx.Graph
+ Graph to represent
+
+ path : string or file or callable or None
+ Filename or file handle for data output.
+ if a function, then it will be called for each generated line.
+ if None, this will default to "sys.stdout.write"
+
+ with_labels : bool | str
+ If True will use the "label" attribute of a node to display if it
+ exists otherwise it will use the node value itself. If given as a
+ string, then that attribute name will be used instead of "label".
+ Defaults to True.
+
+ sources : List
+ Specifies which nodes to start traversal from. Note: nodes that are not
+ reachable from one of these sources may not be shown. If unspecified,
+ the minimal set of nodes needed to reach all others will be used.
+
+ max_depth : int | None
+ The maximum depth to traverse before stopping. Defaults to None.
+
+ ascii_only : Boolean
+ If True only ASCII characters are used to construct the visualization
+
+ end : string
+ The line ending character
+
+ vertical_chains : Boolean
+ If True, chains of nodes will be drawn vertically when possible.
+
+ Examples
+ --------
+ >>> graph = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph)
+ >>> nx.write_network_text(graph)
+ ╙── 0
+ ├─╼ 1
+ │ ├─╼ 3
+ │ └─╼ 4
+ └─╼ 2
+ ├─╼ 5
+ └─╼ 6
+
+ >>> # A near tree with one non-tree edge
+ >>> graph.add_edge(5, 1)
+ >>> nx.write_network_text(graph)
+ ╙── 0
+ ├─╼ 1 ╾ 5
+ │ ├─╼ 3
+ │ └─╼ 4
+ └─╼ 2
+ ├─╼ 5
+ │ └─╼ ...
+ └─╼ 6
+
+ >>> graph = nx.cycle_graph(5)
+ >>> nx.write_network_text(graph)
+ ╙── 0
+ ├── 1
+ │ └── 2
+ │ └── 3
+ │ └── 4 ─ 0
+ └── ...
+
+ >>> graph = nx.cycle_graph(5, nx.DiGraph)
+ >>> nx.write_network_text(graph, vertical_chains=True)
+ ╙── 0 ╾ 4
+ ╽
+ 1
+ ╽
+ 2
+ ╽
+ 3
+ ╽
+ 4
+ └─╼ ...
+
+ >>> nx.write_network_text(graph, vertical_chains=True, ascii_only=True)
+ +-- 0 <- 4
+ !
+ 1
+ !
+ 2
+ !
+ 3
+ !
+ 4
+ L-> ...
+
+ >>> graph = nx.generators.barbell_graph(4, 2)
+ >>> nx.write_network_text(graph, vertical_chains=False)
+ ╙── 4
+ ├── 5
+ │ └── 6
+ │ ├── 7
+ │ │ ├── 8 ─ 6
+ │ │ │ └── 9 ─ 6, 7
+ │ │ └── ...
+ │ └── ...
+ └── 3
+ ├── 0
+ │ ├── 1 ─ 3
+ │ │ └── 2 ─ 0, 3
+ │ └── ...
+ └── ...
+ >>> nx.write_network_text(graph, vertical_chains=True)
+ ╙── 4
+ ├── 5
+ │ │
+ │ 6
+ │ ├── 7
+ │ │ ├── 8 ─ 6
+ │ │ │ │
+ │ │ │ 9 ─ 6, 7
+ │ │ └── ...
+ │ └── ...
+ └── 3
+ ├── 0
+ │ ├── 1 ─ 3
+ │ │ │
+ │ │ 2 ─ 0, 3
+ │ └── ...
+ └── ...
+
+ >>> graph = nx.complete_graph(5, create_using=nx.Graph)
+ >>> nx.write_network_text(graph)
+ ╙── 0
+ ├── 1
+ │ ├── 2 ─ 0
+ │ │ ├── 3 ─ 0, 1
+ │ │ │ └── 4 ─ 0, 1, 2
+ │ │ └── ...
+ │ └── ...
+ └── ...
+
+ >>> graph = nx.complete_graph(3, create_using=nx.DiGraph)
+ >>> nx.write_network_text(graph)
+ ╙── 0 ╾ 1, 2
+ ├─╼ 1 ╾ 2
+ │ ├─╼ 2 ╾ 0
+ │ │ └─╼ ...
+ │ └─╼ ...
+ └─╼ ...
+ """
+ if path is None:
+ # The path is unspecified, write to stdout
+ _write = sys.stdout.write
+ elif hasattr(path, "write"):
+ # The path is already an open file
+ _write = path.write
+ elif callable(path):
+ # The path is a custom callable
+ _write = path
+ else:
+ raise TypeError(type(path))
+
+ for line in generate_network_text(
+ graph,
+ with_labels=with_labels,
+ sources=sources,
+ max_depth=max_depth,
+ ascii_only=ascii_only,
+ vertical_chains=vertical_chains,
+ ):
+ _write(line + end)
+
+
+def _find_sources(graph):
+ """
+ Determine a minimal set of nodes such that the entire graph is reachable
+ """
+ # For each connected part of the graph, choose at least
+ # one node as a starting point, preferably without a parent
+ if graph.is_directed():
+ # Choose one node from each SCC with minimum in_degree
+ sccs = list(nx.strongly_connected_components(graph))
+ # condensing the SCCs forms a dag, the nodes in this graph with
+ # 0 in-degree correspond to the SCCs from which the minimum set
+ # of nodes from which all other nodes can be reached.
+ scc_graph = nx.condensation(graph, sccs)
+ supernode_to_nodes = {sn: [] for sn in scc_graph.nodes()}
+ # Note: the order of mapping differs between pypy and cpython
+ # so we have to loop over graph nodes for consistency
+ mapping = scc_graph.graph["mapping"]
+ for n in graph.nodes:
+ sn = mapping[n]
+ supernode_to_nodes[sn].append(n)
+ sources = []
+ for sn in scc_graph.nodes():
+ if scc_graph.in_degree[sn] == 0:
+ scc = supernode_to_nodes[sn]
+ node = min(scc, key=lambda n: graph.in_degree[n])
+ sources.append(node)
+ else:
+ # For undirected graph, the entire graph will be reachable as
+ # long as we consider one node from every connected component
+ sources = [
+ min(cc, key=lambda n: graph.degree[n])
+ for cc in nx.connected_components(graph)
+ ]
+ sources = sorted(sources, key=lambda n: graph.degree[n])
+ return sources
+
+
+def _parse_network_text(lines):
+ """Reconstructs a graph from a network text representation.
+
+ This is mainly used for testing. Network text is for display, not
+ serialization, as such this cannot parse all network text representations
+ because node labels can be ambiguous with the glyphs and indentation used
+ to represent edge structure. Additionally, there is no way to determine if
+ disconnected graphs were originally directed or undirected.
+
+ Parameters
+ ----------
+ lines : list or iterator of strings
+ Input data in network text format
+
+ Returns
+ -------
+ G: NetworkX graph
+ The graph corresponding to the lines in network text format.
+ """
+ from itertools import chain
+ from typing import Any, NamedTuple, Union
+
+ class ParseStackFrame(NamedTuple):
+ node: Any
+ indent: int
+ has_vertical_child: int | None
+
+ initial_line_iter = iter(lines)
+
+ is_ascii = None
+ is_directed = None
+
+ ##############
+ # Initial Pass
+ ##############
+
+ # Do an initial pass over the lines to determine what type of graph it is.
+ # Remember what these lines were, so we can reiterate over them in the
+ # parsing pass.
+ initial_lines = []
+ try:
+ first_line = next(initial_line_iter)
+ except StopIteration:
+ ...
+ else:
+ initial_lines.append(first_line)
+ # The first character indicates if it is an ASCII or UTF graph
+ first_char = first_line[0]
+ if first_char in {
+ UtfBaseGlyphs.empty,
+ UtfBaseGlyphs.newtree_mid[0],
+ UtfBaseGlyphs.newtree_last[0],
+ }:
+ is_ascii = False
+ elif first_char in {
+ AsciiBaseGlyphs.empty,
+ AsciiBaseGlyphs.newtree_mid[0],
+ AsciiBaseGlyphs.newtree_last[0],
+ }:
+ is_ascii = True
+ else:
+ raise AssertionError(f"Unexpected first character: {first_char}")
+
+ if is_ascii:
+ directed_glyphs = AsciiDirectedGlyphs.as_dict()
+ undirected_glyphs = AsciiUndirectedGlyphs.as_dict()
+ else:
+ directed_glyphs = UtfDirectedGlyphs.as_dict()
+ undirected_glyphs = UtfUndirectedGlyphs.as_dict()
+
+ # For both directed / undirected glyphs, determine which glyphs never
+ # appear as substrings in the other undirected / directed glyphs. Glyphs
+ # with this property unambiguously indicates if a graph is directed /
+ # undirected.
+ directed_items = set(directed_glyphs.values())
+ undirected_items = set(undirected_glyphs.values())
+ unambiguous_directed_items = []
+ for item in directed_items:
+ other_items = undirected_items
+ other_supersets = [other for other in other_items if item in other]
+ if not other_supersets:
+ unambiguous_directed_items.append(item)
+ unambiguous_undirected_items = []
+ for item in undirected_items:
+ other_items = directed_items
+ other_supersets = [other for other in other_items if item in other]
+ if not other_supersets:
+ unambiguous_undirected_items.append(item)
+
+ for line in initial_line_iter:
+ initial_lines.append(line)
+ if any(item in line for item in unambiguous_undirected_items):
+ is_directed = False
+ break
+ elif any(item in line for item in unambiguous_directed_items):
+ is_directed = True
+ break
+
+ if is_directed is None:
+ # Not enough information to determine, choose undirected by default
+ is_directed = False
+
+ glyphs = directed_glyphs if is_directed else undirected_glyphs
+
+ # the backedge symbol by itself can be ambiguous, but with spaces around it
+ # becomes unambiguous.
+ backedge_symbol = " " + glyphs["backedge"] + " "
+
+ # Reconstruct an iterator over all of the lines.
+ parsing_line_iter = chain(initial_lines, initial_line_iter)
+
+ ##############
+ # Parsing Pass
+ ##############
+
+ edges = []
+ nodes = []
+ is_empty = None
+
+ noparent = object() # sentinel value
+
+ # keep a stack of previous nodes that could be parents of subsequent nodes
+ stack = [ParseStackFrame(noparent, -1, None)]
+
+ for line in parsing_line_iter:
+ if line == glyphs["empty"]:
+ # If the line is the empty glyph, we are done.
+ # There shouldn't be anything else after this.
+ is_empty = True
+ continue
+
+ if backedge_symbol in line:
+ # This line has one or more backedges, separate those out
+ node_part, backedge_part = line.split(backedge_symbol)
+ backedge_nodes = [u.strip() for u in backedge_part.split(", ")]
+ # Now the node can be parsed
+ node_part = node_part.rstrip()
+ prefix, node = node_part.rsplit(" ", 1)
+ node = node.strip()
+ # Add the backedges to the edge list
+ edges.extend([(u, node) for u in backedge_nodes])
+ else:
+ # No backedge, the tail of this line is the node
+ prefix, node = line.rsplit(" ", 1)
+ node = node.strip()
+
+ prev = stack.pop()
+
+ if node in glyphs["vertical_edge"]:
+ # Previous node is still the previous node, but we know it will
+ # have exactly one child, which will need to have its nesting level
+ # adjusted.
+ modified_prev = ParseStackFrame(
+ prev.node,
+ prev.indent,
+ True,
+ )
+ stack.append(modified_prev)
+ continue
+
+ # The length of the string before the node characters give us a hint
+ # about our nesting level. The only case where this doesn't work is
+ # when there are vertical chains, which is handled explicitly.
+ indent = len(prefix)
+ curr = ParseStackFrame(node, indent, None)
+
+ if prev.has_vertical_child:
+ # In this case we know prev must be the parent of our current line,
+ # so we don't have to search the stack. (which is good because the
+ # indentation check wouldn't work in this case).
+ ...
+ else:
+ # If the previous node nesting-level is greater than the current
+ # nodes nesting-level than the previous node was the end of a path,
+ # and is not our parent. We can safely pop nodes off the stack
+ # until we find one with a comparable nesting-level, which is our
+ # parent.
+ while curr.indent <= prev.indent:
+ prev = stack.pop()
+
+ if node == "...":
+ # The current previous node is no longer a valid parent,
+ # keep it popped from the stack.
+ stack.append(prev)
+ else:
+ # The previous and current nodes may still be parents, so add them
+ # back onto the stack.
+ stack.append(prev)
+ stack.append(curr)
+
+ # Add the node and the edge to its parent to the node / edge lists.
+ nodes.append(curr.node)
+ if prev.node is not noparent:
+ edges.append((prev.node, curr.node))
+
+ if is_empty:
+ # Sanity check
+ assert len(nodes) == 0
+
+ # Reconstruct the graph
+ cls = nx.DiGraph if is_directed else nx.Graph
+ new = cls()
+ new.add_nodes_from(nodes)
+ new.add_edges_from(edges)
+ return new