chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from easygraph.readwrite.edgelist import *
|
||||
from easygraph.readwrite.gexf import *
|
||||
from easygraph.readwrite.gml import *
|
||||
from easygraph.readwrite.graphml import *
|
||||
from easygraph.readwrite.graphviz import *
|
||||
from easygraph.readwrite.json_graph import *
|
||||
from easygraph.readwrite.pajek import *
|
||||
from easygraph.readwrite.pickle import *
|
||||
from easygraph.readwrite.ucinet import *
|
||||
@@ -0,0 +1,463 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import open_file
|
||||
|
||||
|
||||
__all__ = [
|
||||
"parse_edgelist",
|
||||
"generate_edgelist",
|
||||
"write_edgelist",
|
||||
"read_edgelist",
|
||||
"read_weighted_edgelist",
|
||||
"write_weighted_edgelist",
|
||||
]
|
||||
|
||||
|
||||
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 : EasyGraph graph constructor, optional (default=eg.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: EasyGraph Graph
|
||||
The graph corresponding to lines
|
||||
|
||||
Examples
|
||||
--------
|
||||
Edgelist with no data:
|
||||
|
||||
>>> lines = ["1 2", "2 3", "3 4"]
|
||||
>>> G = eg.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 = eg.parse_edgelist(lines, nodetype=int)
|
||||
>>> list(G)
|
||||
[1, 2, 3, 4]
|
||||
>>> list(G.edges)
|
||||
[(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 = eg.parse_edgelist(lines, nodetype=int, data=(("weight", float),))
|
||||
>>> list(G)
|
||||
[1, 2, 3, 4]
|
||||
>>> list(G.edges)
|
||||
[(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 = eg.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
|
||||
|
||||
|
||||
def generate_edgelist(G, delimiter=" ", data=True):
|
||||
"""Generate a single line of the graph G in edge list format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph 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 = eg.lollipop_graph(4, 3)
|
||||
>>> G[1][2]["weight"] = 3
|
||||
>>> G[3][4]["capacity"] = 12
|
||||
>>> for line in eg.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 eg.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 eg.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
|
||||
"""
|
||||
edges = G.edges
|
||||
if edges and len(edges[0]) > 3:
|
||||
# multigraph
|
||||
edges = ((u, v, d) for u, v, _, d in edges)
|
||||
if data is True:
|
||||
for u, v, d in edges:
|
||||
e = u, v, dict(d)
|
||||
yield delimiter.join(map(str, e))
|
||||
elif data is False:
|
||||
for u, v, _ in edges:
|
||||
e = u, v
|
||||
yield delimiter.join(map(str, e))
|
||||
else:
|
||||
for u, v, d in edges:
|
||||
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 EasyGraph 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 = eg.path_graph(4)
|
||||
>>> eg.write_edgelist(G, "test.edgelist")
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> fh = open("test.edgelist", "wb")
|
||||
>>> eg.write_edgelist(G, fh)
|
||||
>>> eg.write_edgelist(G, "test.edgelist.gz")
|
||||
>>> eg.write_edgelist(G, "test.edgelist.gz", data=False)
|
||||
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edge(1, 2, weight=7, color="red")
|
||||
>>> eg.write_edgelist(G, "test.edgelist", data=False)
|
||||
>>> eg.write_edgelist(G, "test.edgelist", data=["color"])
|
||||
>>> eg.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))
|
||||
|
||||
|
||||
@open_file(0, mode="rb")
|
||||
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 : EasyGraph graph constructor, optional (default=eg.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 easygraph Graph or other type specified with create_using
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> eg.write_edgelist(eg.path_graph(4), "test.edgelist")
|
||||
>>> G = eg.read_edgelist("test.edgelist")
|
||||
|
||||
>>> fh = open("test.edgelist", "rb")
|
||||
>>> G = eg.read_edgelist(fh)
|
||||
>>> fh.close()
|
||||
|
||||
>>> G = eg.read_edgelist("test.edgelist", nodetype=int)
|
||||
>>> G = eg.read_edgelist("test.edgelist", create_using=eg.DiGraph)
|
||||
|
||||
Edgelist with data in a list:
|
||||
|
||||
>>> textline = "1 2 3"
|
||||
>>> fh = open("test.edgelist", "w")
|
||||
>>> d = fh.write(textline)
|
||||
>>> fh.close()
|
||||
>>> G = eg.read_edgelist("test.edgelist", nodetype=int, data=(("weight", float),))
|
||||
>>> list(G)
|
||||
[1, 2]
|
||||
>>> list(G.edges)
|
||||
[(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 EasyGraph 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 = eg.Graph()
|
||||
>>> G.add_edge(1, 2, weight=7)
|
||||
>>> eg.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,
|
||||
)
|
||||
|
||||
|
||||
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 : EasyGraph graph constructor, optional (default=eg.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 easygraph 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=eg.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
@@ -0,0 +1,803 @@
|
||||
"""
|
||||
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
|
||||
from typing import NamedTuple
|
||||
from unicodedata import category
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import open_file
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"]
|
||||
|
||||
LIST_START_VALUE = "_easygraph_list_start"
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
msg = "literal_destringizer is deprecated and will be removed in 3.0."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
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")
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 : EasyGraph graph
|
||||
The parsed graph.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If the input cannot be parsed.
|
||||
|
||||
See Also
|
||||
--------
|
||||
write_gml, read_gml
|
||||
|
||||
Notes
|
||||
-----
|
||||
This stores nested GML attributes as dictionaries in the EasyGraph 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:`easygraph.readwrite.gml` for more details.
|
||||
"""
|
||||
|
||||
def decode_line(line):
|
||||
if isinstance(line, bytes):
|
||||
try:
|
||||
line.decode("ascii")
|
||||
except UnicodeDecodeError as err:
|
||||
raise EasyGraphError("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 EasyGraphError("input line contains newline")
|
||||
yield line
|
||||
|
||||
G = parse_gml_lines(filter_lines(lines), label, destringizer)
|
||||
return G
|
||||
|
||||
|
||||
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
|
||||
for line in lines:
|
||||
length = len(line)
|
||||
pos = 0
|
||||
while pos < length:
|
||||
match = tokens.match(line, pos)
|
||||
if match is None:
|
||||
m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})"
|
||||
raise EasyGraphError(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 EasyGraphError(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_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_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
|
||||
curr_token = next(tokens)
|
||||
elif category == Pattern.DICT_START:
|
||||
curr_token, value = parse_dict(curr_token)
|
||||
else:
|
||||
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)
|
||||
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_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 EasyGraphError("input contains no graph")
|
||||
graph = dct["graph"]
|
||||
if isinstance(graph, list):
|
||||
raise EasyGraphError("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 = eg.DiGraph() if directed else eg.Graph()
|
||||
else:
|
||||
G = eg.MultiDiGraph() if directed else eg.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 EasyGraphError(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 EasyGraphError(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 EasyGraphError(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 EasyGraphError(f"edge #{i} has undefined source {source!r}")
|
||||
if target not in G:
|
||||
raise EasyGraphError(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 EasyGraphError(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 EasyGraphError(msg + " is duplicated\n" + msg2)
|
||||
G.add_edge(source, target, key, **edge)
|
||||
|
||||
if label is not None and label != "id":
|
||||
G = eg.relabel_nodes(G, mapping)
|
||||
return G
|
||||
|
||||
|
||||
def generate_gml(G, stringizer=None):
|
||||
r"""Generate a single entry of the graph `G` in GML format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph 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
|
||||
------
|
||||
EasyGraphError
|
||||
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:`easygraph.readwrite.gml` for more details.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_node("1")
|
||||
>>> print("\n".join(eg.generate_gml(G)))
|
||||
graph [
|
||||
node [
|
||||
id 0
|
||||
label "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 EasyGraphError(f"{key!r} is not a string")
|
||||
if not valid_keys.match(key):
|
||||
raise EasyGraphError(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, (list, tuple))
|
||||
and key != "label"
|
||||
and value
|
||||
and not in_list
|
||||
):
|
||||
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 EasyGraphError(
|
||||
f"{value!r} cannot be converted into a string"
|
||||
) from err
|
||||
if not isinstance(value, str):
|
||||
raise EasyGraphError(f"{value!r} is not a string")
|
||||
yield indent + key + ' "' + escape(value) + '"'
|
||||
|
||||
yield "graph ["
|
||||
|
||||
# Output graph attributes
|
||||
multigraph = G.is_multigraph()
|
||||
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:
|
||||
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(0, mode="rb")
|
||||
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 : EasyGraph graph
|
||||
The parsed graph.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
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:`easygraph.readwrite.gml` for more details.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> eg.write_gml(G, "test.gml")
|
||||
|
||||
GML values are interpreted as strings by default:
|
||||
|
||||
>>> H = eg.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 = eg.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 EasyGraphError("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
|
||||
|
||||
|
||||
@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 : EasyGraph 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
|
||||
------
|
||||
EasyGraphError
|
||||
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:`easygraph.readwrite.gml` for more details.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> eg.write_gml(G, "test.gml")
|
||||
|
||||
Filenames ending in .gz or .bz2 will be compressed.
|
||||
|
||||
>>> eg.write_gml(G, "test.gml.gz")
|
||||
"""
|
||||
for line in generate_gml(G, stringizer):
|
||||
path.write((line + "\n").encode("ascii"))
|
||||
|
||||
|
||||
def literal_stringizer(value):
|
||||
msg = "literal_stringizer is deprecated and will be removed in 3.0."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
|
||||
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 = "{value!r} cannot be converted into a Python literal"
|
||||
raise ValueError(msg)
|
||||
|
||||
buf = StringIO()
|
||||
stringize(value)
|
||||
return buf.getvalue()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = ["write_dot", "read_dot", "from_agraph", "to_agraph"]
|
||||
|
||||
|
||||
def from_agraph(A, create_using=None):
|
||||
"""Returns a EasyGraph Graph or DiGraph from a PyGraphviz graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : PyGraphviz AGraph
|
||||
A graph created with PyGraphviz
|
||||
|
||||
create_using : EasyGraph graph constructor, optional (default=None)
|
||||
Graph type to create. If graph instance, then cleared before populated.
|
||||
If `None`, then the appropriate Graph type is inferred from `A`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> K5 = eg.complete_graph(5)
|
||||
>>> A = eg.to_agraph(K5)
|
||||
>>> G = eg.from_agraph(A)
|
||||
|
||||
Notes
|
||||
-----
|
||||
The Graph G will have a dictionary G.graph_attr containing
|
||||
the default graphviz attributes for graphs, nodes and edges.
|
||||
|
||||
Default node attributes will be in the dictionary G.node_attr
|
||||
which is keyed by node.
|
||||
|
||||
Edge attributes will be returned as edge data in G. With
|
||||
edge_attr=False the edge data will be the Graphviz edge weight
|
||||
attribute or the value 1 if no edge weight attribute is found.
|
||||
|
||||
"""
|
||||
if create_using is None:
|
||||
if A.is_directed():
|
||||
if A.is_strict():
|
||||
create_using = eg.DiGraph
|
||||
else:
|
||||
create_using = eg.MultiDiGraph
|
||||
else:
|
||||
if A.is_strict():
|
||||
create_using = eg.Graph
|
||||
else:
|
||||
create_using = eg.MultiGraph
|
||||
|
||||
# assign defaults
|
||||
N = eg.empty_graph(0, create_using)
|
||||
if A.name is not None:
|
||||
N.name = A.name
|
||||
|
||||
# add graph attributes
|
||||
N.graph.update(A.graph_attr)
|
||||
|
||||
# add nodes, attributes to N.node_attr
|
||||
for n in A.nodes():
|
||||
str_attr = {str(k): v for k, v in n.attr.items()}
|
||||
N.add_node(str(n), **str_attr)
|
||||
|
||||
# add edges, assign edge data as dictionary of attributes
|
||||
for e in A.edges():
|
||||
u, v = str(e[0]), str(e[1])
|
||||
attr = dict(e.attr)
|
||||
str_attr = {str(k): v for k, v in attr.items()}
|
||||
if not N.is_multigraph():
|
||||
if e.name is not None:
|
||||
str_attr["key"] = e.name
|
||||
N.add_edge(u, v, **str_attr)
|
||||
else:
|
||||
N.add_edge(u, v, key=e.name, **str_attr)
|
||||
|
||||
# add default attributes for graph, nodes, and edges
|
||||
# hang them on N.graph_attr
|
||||
N.graph["graph"] = dict(A.graph_attr)
|
||||
N.graph["node"] = dict(A.node_attr)
|
||||
N.graph["edge"] = dict(A.edge_attr)
|
||||
return N
|
||||
|
||||
|
||||
def to_agraph(N):
|
||||
"""Returns a pygraphviz graph from a EasyGraph graph N.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
N : EasyGraph graph
|
||||
A graph created with EasyGraph
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> K5 = eg.complete_graph(5)
|
||||
>>> A = eg.to_agraph(K5)
|
||||
|
||||
Notes
|
||||
-----
|
||||
If N has an dict N.graph_attr an attempt will be made first
|
||||
to copy properties attached to the graph (see from_agraph)
|
||||
and then updated with the calling arguments if any.
|
||||
|
||||
"""
|
||||
try:
|
||||
import pygraphviz
|
||||
except ImportError as err:
|
||||
raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err
|
||||
directed = N.is_directed()
|
||||
strict = eg.number_of_selfloops(N) == 0 and not N.is_multigraph()
|
||||
A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed)
|
||||
|
||||
# default graph attributes
|
||||
A.graph_attr.update(N.graph.get("graph", {}))
|
||||
A.node_attr.update(N.graph.get("node", {}))
|
||||
A.edge_attr.update(N.graph.get("edge", {}))
|
||||
|
||||
A.graph_attr.update(
|
||||
(k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge")
|
||||
)
|
||||
|
||||
# add nodes
|
||||
for n, nodedata in N.nodes.items():
|
||||
A.add_node(n)
|
||||
# Add node data
|
||||
a = A.get_node(n)
|
||||
a.attr.update({k: str(v) for k, v in nodedata.items()})
|
||||
|
||||
# loop over edges
|
||||
if N.is_multigraph():
|
||||
for u, v, key, edgedata in N.edges:
|
||||
str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"}
|
||||
A.add_edge(u, v, key=str(key))
|
||||
# Add edge data
|
||||
a = A.get_edge(u, v)
|
||||
a.attr.update(str_edgedata)
|
||||
|
||||
else:
|
||||
for u, v, edgedata in N.edges:
|
||||
str_edgedata = {k: str(v) for k, v in edgedata.items()}
|
||||
A.add_edge(u, v)
|
||||
# Add edge data
|
||||
a = A.get_edge(u, v)
|
||||
a.attr.update(str_edgedata)
|
||||
|
||||
return A
|
||||
|
||||
|
||||
def write_dot(G, path):
|
||||
"""Write EasyGraph graph G to Graphviz dot format on path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
path : filename
|
||||
Filename or file handle to write
|
||||
"""
|
||||
A = to_agraph(G)
|
||||
A.write(path)
|
||||
A.clear()
|
||||
return
|
||||
|
||||
|
||||
def read_dot(path):
|
||||
"""Returns a EasyGraph graph from a dot file on path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : file or string
|
||||
File name or file handle to read.
|
||||
"""
|
||||
try:
|
||||
import pygraphviz
|
||||
except ImportError as err:
|
||||
raise ImportError(
|
||||
"read_dot() requires pygraphviz http://pygraphviz.github.io/"
|
||||
) from err
|
||||
A = pygraphviz.AGraph(file=path)
|
||||
gr = from_agraph(A)
|
||||
A.clear()
|
||||
return gr
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
*********
|
||||
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 easygraph.readwrite.json_graph.node_link import *
|
||||
@@ -0,0 +1,111 @@
|
||||
from itertools import chain
|
||||
from itertools import count
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = ["node_link_graph"]
|
||||
|
||||
|
||||
_attrs = dict(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_graph(data, directed=False, multigraph=True, attrs=None):
|
||||
"""Returns graph from node-link data format.
|
||||
|
||||
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.
|
||||
|
||||
attrs : dict
|
||||
A dictionary that contains five keys 'source', 'target', 'name',
|
||||
'key' and 'link'. The corresponding values provide the attribute
|
||||
names for storing NetworkX-internal graph data. Default value:
|
||||
|
||||
dict(source='source', target='target', name='id',
|
||||
key='key', link='links')
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : EasyGraph graph
|
||||
A EasyGraph graph object
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from easygraph.readwrite import json_graph
|
||||
>>> G = eg.Graph([("A", "B")])
|
||||
>>> data = json_graph.node_link_data(G)
|
||||
>>> H = json_graph.node_link_graph(data)
|
||||
|
||||
Notes
|
||||
-----
|
||||
Attribute 'key' is only used for multigraphs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
node_link_data, adjacency_data, tree_data
|
||||
"""
|
||||
# Allow 'attrs' to keep default values.
|
||||
if attrs is None:
|
||||
attrs = _attrs
|
||||
else:
|
||||
attrs.update({k: v for k, v in _attrs.items() if k not in attrs})
|
||||
multigraph = data.get("multigraph", multigraph)
|
||||
directed = data.get("directed", directed)
|
||||
if multigraph:
|
||||
graph = eg.MultiGraph()
|
||||
else:
|
||||
graph = eg.Graph()
|
||||
if directed:
|
||||
graph = graph.to_directed()
|
||||
name = attrs["name"]
|
||||
source = attrs["source"]
|
||||
target = attrs["target"]
|
||||
links = attrs["link"]
|
||||
# Allow 'key' to be omitted from attrs if the graph is not a multigraph.
|
||||
key = None if not multigraph else attrs["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[links]:
|
||||
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
|
||||
@@ -0,0 +1,337 @@
|
||||
# This file is part of the NetworkX distribution.
|
||||
|
||||
# NetworkX is distributed with the 3-clause BSD license.
|
||||
|
||||
|
||||
# ::
|
||||
# Copyright (C) 2004-2022, NetworkX Developers
|
||||
# Aric Hagberg <hagberg@lanl.gov>
|
||||
# Dan Schult <dschult@colgate.edu>
|
||||
# Pieter Swart <swart@lanl.gov>
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
|
||||
# * Neither the name of the NetworkX Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
*****
|
||||
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 easygraph as eg
|
||||
|
||||
# import networkx as nx
|
||||
from easygraph.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 EasyGraph graph
|
||||
|
||||
References
|
||||
----------
|
||||
See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
|
||||
for format information.
|
||||
"""
|
||||
if G.name == "":
|
||||
name = "EasyGraph"
|
||||
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."
|
||||
f" {('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"
|
||||
# from icecream import ic
|
||||
# ic(G.edges)
|
||||
# if isinstance(G, MultiGraph)
|
||||
for u, v, *edgedata in G.edges:
|
||||
# if len(edgedata) > 1:
|
||||
# edgedata = edgedata[1]
|
||||
# else:
|
||||
# edgedata = edgedata[0]
|
||||
edgedata = edgedata[-1]
|
||||
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."
|
||||
f" {('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 EasyGraph graph
|
||||
path : file or string
|
||||
File or filename to write.
|
||||
Filenames ending in .gz or .bz2 will be compressed.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> eg.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")
|
||||
def read_pajek(path):
|
||||
"""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 : EasyGraph MultiGraph or MultiDiGraph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> eg.write_pajek(G, "test.net")
|
||||
>>> G = eg.read_pajek("test.net")
|
||||
|
||||
To create a Graph instead of a MultiGraph use
|
||||
|
||||
>>> G1 = eg.Graph(G)
|
||||
|
||||
References
|
||||
----------
|
||||
See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm
|
||||
for format information.
|
||||
"""
|
||||
lines = (line.decode() for line in path)
|
||||
# with open(path) as f:
|
||||
# lines = f.readlines()
|
||||
return parse_pajek(lines)
|
||||
|
||||
|
||||
def parse_pajek(lines):
|
||||
"""Parse Pajek format graph from string or iterable.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : string or iterable
|
||||
Data in Pajek format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : EasyGraph graph
|
||||
|
||||
See Also
|
||||
--------
|
||||
read_pajek
|
||||
|
||||
"""
|
||||
import shlex
|
||||
|
||||
# multigraph=False
|
||||
if isinstance(lines, str):
|
||||
lines = iter(lines.split("\n"))
|
||||
# from itertools import tee
|
||||
# lines, lines2 = tee(lines)
|
||||
# from icecream import ic
|
||||
# ic(next(lines2))
|
||||
lines = iter([line.rstrip("\n") for line in lines])
|
||||
G = eg.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 for x in shlex.split(str(l))]
|
||||
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 = eg.MultiGraph(G)
|
||||
if l.lower().startswith("*arcs"):
|
||||
# switch to directed with multiple arcs for each existing edge
|
||||
# G = G.to_directed()
|
||||
pass
|
||||
for l in lines:
|
||||
try:
|
||||
splitline = [x for x in shlex.split(str(l))]
|
||||
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 = eg.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
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
def read_pickle(file_name):
|
||||
import pickle
|
||||
|
||||
with open(file_name, "rb") as f:
|
||||
return pickle.load(f)
|
||||
|
||||
|
||||
def write_pickle(file_name, obj):
|
||||
import pickle
|
||||
|
||||
with open(file_name, "wb") as f:
|
||||
pickle.dump(obj, f)
|
||||
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
Unit tests for edgelists.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.utils import edges_equal
|
||||
from easygraph.utils import graphs_equal
|
||||
from easygraph.utils import 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 = eg.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 = eg.read_weighted_edgelist(bytesIO, nodetype=int)
|
||||
assert edges_equal(G.edges, _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 = eg.read_edgelist(bytesIO, nodetype=int, **extra_kwargs)
|
||||
assert edges_equal(G.edges, expected)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def example_graph():
|
||||
G = eg.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 = eg.parse_edgelist(["1 2", "2 3", "3 4"], nodetype=int)
|
||||
assert nodes_equal(G.nodes, H.nodes)
|
||||
assert edges_equal(G.edges, H.edges, need_data=False)
|
||||
|
||||
|
||||
def test_parse_edgelist_with_data_dict(example_graph):
|
||||
G = example_graph
|
||||
H = eg.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, H.edges)
|
||||
|
||||
|
||||
def test_parse_edgelist_with_data_list(example_graph):
|
||||
G = example_graph
|
||||
H = eg.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, H.edges)
|
||||
|
||||
|
||||
def test_parse_edgelist():
|
||||
# ignore lines with less than 2 nodes
|
||||
lines = ["1;2", "2 3", "3 4"]
|
||||
G = eg.parse_edgelist(lines, nodetype=int)
|
||||
# assert list(G.edges) == [(2, 3), (3, 4)]
|
||||
assert edges_equal(G.edges, [(2, 3), (3, 4)], need_data=False)
|
||||
# unknown nodetype
|
||||
with pytest.raises(TypeError, match="Failed to convert nodes"):
|
||||
lines = ["1 2", "2 3", "3 4"]
|
||||
eg.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"]
|
||||
eg.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"]
|
||||
eg.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'"]
|
||||
eg.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 = eg.parse_edgelist(edgelist, comments=None)
|
||||
H = eg.Graph([e.split(" ") for e in edgelist])
|
||||
assert edges_equal(G.edges, H.edges)
|
||||
|
||||
|
||||
class TestEdgelist:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.G = eg.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 = eg.DiGraph(cls.G)
|
||||
cls.XG = eg.MultiGraph()
|
||||
cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)])
|
||||
cls.XDG = eg.MultiDiGraph(cls.XG)
|
||||
|
||||
def test_write_edgelist_1(self):
|
||||
fh = io.BytesIO()
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
eg.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 = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
eg.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 = eg.Graph()
|
||||
G.add_edge(1, 2, weight=2.0)
|
||||
G.add_edge(2, 3, weight=3.0)
|
||||
eg.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 = eg.Graph()
|
||||
G.add_edge(1, 2, weight=2.0)
|
||||
G.add_edge(2, 3, weight=3.0)
|
||||
eg.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 = eg.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()
|
||||
eg.write_edgelist(G, fname)
|
||||
H = eg.read_edgelist(fname)
|
||||
assert graphs_equal(G, H)
|
||||
os.close(fd)
|
||||
os.unlink(fname)
|
||||
|
||||
def test_latin1_issue(self):
|
||||
G = eg.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, eg.write_edgelist, G, fname, encoding="latin-1"
|
||||
)
|
||||
os.close(fd)
|
||||
os.unlink(fname)
|
||||
|
||||
def test_latin1(self):
|
||||
G = eg.Graph()
|
||||
name1 = "Bj" + chr(246) + "rk"
|
||||
name2 = chr(220) + "ber"
|
||||
G.add_edge(name1, "Radiohead", **{name2: 3})
|
||||
fd, fname = tempfile.mkstemp()
|
||||
eg.write_edgelist(G, fname, encoding="latin-1")
|
||||
H = eg.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()
|
||||
eg.write_edgelist(G, fname)
|
||||
H = eg.read_edgelist(fname)
|
||||
H2 = eg.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()
|
||||
eg.write_edgelist(G, fname)
|
||||
H = eg.read_edgelist(fname, create_using=eg.DiGraph())
|
||||
H2 = eg.read_edgelist(fname, create_using=eg.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 = eg.convert_node_labels_to_integers(self.G)
|
||||
(fd, fname) = tempfile.mkstemp()
|
||||
eg.write_edgelist(G, fname)
|
||||
H = eg.read_edgelist(fname, nodetype=int)
|
||||
# isolated nodes are not written in edgelist
|
||||
G.remove_nodes_from(list(eg.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()
|
||||
eg.write_edgelist(G, fname)
|
||||
H = eg.read_edgelist(fname, nodetype=int, create_using=eg.MultiGraph())
|
||||
H2 = eg.read_edgelist(fname, nodetype=int, create_using=eg.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), need_data=False)
|
||||
os.close(fd)
|
||||
os.unlink(fname)
|
||||
|
||||
def test_edgelist_multidigraph(self):
|
||||
G = self.XDG
|
||||
(fd, fname) = tempfile.mkstemp()
|
||||
eg.write_edgelist(G, fname)
|
||||
H = eg.read_edgelist(fname, nodetype=int, create_using=eg.MultiDiGraph())
|
||||
H2 = eg.read_edgelist(fname, nodetype=int, create_using=eg.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), need_data=False)
|
||||
os.close(fd)
|
||||
os.unlink(fname)
|
||||
@@ -0,0 +1,447 @@
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
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 = eg.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 = eg.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 = eg.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 = eg.read_gexf(self.simple_directed_fh)
|
||||
assert sorted(G.nodes) == sorted(H.nodes)
|
||||
assert sorted(G.edges) == sorted(H.edges)
|
||||
self.simple_directed_fh.seek(0)
|
||||
|
||||
def test_write_read_simple_directed_graphml(self):
|
||||
G = self.simple_directed_graph
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_gexf(fh)
|
||||
assert sorted(G.nodes) == sorted(H.nodes)
|
||||
assert sorted(G.edges) == sorted(H.edges)
|
||||
self.simple_directed_fh.seek(0)
|
||||
|
||||
def test_read_simple_undirected_graphml(self):
|
||||
G = self.simple_undirected_graph
|
||||
H = eg.read_gexf(self.simple_undirected_fh)
|
||||
assert sorted(G.nodes) == sorted(H.nodes)
|
||||
assert sorted(G.edges) == sorted(H.edges)
|
||||
self.simple_undirected_fh.seek(0)
|
||||
|
||||
def test_read_attribute_graphml(self):
|
||||
G = self.attribute_graph
|
||||
H = eg.read_gexf(self.attribute_fh)
|
||||
assert sorted(G.nodes) == sorted(H.nodes)
|
||||
ge = sorted(G.edges)
|
||||
he = sorted(H.edges)
|
||||
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(eg.EasyGraphError, eg.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(eg.EasyGraphError, eg.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(eg.EasyGraphError, eg.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 = eg.read_gexf(fh, relabel=True)
|
||||
assert sorted(G.nodes) == ["Hello", "Word"]
|
||||
|
||||
def test_default_attribute(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1, label="1", color="green")
|
||||
eg.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()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_gexf(fh, node_type=int)
|
||||
assert sorted(G.nodes) == sorted(H.nodes)
|
||||
# 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 = eg.Graph()
|
||||
G.add_node(1, id=7, label=77)
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_gexf(fh, node_type=int)
|
||||
assert list(H) == [7]
|
||||
assert H.nodes[7]["label"] == "77"
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires >= python3.8")
|
||||
def test_edge_id_construct(self):
|
||||
G = eg.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>EasyGraph</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(eg.generate_gexf(G))
|
||||
assert expected == obtained
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires >= python3.8")
|
||||
def test_numpy_type(self):
|
||||
np = pytest.importorskip("numpy")
|
||||
G = eg.path_graph(4)
|
||||
eg.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>EasyGraph</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(eg.generate_gexf(G))
|
||||
assert expected == obtained
|
||||
|
||||
def test_bool(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1, testattr=True)
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_gexf(fh, node_type=int)
|
||||
assert H.nodes[1]["testattr"]
|
||||
|
||||
def test_specials(self):
|
||||
from math import isnan
|
||||
|
||||
inf, nan = float("inf"), float("nan")
|
||||
G = eg.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()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
filetext = fh.read()
|
||||
fh.seek(0)
|
||||
H = eg.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]["easygraph_key"] == "a"
|
||||
assert H.nodes[2]["easygraph_key"] == "b"
|
||||
assert H.nodes[3]["easygraph_key"] == "c"
|
||||
|
||||
def test_simple_list(self):
|
||||
G = eg.Graph()
|
||||
list_value = [(1, 2, 3), (9, 1, 2)]
|
||||
G.add_node(1, key=list_value)
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_gexf(fh, node_type=int)
|
||||
assert H.nodes[1]["easygraph_key"] == list_value
|
||||
|
||||
def test_dynamic_mode(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1, label="1", color="green")
|
||||
G.graph["mode"] = "dynamic"
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.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 = eg.Graph()
|
||||
G.add_node(0, label="1", color="green")
|
||||
G.nodes[0]["spells"] = [(1, 2)]
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.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 = eg.Graph()
|
||||
G.add_node(0, label="1", color="green")
|
||||
G.nodes[0]["slices"] = [(1, 2)]
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh, version="1.1draft")
|
||||
fh.seek(0)
|
||||
H = eg.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 = eg.Graph()
|
||||
G.add_node(0, label="1", color="green", parents=[1, 2])
|
||||
fh = io.BytesIO()
|
||||
eg.write_gexf(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.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)
|
||||
@@ -0,0 +1,589 @@
|
||||
import codecs
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from ast import literal_eval
|
||||
from contextlib import contextmanager
|
||||
from textwrap import dedent
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.readwrite.gml import literal_destringizer
|
||||
from easygraph.readwrite.gml import 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"
|
||||
]
|
||||
]
|
||||
"""
|
||||
eg.parse_gml(cytoscape_example)
|
||||
|
||||
def test_parse_gml(self):
|
||||
G = eg.parse_gml(self.simple_data, label="label")
|
||||
assert sorted(G.nodes) == ["Node 1", "Node 2", "Node 3"]
|
||||
assert [e[:2] for e in sorted(G.edges)] == [
|
||||
("Node 1", "Node 2"),
|
||||
("Node 2", "Node 3"),
|
||||
("Node 3", "Node 1"),
|
||||
]
|
||||
|
||||
assert [e for e in sorted(G.edges)] == [
|
||||
(
|
||||
"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 = eg.read_gml(fname, label="label")
|
||||
G = eg.parse_gml(self.simple_data, label="label")
|
||||
assert sorted(G.nodes) == sorted(Gin.nodes)
|
||||
assert sorted(G.edges) == sorted(Gin.edges)
|
||||
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 = eg.Graph()
|
||||
G.add_node(1203)
|
||||
data = "\n".join(eg.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(eg.EasyGraphError, eg.read_gml, fh, label="label")
|
||||
|
||||
def test_quotes(self):
|
||||
G = eg.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()
|
||||
eg.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 "quoted" and this is a copyright: ©"
|
||||
]
|
||||
]"""
|
||||
assert data == answer
|
||||
|
||||
def test_unicode_node(self):
|
||||
node = "node" + chr(169)
|
||||
G = eg.Graph()
|
||||
G.add_node(node)
|
||||
fobj = tempfile.NamedTemporaryFile()
|
||||
eg.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©"
|
||||
]
|
||||
]"""
|
||||
assert data == answer
|
||||
|
||||
def test_float_label(self):
|
||||
node = 1.0
|
||||
G = eg.Graph()
|
||||
G.add_node(node)
|
||||
fobj = tempfile.NamedTemporaryFile()
|
||||
eg.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_name(self):
|
||||
G = eg.parse_gml('graph [ name "x" node [ id 0 label "x" ] ]')
|
||||
assert "x" == G.graph["name"]
|
||||
G = eg.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 = eg.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(eg.generate_gml(G))
|
||||
|
||||
def test_data_types(self):
|
||||
data = [
|
||||
True,
|
||||
False,
|
||||
10**20,
|
||||
-2e33,
|
||||
"'",
|
||||
'"&&&""',
|
||||
[{(b"\xfd",): "\x7f", chr(0x4444): (1, 2)}, (2, "3")],
|
||||
]
|
||||
data.append(chr(0x14444))
|
||||
data.append(literal_eval("{2.3j, 1 - 2.3j, ()}"))
|
||||
G = eg.Graph()
|
||||
G.name = data
|
||||
G.graph["data"] = data
|
||||
G.add_node(0, int=-1, data=dict(data=data))
|
||||
G.add_edge(0, 0, float=-2.5, data=data)
|
||||
gml = "\n".join(eg.generate_gml(G, stringizer=literal_stringizer))
|
||||
G = eg.parse_gml(gml, destringizer=literal_destringizer)
|
||||
assert data == G.name
|
||||
assert {"name": data, "data": data} == G.graph
|
||||
assert G.nodes == {0: dict(int=-1, data=dict(data=data))}
|
||||
assert list(G.edges) == [(0, 0, dict(float=-2.5, data=data))]
|
||||
G = eg.Graph()
|
||||
G.graph["data"] = "frozenset([1, 2, 3])"
|
||||
G = eg.parse_gml(eg.generate_gml(G), destringizer=literal_eval)
|
||||
assert G.graph["data"] == "frozenset([1, 2, 3])"
|
||||
|
||||
def test_escape_unescape(self):
|
||||
gml = """graph [
|
||||
name "&"䑄��&unknown;"
|
||||
]"""
|
||||
G = eg.parse_gml(gml)
|
||||
assert (
|
||||
'&"\x0f' + chr(0x4444) + "��&unknown;"
|
||||
== G.name
|
||||
)
|
||||
gml = "\n".join(eg.generate_gml(G))
|
||||
alnu = "#1234567890;&#x1234567890abcdef"
|
||||
answer = (
|
||||
"""graph [
|
||||
name "&"䑄&"""
|
||||
+ alnu
|
||||
+ """;&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(eg.EasyGraphError, eg.read_gml, f)
|
||||
|
||||
def assert_parse_error(gml):
|
||||
pytest.raises(eg.EasyGraphError, eg.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 ] ]")
|
||||
eg.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 ] ]")
|
||||
eg.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 ] ]"
|
||||
)
|
||||
eg.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 ]"
|
||||
)
|
||||
eg.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 ]"
|
||||
)
|
||||
eg.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 ]"
|
||||
)
|
||||
eg.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
|
||||
eg.parse_gml("graph [edge [ source a target a ] node [ id a label b ] ]")
|
||||
eg.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(
|
||||
eg.EasyGraphError, lambda: list(eg.generate_gml(*args, **kwargs))
|
||||
)
|
||||
|
||||
G = eg.Graph()
|
||||
G.graph[3] = 3
|
||||
assert_generate_error(G)
|
||||
G = eg.Graph()
|
||||
G.graph["3"] = 3
|
||||
assert_generate_error(G)
|
||||
G = eg.Graph()
|
||||
G.graph["data"] = frozenset([1, 2, 3])
|
||||
assert_generate_error(G, stringizer=literal_stringizer)
|
||||
G = eg.Graph()
|
||||
G.graph["data"] = []
|
||||
assert_generate_error(G)
|
||||
assert_generate_error(G, stringizer=len)
|
||||
|
||||
def test_label_kwarg(self):
|
||||
G = eg.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 = eg.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 = eg.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:
|
||||
eg.write_gml(G, fname)
|
||||
# Check that the export wrote the nonfitting numbers as strings
|
||||
G2 = eg.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)
|
||||
|
||||
|
||||
@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 = eg.Graph()
|
||||
g.add_node("n1", properties=["element", 0, 1, 2.5, True, False])
|
||||
with byte_file() as f:
|
||||
eg.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 = eg.Graph()
|
||||
g.add_node("n1", properties=["element"])
|
||||
with byte_file() as f:
|
||||
eg.write_gml(g, f)
|
||||
result = f.read().decode()
|
||||
|
||||
assert result == dedent(
|
||||
"""\
|
||||
graph [
|
||||
node [
|
||||
id 0
|
||||
label "n1"
|
||||
properties "_easygraph_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 = eg.read_gml(f)
|
||||
assert graph.nodes["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 "_easygraph_list_start"
|
||||
properties "element"
|
||||
]
|
||||
]
|
||||
"""
|
||||
).encode("ascii")
|
||||
)
|
||||
f.seek(0)
|
||||
graph = eg.read_gml(f)
|
||||
assert graph.nodes["n1"] == {"properties": ["element"]}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
pygraphviz = pytest.importorskip("pygraphviz")
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import edges_equal
|
||||
from easygraph.utils import nodes_equal
|
||||
|
||||
|
||||
class TestAGraph:
|
||||
def build_graph(self, G):
|
||||
edges = [("A", "B"), ("A", "C"), ("A", "C"), ("B", "C"), ("A", "D")]
|
||||
G.add_edges_from(edges)
|
||||
G.add_node("E")
|
||||
G.graph["metal"] = "bronze"
|
||||
return G
|
||||
|
||||
def assert_equal(self, G1, G2):
|
||||
assert nodes_equal(G1.nodes, G2.nodes)
|
||||
assert edges_equal(G1.edges, G2.edges)
|
||||
assert G1.graph["metal"] == G2.graph["metal"]
|
||||
|
||||
def agraph_checks(self, G):
|
||||
G = self.build_graph(G)
|
||||
A = eg.to_agraph(G)
|
||||
H = eg.from_agraph(A)
|
||||
self.assert_equal(G, H)
|
||||
|
||||
fd, fname = tempfile.mkstemp()
|
||||
eg.write_dot(H, fname)
|
||||
Hin = eg.read_dot(fname)
|
||||
self.assert_equal(H, Hin)
|
||||
os.close(fd)
|
||||
os.unlink(fname)
|
||||
|
||||
(fd, fname) = tempfile.mkstemp()
|
||||
with open(fname, "w") as fh:
|
||||
eg.write_dot(H, fh)
|
||||
|
||||
with open(fname) as fh:
|
||||
Hin = eg.read_dot(fh)
|
||||
os.close(fd)
|
||||
os.unlink(fname)
|
||||
self.assert_equal(H, Hin)
|
||||
|
||||
def test_from_agraph_name(self):
|
||||
G = eg.Graph(name="test")
|
||||
A = eg.to_agraph(G)
|
||||
H = eg.from_agraph(A)
|
||||
assert G.name == "test"
|
||||
|
||||
def test_undirected(self):
|
||||
self.agraph_checks(eg.Graph())
|
||||
@@ -0,0 +1,307 @@
|
||||
# # This file is part of the NetworkX distribution.
|
||||
|
||||
# NetworkX is distributed with the 3-clause BSD license.
|
||||
|
||||
|
||||
# ::
|
||||
# Copyright (C) 2004-2022, NetworkX Developers
|
||||
# Aric Hagberg <hagberg@lanl.gov>
|
||||
# Dan Schult <dschult@colgate.edu>
|
||||
# Pieter Swart <swart@lanl.gov>
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
|
||||
# * Neither the name of the NetworkX Developers nor the names of its
|
||||
# contributors may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
Pajek tests
|
||||
"""
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
print(eg)
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from easygraph.utils import edges_equal
|
||||
from easygraph.utils import nodes_equal
|
||||
|
||||
|
||||
# from rich import print
|
||||
|
||||
test_parse_pajek_edges = [
|
||||
(
|
||||
"A1",
|
||||
"A1",
|
||||
0,
|
||||
{
|
||||
"weight": 1.0,
|
||||
"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",
|
||||
},
|
||||
),
|
||||
(
|
||||
"A1",
|
||||
"Bb",
|
||||
0,
|
||||
{
|
||||
"weight": 1.0,
|
||||
"h2": "0",
|
||||
"a1": "40",
|
||||
"k1": "2.8",
|
||||
"a2": "30",
|
||||
"k2": "0.8",
|
||||
"ap": "25",
|
||||
"l": "Bezier arc",
|
||||
"lphi": "90",
|
||||
"la": "0",
|
||||
"lp": "0.65",
|
||||
},
|
||||
),
|
||||
(
|
||||
"A1",
|
||||
"C",
|
||||
0,
|
||||
{
|
||||
"weight": 1.0,
|
||||
"p": "Dashed",
|
||||
"h2": "0",
|
||||
"w": "5",
|
||||
"k1": "-1",
|
||||
"k2": "-20",
|
||||
"ap": "25",
|
||||
"l": "Oval arc",
|
||||
"c": "Brown",
|
||||
"lc": "Black",
|
||||
},
|
||||
),
|
||||
(
|
||||
"Bb",
|
||||
"A1",
|
||||
0,
|
||||
{
|
||||
"weight": 1.0,
|
||||
"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",
|
||||
},
|
||||
),
|
||||
(
|
||||
"C",
|
||||
"D2",
|
||||
0,
|
||||
{
|
||||
"weight": 1.0,
|
||||
"p": "Dashed",
|
||||
"h2": "0",
|
||||
"w": "2",
|
||||
"c": "OliveGreen",
|
||||
"ap": "25",
|
||||
"l": "Straight arc",
|
||||
"lc": "PineGreen",
|
||||
},
|
||||
),
|
||||
(
|
||||
"C",
|
||||
"C",
|
||||
0,
|
||||
{
|
||||
"weight": -1.0,
|
||||
"h1": "6",
|
||||
"w": "1",
|
||||
"h2": "12",
|
||||
"k1": "-2",
|
||||
"k2": "-15",
|
||||
"ap": "0.5",
|
||||
"l": "Circular loop",
|
||||
"c": "Red",
|
||||
"lc": "OrangeRed",
|
||||
"lphi": "270",
|
||||
"la": "180",
|
||||
},
|
||||
),
|
||||
(
|
||||
"D2",
|
||||
"Bb",
|
||||
0,
|
||||
{
|
||||
"weight": -1.0,
|
||||
"h2": "0",
|
||||
"w": "1",
|
||||
"k1": "-2",
|
||||
"k2": "250",
|
||||
"ap": "25",
|
||||
"l": "Circular arc",
|
||||
"c": "Red",
|
||||
"lc": "OrangeRed",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
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 = eg.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 = eg.parse_pajek(data)
|
||||
assert sorted(G.nodes) == ["1", "2"]
|
||||
assert edges_equal(G.edges, [("1", "2", 0, {}), ("1", "2", 1, {})])
|
||||
|
||||
def test_parse_pajek(self):
|
||||
G = eg.parse_pajek(self.data)
|
||||
assert sorted(G.nodes) == ["A1", "Bb", "C", "D2"]
|
||||
# print(G.edges)
|
||||
assert edges_equal(G.edges, test_parse_pajek_edges)
|
||||
|
||||
def test_parse_pajek_mat(self):
|
||||
data = """*Vertices 3\n1 "one"\n2 "two"\n3 "three"\n*Matrix\n1 1 0\n0 1 0\n0 1 0\n"""
|
||||
G = eg.parse_pajek(data)
|
||||
assert set(G.nodes) == {"one", "two", "three"}
|
||||
assert G.nodes["two"] == {"id": "2"}
|
||||
assert edges_equal(
|
||||
# set(G.edges),
|
||||
G.edges,
|
||||
[
|
||||
("one", "one", {"weight": 1}),
|
||||
("one", "two", {"weight": 1}),
|
||||
("two", "two", {"weight": 1}),
|
||||
("three", "two", {"weight": 1}),
|
||||
],
|
||||
)
|
||||
|
||||
def test_read_pajek(self):
|
||||
G = eg.parse_pajek(self.data)
|
||||
Gin = eg.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 = eg.parse_pajek(self.data)
|
||||
fh = io.BytesIO()
|
||||
eg.write_pajek(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_pajek(fh)
|
||||
assert nodes_equal(G.nodes, list(H))
|
||||
assert edges_equal(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 = eg.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:
|
||||
eg.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 = eg.parse_pajek(data)
|
||||
|
||||
def test_unicode(self):
|
||||
import io
|
||||
|
||||
G = eg.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()
|
||||
eg.write_pajek(G, fh)
|
||||
fh.seek(0)
|
||||
H = eg.read_pajek(fh)
|
||||
assert nodes_equal(list(G), list(H))
|
||||
# from icecream import ic
|
||||
# ic(G.edges)
|
||||
# ic(H.edges)
|
||||
# ic(G.graph)
|
||||
# ic(H.graph)
|
||||
# assert edges_equal(list(G.edges), list(H.edges))
|
||||
assert G.graph == H.graph
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pickle read / write tests
|
||||
"""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import tempfile
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import edges_equal
|
||||
|
||||
|
||||
class TestPickle:
|
||||
@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 = eg.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(pickle.dumps(cls.G))
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
os.unlink(cls.fname)
|
||||
|
||||
def test_read_pickle(self):
|
||||
G = eg.read_pickle(self.fname)
|
||||
assert G.nodes == self.G.nodes
|
||||
assert G.edges == self.G.edges
|
||||
|
||||
def test_write_pickle(self):
|
||||
G = eg.parse_pajek(self.data)
|
||||
eg.write_pickle(self.fname, G)
|
||||
Gin = eg.read_pickle(self.fname)
|
||||
assert sorted(G.nodes) == sorted(Gin.nodes)
|
||||
assert edges_equal(G.edges, Gin.edges)
|
||||
assert self.G.graph == Gin.graph
|
||||
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
UCINET tests
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
# from nose import SkipTest
|
||||
# from nose.tools import *
|
||||
|
||||
|
||||
def filterEdges(edges):
|
||||
return [e[:3] for e in edges]
|
||||
|
||||
|
||||
class TestUcinet:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
self.G = eg.MultiDiGraph()
|
||||
self.G.add_nodes_from(["a", "b", "c", "d", "e"])
|
||||
self.G.add_edges_from(
|
||||
[
|
||||
("a", "b"),
|
||||
("a", "c"),
|
||||
("a", "d"),
|
||||
("a", "e"),
|
||||
("b", "a"),
|
||||
("b", "c"),
|
||||
("b", "d"),
|
||||
("c", "a"),
|
||||
("c", "b"),
|
||||
("d", "a"),
|
||||
("d", "b"),
|
||||
("e", "a"),
|
||||
]
|
||||
)
|
||||
try:
|
||||
pass
|
||||
except ImportError:
|
||||
print("NumPy not available.")
|
||||
# raise SkipTest("NumPy not available.")
|
||||
|
||||
def test_generate_ucinet(self):
|
||||
Gout = eg.generate_ucinet(self.G)
|
||||
s = ""
|
||||
for line in Gout:
|
||||
s += line + "\n"
|
||||
G_generated = eg.parse_ucinet(s)
|
||||
|
||||
data = """\
|
||||
dl n=5 format=fullmatrix
|
||||
labels:
|
||||
a,b,c,d,e
|
||||
data:
|
||||
0 1 1 1 1
|
||||
1 0 1 1 0
|
||||
1 1 0 0 0
|
||||
1 1 0 0 0
|
||||
1 0 0 0 0"""
|
||||
G = eg.parse_ucinet(data)
|
||||
assert sorted(G.nodes) == sorted(G_generated.nodes)
|
||||
assert sorted(G.edges) == sorted(G_generated.edges)
|
||||
|
||||
def test_parse_ucinet(self):
|
||||
data = """
|
||||
DL N = 5
|
||||
Data:
|
||||
0 1 1 1 1
|
||||
1 0 1 0 0
|
||||
1 1 0 0 1
|
||||
1 0 0 0 0
|
||||
1 0 1 0 0
|
||||
"""
|
||||
graph = eg.MultiDiGraph()
|
||||
graph.add_nodes_from([0, 1, 2, 3, 4])
|
||||
graph.add_edges_from(
|
||||
[
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(0, 4),
|
||||
(1, 0),
|
||||
(1, 2),
|
||||
(2, 0),
|
||||
(2, 1),
|
||||
(2, 4),
|
||||
(3, 0),
|
||||
(4, 0),
|
||||
(4, 2),
|
||||
]
|
||||
)
|
||||
G = eg.parse_ucinet(data)
|
||||
assert sorted(G.nodes) == sorted(graph.nodes)
|
||||
assert sorted(filterEdges(G.edges)) == sorted(filterEdges(graph.edges))
|
||||
# print [n for n in G.nodes(data=True)]
|
||||
# print [e for e in G.edges]
|
||||
|
||||
def test_parse_ucinet_labels(self):
|
||||
"""
|
||||
Test parsing of labels : single line (data1), multiple lines (data2), embedded (data3)
|
||||
Labels must be separated by spaces, carriage returns, equal signs or commas.
|
||||
Labels with embedded spaces are not advisable, but can be entered by
|
||||
surrounding the label in quotes (e.g., "Humpty Dumpty").
|
||||
"""
|
||||
data1 = """
|
||||
dl n=5
|
||||
format = fullmatrix
|
||||
labels:
|
||||
barry,david,lin,pat,russ
|
||||
data:
|
||||
0 1 1 1 0
|
||||
1 0 0 0 1
|
||||
1 0 0 1 0
|
||||
1 0 1 0 1
|
||||
0 1 0 1 0
|
||||
"""
|
||||
data2 = """
|
||||
dl n=5
|
||||
format = fullmatrix
|
||||
labels:
|
||||
barry,david
|
||||
lin,pat
|
||||
russ
|
||||
data:
|
||||
0 1 1 1 0
|
||||
1 0 0 0 1
|
||||
1 0 0 1 0
|
||||
1 0 1 0 1
|
||||
0 1 0 1 0
|
||||
"""
|
||||
data3 = """\
|
||||
dl n=5
|
||||
format = fullmatrix
|
||||
labels embedded
|
||||
data:
|
||||
barry david lin pat russ
|
||||
Barry 0 1 1 1 0
|
||||
david 1 0 0 0 1
|
||||
Lin 1 0 0 1 0
|
||||
Pat 1 0 1 0 1
|
||||
Russ 0 1 0 1 0
|
||||
"""
|
||||
G = eg.MultiDiGraph()
|
||||
G.add_nodes_from(["russ", "barry", "lin", "pat", "david"])
|
||||
G.add_edges_from(
|
||||
[
|
||||
("russ", "pat"),
|
||||
("russ", "david"),
|
||||
("barry", "lin"),
|
||||
("barry", "pat"),
|
||||
("barry", "david"),
|
||||
("lin", "barry"),
|
||||
("lin", "pat"),
|
||||
("pat", "barry"),
|
||||
("pat", "lin"),
|
||||
("pat", "russ"),
|
||||
("david", "barry"),
|
||||
("david", "russ"),
|
||||
]
|
||||
)
|
||||
G1 = eg.parse_ucinet(data1)
|
||||
G2 = eg.parse_ucinet(data2)
|
||||
G3 = eg.parse_ucinet(data3)
|
||||
assert sorted(G1.nodes) == sorted(G.nodes)
|
||||
assert sorted(G2.nodes) == sorted(G.nodes)
|
||||
assert sorted(G3.nodes) == sorted(G.nodes)
|
||||
assert sorted(e[:3] for e in G1.edges) == sorted(e[:3] for e in G.edges)
|
||||
assert sorted(e[:3] for e in G2.edges) == sorted(e[:3] for e in G.edges)
|
||||
assert sorted(e[:3] for e in G3.edges) == sorted(e[:3] for e in G.edges)
|
||||
# print [n for n in G.nodes]
|
||||
# print [e for e in G.edges]
|
||||
|
||||
def test_parse_ucinet_nodelist1(self):
|
||||
data1 = """
|
||||
DL n=4
|
||||
format = nodelist1
|
||||
data:
|
||||
1 3 2 1
|
||||
4 1 4
|
||||
2 2 4 1
|
||||
"""
|
||||
data2 = """
|
||||
DL n=4
|
||||
format = nodelist1b
|
||||
data:
|
||||
3 1 2 3
|
||||
3 1 2 4
|
||||
0
|
||||
2 1 4
|
||||
"""
|
||||
G = eg.MultiDiGraph()
|
||||
G.add_nodes_from([0, 1, 2, 3])
|
||||
G.add_edges_from(
|
||||
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 3), (3, 0), (3, 3)]
|
||||
)
|
||||
G1 = eg.parse_ucinet(data1)
|
||||
G2 = eg.parse_ucinet(data2)
|
||||
assert sorted(G1.nodes) == sorted(G.nodes)
|
||||
assert sorted(G2.nodes) == sorted(G.nodes)
|
||||
assert sorted(filterEdges(G1.edges)) == sorted(filterEdges(G.edges))
|
||||
assert sorted(filterEdges(G2.edges)) == sorted(filterEdges(G.edges))
|
||||
|
||||
def test_parse_ucinet_nodelist1_labels(self):
|
||||
data1 = """
|
||||
DL n=5
|
||||
format = nodelist1
|
||||
labels:
|
||||
george, sally, jim, billy, jane
|
||||
data:
|
||||
1 2 3
|
||||
2 3
|
||||
4 1
|
||||
5 3
|
||||
"""
|
||||
data2 = """
|
||||
DL n=5
|
||||
format = nodelist1
|
||||
labels embedded:
|
||||
data:
|
||||
george sally jim
|
||||
sally jim
|
||||
billy george
|
||||
jane jim
|
||||
"""
|
||||
G = eg.MultiDiGraph()
|
||||
G.add_nodes_from(["george", "sally", "jim", "billy", "jane"])
|
||||
G.add_edges_from(
|
||||
[
|
||||
("billy", "george"),
|
||||
("jane", "jim"),
|
||||
("sally", "jim"),
|
||||
("george", "jim"),
|
||||
("george", "sally"),
|
||||
]
|
||||
)
|
||||
G1 = eg.parse_ucinet(data1)
|
||||
G2 = eg.parse_ucinet(data2)
|
||||
assert sorted(G1.nodes) == sorted(G.nodes)
|
||||
assert sorted(G2.nodes) == sorted(G.nodes)
|
||||
assert sorted(G1.edges) == sorted(G.edges)
|
||||
assert sorted(G2.edges) == sorted(G.edges)
|
||||
|
||||
def test_read_ucinet(self):
|
||||
fh = io.BytesIO()
|
||||
data = """
|
||||
DL N = 5
|
||||
Data:
|
||||
0 1 1 1 1
|
||||
1 0 1 0 0
|
||||
1 1 0 0 1
|
||||
1 0 0 0 0
|
||||
1 0 1 0 0
|
||||
"""
|
||||
Gin = eg.parse_ucinet(data)
|
||||
fh.write(data.encode("UTF-8"))
|
||||
fh.seek(0)
|
||||
Gout = eg.read_ucinet(fh)
|
||||
assert sorted(Gout.nodes) == sorted(Gin.nodes)
|
||||
assert sorted(e[:3] for e in Gout.edges) == sorted(e[:3] for e in Gin.edges)
|
||||
|
||||
def test_write_ucinet(self):
|
||||
fh = io.BytesIO()
|
||||
data = """\
|
||||
dl n=5 format=fullmatrix
|
||||
data:
|
||||
0 1 1 1 1
|
||||
1 0 1 0 0
|
||||
1 1 0 0 1
|
||||
1 0 0 0 0
|
||||
1 0 1 0 0
|
||||
"""
|
||||
graph = eg.MultiDiGraph()
|
||||
graph.add_nodes_from([0, 1, 2, 3, 4])
|
||||
graph.add_edges_from(
|
||||
[
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(0, 4),
|
||||
(1, 0),
|
||||
(1, 2),
|
||||
(2, 0),
|
||||
(2, 1),
|
||||
(2, 4),
|
||||
(3, 0),
|
||||
(4, 0),
|
||||
(4, 2),
|
||||
]
|
||||
)
|
||||
|
||||
eg.write_ucinet(graph, fh)
|
||||
fh.seek(0)
|
||||
G = eg.parse_ucinet(fh.readlines())
|
||||
assert sorted(G.nodes) == sorted(graph.nodes)
|
||||
assert sorted(e[:3] for e in G.edges) == sorted(e[:3] for e in graph.edges)
|
||||
|
||||
def test_parse_ucinet_edgelist1(self):
|
||||
data1 = """
|
||||
DL n=5
|
||||
format = edgelist1
|
||||
labels:
|
||||
george, sally, jim, billy, jane
|
||||
data:
|
||||
1 2
|
||||
1 3
|
||||
2 3
|
||||
3 1
|
||||
5 4
|
||||
"""
|
||||
data2 = """
|
||||
DL n=5
|
||||
format = edgelist1
|
||||
labels embedded:
|
||||
data:
|
||||
george sally
|
||||
george jim
|
||||
sally jim
|
||||
jim george
|
||||
jane billy
|
||||
"""
|
||||
G = eg.MultiDiGraph()
|
||||
G.add_nodes_from(["george", "sally", "jim", "billy", "jane"])
|
||||
G.add_edges_from(
|
||||
[
|
||||
("jim", "george"),
|
||||
("jane", "billy"),
|
||||
("sally", "jim"),
|
||||
("george", "jim"),
|
||||
("george", "sally"),
|
||||
]
|
||||
)
|
||||
|
||||
G1 = eg.parse_ucinet(data1)
|
||||
G2 = eg.parse_ucinet(data2)
|
||||
assert sorted(G1.nodes) == sorted(G.nodes)
|
||||
assert sorted(G2.nodes) == sorted(G.nodes)
|
||||
assert sorted(G1.edges) == sorted(G.edges)
|
||||
assert sorted(G2.edges) == sorted(G.edges)
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
**************
|
||||
UCINET DL
|
||||
**************
|
||||
Read and write graphs in UCINET DL format.
|
||||
This implementation currently supports only the 'fullmatrix' data format.
|
||||
Format
|
||||
------
|
||||
The UCINET DL format is the most common file format used by UCINET package.
|
||||
Basic example:
|
||||
DL N = 5
|
||||
Data:
|
||||
0 1 1 1 1
|
||||
1 0 1 0 0
|
||||
1 1 0 0 1
|
||||
1 0 0 0 0
|
||||
1 0 1 0 0
|
||||
References
|
||||
----------
|
||||
See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm
|
||||
for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils import open_file
|
||||
|
||||
|
||||
__all__ = ["generate_ucinet", "read_ucinet", "parse_ucinet", "write_ucinet"]
|
||||
|
||||
|
||||
def generate_ucinet(G):
|
||||
"""Generate lines in UCINET graph format.
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
Examples
|
||||
--------
|
||||
Notes
|
||||
-----
|
||||
The default format 'fullmatrix' is used (for UCINET DL format).
|
||||
|
||||
References
|
||||
----------
|
||||
See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm
|
||||
for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm
|
||||
"""
|
||||
|
||||
n = G.number_of_nodes()
|
||||
nodes = sorted(list(G.nodes))
|
||||
yield "dl n=%i format=fullmatrix" % n
|
||||
|
||||
# Labels
|
||||
try:
|
||||
int(nodes[0])
|
||||
except ValueError:
|
||||
s = "labels:\n"
|
||||
for label in nodes:
|
||||
s += label + " "
|
||||
yield s
|
||||
|
||||
yield "data:"
|
||||
|
||||
yield str(np.asmatrix(eg.to_numpy_array(G, nodelist=nodes, dtype=int))).replace(
|
||||
"[", " "
|
||||
).replace("]", " ").lstrip().rstrip()
|
||||
|
||||
|
||||
@open_file(0, mode="rb")
|
||||
def read_ucinet(path, encoding="UTF-8"):
|
||||
"""Read graph in UCINET format from path.
|
||||
Parameters
|
||||
----------
|
||||
path : file or string
|
||||
File or filename to read.
|
||||
Filenames ending in .gz or .bz2 will be uncompressed.
|
||||
Returns
|
||||
-------
|
||||
G : EasyGraph MultiGraph or MultiDiGraph.
|
||||
Examples
|
||||
--------
|
||||
>>> G=eg.path_graph(4)
|
||||
>>> eg.write_ucinet(G, "test.dl")
|
||||
>>> G=eg.read_ucinet("test.dl")
|
||||
To create a Graph instead of a MultiGraph use
|
||||
>>> G1=eg.Graph(G)
|
||||
See Also
|
||||
--------
|
||||
parse_ucinet()
|
||||
References
|
||||
----------
|
||||
See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm
|
||||
for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm
|
||||
"""
|
||||
lines = (line.decode(encoding) for line in path)
|
||||
return parse_ucinet(lines)
|
||||
|
||||
|
||||
@open_file(1, mode="wb")
|
||||
def write_ucinet(G, path, encoding="UTF-8"):
|
||||
"""Write graph in UCINET format to path.
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
path : file or string
|
||||
File or filename to write.
|
||||
Filenames ending in .gz or .bz2 will be compressed.
|
||||
Examples
|
||||
--------
|
||||
>>> G=eg.path_graph(4)
|
||||
>>> eg.write_ucinet(G, "test.net")
|
||||
References
|
||||
----------
|
||||
See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm
|
||||
for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm
|
||||
"""
|
||||
for line in generate_ucinet(G):
|
||||
line += "\n"
|
||||
path.write(line.encode(encoding))
|
||||
|
||||
|
||||
def parse_ucinet(lines):
|
||||
"""Parse UCINET format graph from string or iterable.
|
||||
Currently only the 'fullmatrix', 'nodelist1' and 'nodelist1b' formats are supported.
|
||||
Parameters
|
||||
----------
|
||||
lines : string or iterable
|
||||
Data in UCINET format.
|
||||
Returns
|
||||
-------
|
||||
G : EasyGraph graph
|
||||
See Also
|
||||
--------
|
||||
read_ucinet()
|
||||
References
|
||||
----------
|
||||
See UCINET User Guide or http://www.analytictech.com/ucinet/help/hs5000.htm
|
||||
for full format information. Short version on http://www.analytictech.com/networks/dataentry.htm
|
||||
"""
|
||||
from numpy import genfromtxt
|
||||
from numpy import isnan
|
||||
from numpy import reshape
|
||||
|
||||
G = eg.MultiDiGraph()
|
||||
|
||||
if not isinstance(lines, str):
|
||||
s = ""
|
||||
for line in lines:
|
||||
if type(line) == bytes:
|
||||
s += line.decode("utf-8")
|
||||
else:
|
||||
s += line
|
||||
lines = s
|
||||
lexer = shlex.shlex(lines.lower())
|
||||
lexer.whitespace += ",="
|
||||
lexer.whitespace_split = True
|
||||
|
||||
number_of_nodes = 0
|
||||
number_of_matrices = 0
|
||||
nr = 0 # number of rows (rectangular matrix)
|
||||
nc = 0 # number of columns (rectangular matrix)
|
||||
ucinet_format = "fullmatrix" # Format by default
|
||||
labels = {} # Contains labels of nodes
|
||||
row_labels_embedded = False # Whether labels are embedded in data or not
|
||||
cols_labels_embedded = False
|
||||
diagonal = True # whether the main diagonal is present or absent
|
||||
|
||||
KEYWORDS = ("format", "data:", "labels:") # TODO remove ':' in keywords
|
||||
|
||||
while lexer:
|
||||
try:
|
||||
token = next(lexer)
|
||||
except StopIteration:
|
||||
break
|
||||
# print "Token : %s" % token
|
||||
if token.startswith("n"):
|
||||
if token.startswith("nr"):
|
||||
nr = int(get_param(r"\d+", token, lexer))
|
||||
number_of_nodes = max(nr, nc)
|
||||
elif token.startswith("nc"):
|
||||
nc = int(get_param(r"\d+", token, lexer))
|
||||
number_of_nodes = max(nr, nc)
|
||||
elif token.startswith("nm"):
|
||||
number_of_matrices = int(get_param(r"\d+", token, lexer))
|
||||
else:
|
||||
number_of_nodes = int(get_param(r"\d+", token, lexer))
|
||||
nr = number_of_nodes
|
||||
nc = number_of_nodes
|
||||
|
||||
elif token.startswith("diagonal"):
|
||||
diagonal = get_param("present|absent", token, lexer)
|
||||
|
||||
elif token.startswith("format"):
|
||||
ucinet_format = get_param(
|
||||
"""^(fullmatrix|upperhalf|lowerhalf|nodelist1|nodelist2|nodelist1b|\
|
||||
edgelist1|edgelist2|blockmatrix|partition)$""",
|
||||
token,
|
||||
lexer,
|
||||
)
|
||||
|
||||
# TODO : row and columns labels
|
||||
elif token.startswith("row"): # Row labels
|
||||
pass
|
||||
elif token.startswith("column"): # Columns labels
|
||||
pass
|
||||
|
||||
elif token.startswith("labels"):
|
||||
token = next(lexer)
|
||||
i = 0
|
||||
while token not in KEYWORDS:
|
||||
if token.startswith("embedded"):
|
||||
row_labels_embedded = True
|
||||
cols_labels_embedded = True
|
||||
break
|
||||
else:
|
||||
labels[i] = token.replace(
|
||||
'"', ""
|
||||
) # for labels with embedded spaces
|
||||
i += 1
|
||||
try:
|
||||
token = next(lexer)
|
||||
except StopIteration:
|
||||
break
|
||||
elif token.startswith("data"):
|
||||
break
|
||||
|
||||
data_lines = lines.lower().split("data:", 1)[1]
|
||||
# Generate edges
|
||||
params = {}
|
||||
if cols_labels_embedded:
|
||||
# params['names'] = True
|
||||
labels = dict(zip(range(0, nc), data_lines.splitlines()[1].split()))
|
||||
# params['skip_header'] = 2 # First character is \n
|
||||
if row_labels_embedded: # Skip first column
|
||||
# TODO rectangular case : labels can differ from rows to columns
|
||||
# params['usecols'] = range(1, nc + 1)
|
||||
pass
|
||||
|
||||
if ucinet_format == "fullmatrix":
|
||||
# In Python3 genfromtxt requires bytes string
|
||||
try:
|
||||
data_lines = bytes(data_lines, "utf-8")
|
||||
except TypeError:
|
||||
pass
|
||||
# Do not use splitlines() because it is not necessarily written as a square matrix
|
||||
data = genfromtxt([data_lines], case_sensitive=False, **params)
|
||||
if cols_labels_embedded or row_labels_embedded:
|
||||
# data = insert(data, 0, float('nan'))
|
||||
data = data[~isnan(data)]
|
||||
mat = reshape(data, (max(number_of_nodes, nr), -1))
|
||||
G = eg.from_numpy_array(mat, create_using=eg.MultiDiGraph())
|
||||
|
||||
elif ucinet_format in (
|
||||
"nodelist1",
|
||||
"nodelist1b",
|
||||
): # Since genfromtxt only accepts square matrix...
|
||||
s = ""
|
||||
for i, line in enumerate(data_lines.splitlines()):
|
||||
row = line.split()
|
||||
if row:
|
||||
if ucinet_format == "nodelist1b" and row[0] == "0":
|
||||
pass
|
||||
else:
|
||||
for neighbor in row[1:]:
|
||||
if ucinet_format == "nodelist1":
|
||||
source = row[0]
|
||||
else:
|
||||
source = str(i)
|
||||
s += source + " " + neighbor + "\n"
|
||||
|
||||
G = eg.parse_edgelist(
|
||||
s.splitlines(),
|
||||
nodetype=str if row_labels_embedded and cols_labels_embedded else int,
|
||||
create_using=eg.MultiDiGraph(),
|
||||
)
|
||||
|
||||
if not row_labels_embedded or not cols_labels_embedded:
|
||||
G = eg.relabel_nodes(G, dict(zip(list(G.nodes), [i - 1 for i in G.nodes])))
|
||||
|
||||
elif ucinet_format == "edgelist1":
|
||||
G = eg.parse_edgelist(
|
||||
data_lines.splitlines(),
|
||||
nodetype=str if row_labels_embedded and cols_labels_embedded else int,
|
||||
create_using=eg.MultiDiGraph(),
|
||||
)
|
||||
|
||||
if not row_labels_embedded or not cols_labels_embedded:
|
||||
G = eg.relabel_nodes(G, dict(zip(list(G.nodes), [i - 1 for i in G.nodes])))
|
||||
|
||||
# Relabel nodes
|
||||
if labels:
|
||||
try:
|
||||
if len(list(G.nodes)) < number_of_nodes:
|
||||
G.add_nodes_from(
|
||||
labels.values() if labels else range(0, number_of_nodes)
|
||||
)
|
||||
G = eg.relabel_nodes(G, labels)
|
||||
except KeyError:
|
||||
pass # Nodes already labelled
|
||||
|
||||
return G
|
||||
|
||||
|
||||
def get_param(regex, token, lines):
|
||||
"""
|
||||
Get a parameter value in UCINET DL file
|
||||
:param regex: string with the regex matching the parameter value
|
||||
:param token: token (string) in which we search for the parameter
|
||||
:param lines: to iterate through the next tokens
|
||||
:return:
|
||||
"""
|
||||
n = token
|
||||
query = re.search(regex, n)
|
||||
while query is None:
|
||||
try:
|
||||
n = next(lines)
|
||||
except StopIteration:
|
||||
raise Exception("Parameter %s value not recognized" % token)
|
||||
query = re.search(regex, n)
|
||||
return query.group()
|
||||
Reference in New Issue
Block a user