Adding all project files

This commit is contained in:
Martina Burlando 2025-08-02 02:00:33 +02:00
parent 6c9e127bdc
commit cd4316ad0f
42289 changed files with 8009643 additions and 0 deletions

View file

@ -0,0 +1,18 @@
"""
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 *

View file

@ -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._dispatch(graphs=None)
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.strip().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 " f"{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._dispatch(graphs=None)
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,
)

View file

@ -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._dispatch(graphs=None)
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.strip().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._dispatch(graphs=None)
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._dispatch(graphs=None)
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,
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,878 @@
"""
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._dispatch(graphs=None)
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._dispatch(graphs=None)
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"))

View file

@ -0,0 +1,416 @@
# 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._dispatch(graphs=None)
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._dispatch(graphs=None)
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,
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
"""
*********
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 *

View file

@ -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._dispatch(graphs=None)
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

View file

@ -0,0 +1,174 @@
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._dispatch(graphs=None)
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

View file

@ -0,0 +1,244 @@
from itertools import chain, count
import networkx as nx
__all__ = ["node_link_data", "node_link_graph"]
_attrs = {
"source": "source",
"target": "target",
"name": "id",
"key": "key",
"link": "links",
}
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",
link="links",
):
"""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.
link : string
A string that provides the 'link' 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
--------
>>> G = nx.Graph([("A", "B")])
>>> data1 = nx.node_link_data(G)
>>> data1
{'directed': False, 'multigraph': False, 'graph': {}, 'nodes': [{'id': 'A'}, {'id': 'B'}], 'links': [{'source': 'A', 'target': 'B'}]}
To serialize with JSON
>>> import json
>>> s1 = json.dumps(data1)
>>> s1
'{"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "A"}, {"id": "B"}], "links": [{"source": "A", "target": "B"}]}'
A graph can also be serialized by passing `node_link_data` as an encoder function. The two methods are equivalent.
>>> 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, link="edges", source="from", target="to")
>>> data2
{'directed': True, 'multigraph': False, 'graph': {}, 'nodes': [{'id': 0}, {'id': 1}], 'edges': [{'from': 1, 'to': 0}]}
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
"""
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[link] = [
{**d, source: u, target: v, key: k}
for u, v, k, d in G.edges(keys=True, data=True)
]
else:
data[link] = [{**d, source: u, target: v} for u, v, d in G.edges(data=True)]
return data
@nx._dispatch(graphs=None)
def node_link_graph(
data,
directed=False,
multigraph=True,
*,
source="source",
target="target",
name="id",
key="key",
link="links",
):
"""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.
link : string
A string that provides the 'link' 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.
>>> G = nx.Graph([('A', 'B')])
>>> data = nx.node_link_data(G)
>>> data
{'directed': False, 'multigraph': False, 'graph': {}, 'nodes': [{'id': 'A'}, {'id': 'B'}], 'links': [{'source': 'A', 'target': 'B'}]}
Revert data in node-link format to a graph.
>>> H = nx.node_link_graph(data)
>>> print(H.edges)
[('A', 'B')]
To serialize and deserialize a graph with JSON,
>>> import json
>>> d = json.dumps(node_link_data(G))
>>> H = node_link_graph(json.loads(d))
>>> 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
"""
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[link]:
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

View file

@ -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)

View file

@ -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")

View file

@ -0,0 +1,144 @@
import json
import pytest
import networkx as nx
from networkx.readwrite.json_graph import node_link_data, node_link_graph
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):
with pytest.raises(nx.NetworkXError):
G = nx.MultiDiGraph()
node_link_data(G, name="node", source="node", target="node", key="node")
def test_graph(self):
G = nx.path_graph(4)
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"
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
d = json.dumps(node_link_data(G))
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()
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")
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])
d = node_link_data(G)
dumped_d = json.dumps(d)
dd = json.loads(dumped_d)
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})
s = node_link_data(G)
output = json.dumps(s, ensure_ascii=False)
data = json.loads(output)
H = node_link_graph(data)
assert H.nodes[1][q] == q
def test_exception(self):
with pytest.raises(nx.NetworkXError):
G = nx.MultiDiGraph()
attrs = {"name": "node", "source": "node", "target": "node", "key": "node"}
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)
data = node_link_data(G)
assert data["links"][0]["source"] == "A"
assert data["links"][0]["target"] == q
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

View file

@ -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")

View file

@ -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._dispatch(graphs=None)
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

View file

@ -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._dispatch(graphs=None)
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._dispatch(graphs=None)
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

View file

@ -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._dispatch(graphs=None)
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.strip().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 " f"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.strip().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}) " f"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}) " f"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._dispatch(graphs=None)
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,
)

View file

@ -0,0 +1,104 @@
"""
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._dispatch(graphs=None)
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._dispatch(graphs=None)
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

View file

@ -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._dispatch(graphs=None)
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._dispatch(graphs=None)
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

View file

@ -0,0 +1,376 @@
# 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._dispatch(graphs=None)
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._dispatch(graphs=None)
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)

View file

@ -0,0 +1,268 @@
"""
Unit tests for adjlist.
"""
import io
import os
import tempfile
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):
G = nx.Graph()
name1 = chr(2344) + chr(123) + chr(6543)
name2 = chr(5543) + chr(1543) + chr(324)
G.add_edge(name1, "Radiohead", **{name2: 3})
fd, fname = tempfile.mkstemp()
nx.write_multiline_adjlist(G, fname)
H = nx.read_multiline_adjlist(fname)
assert graphs_equal(G, H)
os.close(fd)
os.unlink(fname)
def test_latin1_err(self):
G = nx.Graph()
name1 = chr(2344) + chr(123) + chr(6543)
name2 = chr(5543) + chr(1543) + chr(324)
G.add_edge(name1, "Radiohead", **{name2: 3})
fd, fname = tempfile.mkstemp()
pytest.raises(
UnicodeEncodeError, nx.write_multiline_adjlist, G, fname, encoding="latin-1"
)
os.close(fd)
os.unlink(fname)
def test_latin1(self):
G = nx.Graph()
name1 = "Bj" + chr(246) + "rk"
name2 = chr(220) + "ber"
G.add_edge(name1, "Radiohead", **{name2: 3})
fd, fname = tempfile.mkstemp()
nx.write_multiline_adjlist(G, fname, encoding="latin-1")
H = nx.read_multiline_adjlist(fname, encoding="latin-1")
assert graphs_equal(G, H)
os.close(fd)
os.unlink(fname)
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):
G = self.G
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_adjlist_digraph(self):
G = self.DG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_adjlist_integers(self):
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_adjlist_multigraph(self):
G = self.XG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_adjlist_multidigraph(self):
G = self.XDG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
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):
G = self.G
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_multiline_adjlist_digraph(self):
G = self.DG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_multiline_adjlist_integers(self):
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_multiline_adjlist_multigraph(self):
G = self.XG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_multiline_adjlist_multidigraph(self):
G = self.XDG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
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()))

View file

@ -0,0 +1,314 @@
"""
Unit tests for edgelists.
"""
import io
import os
import tempfile
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):
G = nx.Graph()
name1 = chr(2344) + chr(123) + chr(6543)
name2 = chr(5543) + chr(1543) + chr(324)
G.add_edge(name1, "Radiohead", **{name2: 3})
fd, fname = tempfile.mkstemp()
nx.write_edgelist(G, fname)
H = nx.read_edgelist(fname)
assert graphs_equal(G, H)
os.close(fd)
os.unlink(fname)
def test_latin1_issue(self):
G = nx.Graph()
name1 = chr(2344) + chr(123) + chr(6543)
name2 = chr(5543) + chr(1543) + chr(324)
G.add_edge(name1, "Radiohead", **{name2: 3})
fd, fname = tempfile.mkstemp()
pytest.raises(
UnicodeEncodeError, nx.write_edgelist, G, fname, encoding="latin-1"
)
os.close(fd)
os.unlink(fname)
def test_latin1(self):
G = nx.Graph()
name1 = "Bj" + chr(246) + "rk"
name2 = chr(220) + "ber"
G.add_edge(name1, "Radiohead", **{name2: 3})
fd, fname = tempfile.mkstemp()
nx.write_edgelist(G, fname, encoding="latin-1")
H = nx.read_edgelist(fname, encoding="latin-1")
assert graphs_equal(G, H)
os.close(fd)
os.unlink(fname)
def test_edgelist_graph(self):
G = self.G
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_edgelist_digraph(self):
G = self.DG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_edgelist_integers(self):
G = nx.convert_node_labels_to_integers(self.G)
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_edgelist_multigraph(self):
G = self.XG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)
def test_edgelist_multidigraph(self):
G = self.XDG
(fd, fname) = tempfile.mkstemp()
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()))
os.close(fd)
os.unlink(fname)

View file

@ -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()
)

View file

@ -0,0 +1,753 @@
import codecs
import io
import math
import os
import tempfile
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):
(fd, fname) = tempfile.mkstemp()
fh = open(fname, "w")
fh.write(self.simple_data)
fh.close()
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))
os.close(fd)
os.unlink(fname)
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):
# 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
fobj = tempfile.NamedTemporaryFile()
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):
node = "node" + chr(169)
G = nx.Graph()
G.add_node(node)
fobj = tempfile.NamedTemporaryFile()
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):
node = 1.0
G = nx.Graph()
G.add_node(node)
fobj = tempfile.NamedTemporaryFile()
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):
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")
fobj = tempfile.NamedTemporaryFile()
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):
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 tempfile.TemporaryFile() 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 u'u\4200' target u'u\4200' ] "
+ "node [ id u'u\4200' 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):
# 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)
fd, fname = tempfile.mkstemp()
try:
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
finally:
os.close(fd)
os.unlink(fname)
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)

View file

@ -0,0 +1,169 @@
import tempfile
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):
with tempfile.NamedTemporaryFile() 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"

File diff suppressed because it is too large Load diff

View file

@ -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())

View file

@ -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())

View file

@ -0,0 +1,130 @@
"""
Pajek tests
"""
import os
import tempfile
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"
(fd, cls.fname) = tempfile.mkstemp()
with os.fdopen(fd, "wb") as fh:
fh.write(cls.data.encode("UTF-8"))
@classmethod
def teardown_class(cls):
os.unlink(cls.fname)
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):
G = nx.parse_pajek(self.data)
Gin = nx.read_pajek(self.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

View file

@ -0,0 +1,173 @@
import tempfile
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):
# On Windows, we can't reopen a file that is open
# So, for test we get a valid name from tempfile but close it.
with tempfile.NamedTemporaryFile() as f:
fullfilename = f.name
# file should be closed now, so write_sparse6 can open it
nx.write_sparse6(nx.null_graph(), fullfilename)
fh = open(fullfilename, mode="rb")
assert fh.read() == b">>sparse6<<:?\n"
fh.close()
import os
os.remove(fullfilename)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,950 @@
"""
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__ = ["forest_str", "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 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 forest_str(graph, with_labels=True, sources=None, write=None, ascii_only=False):
"""Creates a nice utf8 representation of a forest
This function has been superseded by
:func:`nx.readwrite.text.generate_network_text`, which should be used
instead.
Parameters
----------
graph : nx.DiGraph | nx.Graph
Graph to represent (must be a tree, forest, or the empty graph)
with_labels : bool
If True will use the "label" attribute of a node to display if it
exists otherwise it will use the node value itself. Defaults to True.
sources : List
Mainly relevant for undirected forests, specifies which nodes to list
first. If unspecified the root nodes of each tree will be used for
directed forests; for undirected forests this defaults to the nodes
with the smallest degree.
write : callable
Function to use to write to, if None new lines are appended to
a list and returned. If set to the `print` function, lines will
be written to stdout as they are generated. If specified,
this function will return None. Defaults to None.
ascii_only : Boolean
If True only ASCII characters are used to construct the visualization
Returns
-------
str | None :
utf8 representation of the tree / forest
Examples
--------
>>> graph = nx.balanced_tree(r=2, h=3, create_using=nx.DiGraph)
>>> print(nx.forest_str(graph))
0
1
3
7
8
4
9
10
2
5
11
12
6
13
14
>>> graph = nx.balanced_tree(r=1, h=2, create_using=nx.Graph)
>>> print(nx.forest_str(graph))
0
1
2
>>> print(nx.forest_str(graph, ascii_only=True))
+-- 0
L-- 1
L-- 2
"""
msg = (
"\nforest_str is deprecated as of version 3.1 and will be removed "
"in version 3.3. Use generate_network_text or write_network_text "
"instead.\n"
)
warnings.warn(msg, DeprecationWarning)
if len(graph.nodes) > 0:
if not nx.is_forest(graph):
raise nx.NetworkXNotImplemented("input must be a forest or the empty graph")
printbuf = []
if write is None:
_write = printbuf.append
else:
_write = write
write_network_text(
graph,
_write,
with_labels=with_labels,
sources=sources,
ascii_only=ascii_only,
end="",
)
if write is None:
# Only return a string if the custom write function was not specified
return "\n".join(printbuf)
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: Union[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