515 lines
16 KiB
Python
515 lines
16 KiB
Python
|
#!/usr/bin/env python
|
||
|
# encoding: utf-8
|
||
|
|
||
|
from ast import literal_eval
|
||
|
import codecs
|
||
|
import io
|
||
|
import pytest
|
||
|
import networkx as nx
|
||
|
from networkx.readwrite.gml import literal_stringizer, literal_destringizer
|
||
|
import os
|
||
|
import tempfile
|
||
|
|
||
|
try:
|
||
|
unicode
|
||
|
except NameError:
|
||
|
unicode = str
|
||
|
try:
|
||
|
unichr
|
||
|
except NameError:
|
||
|
unichr = chr
|
||
|
|
||
|
|
||
|
class TestGraph(object):
|
||
|
|
||
|
@classmethod
|
||
|
def setup_class(cls):
|
||
|
cls.simple_data = """Creator "me"
|
||
|
Version "xx"
|
||
|
graph [
|
||
|
comment "This is a sample graph"
|
||
|
directed 1
|
||
|
IsPlanar 1
|
||
|
pos [ x 0 y 1 ]
|
||
|
node [
|
||
|
id 1
|
||
|
label "Node 1"
|
||
|
pos [ x 1 y 1 ]
|
||
|
]
|
||
|
node [
|
||
|
id 2
|
||
|
pos [ x 1 y 2 ]
|
||
|
label "Node 2"
|
||
|
]
|
||
|
node [
|
||
|
id 3
|
||
|
label "Node 3"
|
||
|
pos [ x 1 y 3 ]
|
||
|
]
|
||
|
edge [
|
||
|
source 1
|
||
|
target 2
|
||
|
label "Edge from node 1 to node 2"
|
||
|
color [line "blue" thickness 3]
|
||
|
|
||
|
]
|
||
|
edge [
|
||
|
source 2
|
||
|
target 3
|
||
|
label "Edge from node 2 to node 3"
|
||
|
]
|
||
|
edge [
|
||
|
source 3
|
||
|
target 1
|
||
|
label "Edge from node 3 to node 1"
|
||
|
]
|
||
|
]
|
||
|
"""
|
||
|
|
||
|
def test_parse_gml_cytoscape_bug(self):
|
||
|
# example from issue #321, originally #324 in trac
|
||
|
cytoscape_example = """
|
||
|
Creator "Cytoscape"
|
||
|
Version 1.0
|
||
|
graph [
|
||
|
node [
|
||
|
root_index -3
|
||
|
id -3
|
||
|
graphics [
|
||
|
x -96.0
|
||
|
y -67.0
|
||
|
w 40.0
|
||
|
h 40.0
|
||
|
fill "#ff9999"
|
||
|
type "ellipse"
|
||
|
outline "#666666"
|
||
|
outline_width 1.5
|
||
|
]
|
||
|
label "node2"
|
||
|
]
|
||
|
node [
|
||
|
root_index -2
|
||
|
id -2
|
||
|
graphics [
|
||
|
x 63.0
|
||
|
y 37.0
|
||
|
w 40.0
|
||
|
h 40.0
|
||
|
fill "#ff9999"
|
||
|
type "ellipse"
|
||
|
outline "#666666"
|
||
|
outline_width 1.5
|
||
|
]
|
||
|
label "node1"
|
||
|
]
|
||
|
node [
|
||
|
root_index -1
|
||
|
id -1
|
||
|
graphics [
|
||
|
x -31.0
|
||
|
y -17.0
|
||
|
w 40.0
|
||
|
h 40.0
|
||
|
fill "#ff9999"
|
||
|
type "ellipse"
|
||
|
outline "#666666"
|
||
|
outline_width 1.5
|
||
|
]
|
||
|
label "node0"
|
||
|
]
|
||
|
edge [
|
||
|
root_index -2
|
||
|
target -2
|
||
|
source -1
|
||
|
graphics [
|
||
|
width 1.5
|
||
|
fill "#0000ff"
|
||
|
type "line"
|
||
|
Line [
|
||
|
]
|
||
|
source_arrow 0
|
||
|
target_arrow 3
|
||
|
]
|
||
|
label "DirectedEdge"
|
||
|
]
|
||
|
edge [
|
||
|
root_index -1
|
||
|
target -1
|
||
|
source -3
|
||
|
graphics [
|
||
|
width 1.5
|
||
|
fill "#0000ff"
|
||
|
type "line"
|
||
|
Line [
|
||
|
]
|
||
|
source_arrow 0
|
||
|
target_arrow 3
|
||
|
]
|
||
|
label "DirectedEdge"
|
||
|
]
|
||
|
]
|
||
|
"""
|
||
|
nx.parse_gml(cytoscape_example)
|
||
|
|
||
|
def test_parse_gml(self):
|
||
|
G = nx.parse_gml(self.simple_data, label='label')
|
||
|
assert (sorted(G.nodes()) ==
|
||
|
['Node 1', 'Node 2', 'Node 3'])
|
||
|
assert ([e 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(data=True))] ==
|
||
|
[('Node 1', 'Node 2',
|
||
|
{'color': {'line': 'blue', 'thickness': 3},
|
||
|
'label': 'Edge from node 1 to node 2'}),
|
||
|
('Node 2', 'Node 3',
|
||
|
{'label': 'Edge from node 2 to node 3'}),
|
||
|
('Node 3', 'Node 1',
|
||
|
{'label': 'Edge from node 3 to node 1'})])
|
||
|
|
||
|
def test_read_gml(self):
|
||
|
(fd, fname) = tempfile.mkstemp()
|
||
|
fh = open(fname, 'w')
|
||
|
fh.write(self.simple_data)
|
||
|
fh.close()
|
||
|
Gin = nx.read_gml(fname, label='label')
|
||
|
G = nx.parse_gml(self.simple_data, label='label')
|
||
|
assert sorted(G.nodes(data=True)) == sorted(Gin.nodes(data=True))
|
||
|
assert sorted(G.edges(data=True)) == sorted(Gin.edges(data=True))
|
||
|
os.close(fd)
|
||
|
os.unlink(fname)
|
||
|
|
||
|
def test_labels_are_strings(self):
|
||
|
# GML requires labels to be strings (i.e., in quotes)
|
||
|
answer = """graph [
|
||
|
node [
|
||
|
id 0
|
||
|
label "1203"
|
||
|
]
|
||
|
]"""
|
||
|
G = nx.Graph()
|
||
|
G.add_node(1203)
|
||
|
data = '\n'.join(nx.generate_gml(G, stringizer=literal_stringizer))
|
||
|
assert data == answer
|
||
|
|
||
|
def test_relabel_duplicate(self):
|
||
|
data = """
|
||
|
graph
|
||
|
[
|
||
|
label ""
|
||
|
directed 1
|
||
|
node
|
||
|
[
|
||
|
id 0
|
||
|
label "same"
|
||
|
]
|
||
|
node
|
||
|
[
|
||
|
id 1
|
||
|
label "same"
|
||
|
]
|
||
|
]
|
||
|
"""
|
||
|
fh = io.BytesIO(data.encode('UTF-8'))
|
||
|
fh.seek(0)
|
||
|
pytest.raises(
|
||
|
nx.NetworkXError, nx.read_gml, fh, label='label')
|
||
|
|
||
|
def test_tuplelabels(self):
|
||
|
# https://github.com/networkx/networkx/pull/1048
|
||
|
# Writing tuple labels to GML failed.
|
||
|
G = nx.OrderedGraph()
|
||
|
G.add_edge((0, 1), (1, 0))
|
||
|
data = '\n'.join(nx.generate_gml(G, stringizer=literal_stringizer))
|
||
|
answer = """graph [
|
||
|
node [
|
||
|
id 0
|
||
|
label "(0,1)"
|
||
|
]
|
||
|
node [
|
||
|
id 1
|
||
|
label "(1,0)"
|
||
|
]
|
||
|
edge [
|
||
|
source 0
|
||
|
target 1
|
||
|
]
|
||
|
]"""
|
||
|
assert data == answer
|
||
|
|
||
|
def test_quotes(self):
|
||
|
# https://github.com/networkx/networkx/issues/1061
|
||
|
# Encoding quotes as HTML entities.
|
||
|
G = nx.path_graph(1)
|
||
|
G.name = "path_graph(1)"
|
||
|
attr = 'This is "quoted" and this is a copyright: ' + unichr(169)
|
||
|
G.nodes[0]['demo'] = attr
|
||
|
fobj = tempfile.NamedTemporaryFile()
|
||
|
nx.write_gml(G, fobj)
|
||
|
fobj.seek(0)
|
||
|
# Should be bytes in 2.x and 3.x
|
||
|
data = fobj.read().strip().decode('ascii')
|
||
|
answer = """graph [
|
||
|
name "path_graph(1)"
|
||
|
node [
|
||
|
id 0
|
||
|
label "0"
|
||
|
demo "This is "quoted" and this is a copyright: ©"
|
||
|
]
|
||
|
]"""
|
||
|
assert data == answer
|
||
|
|
||
|
def test_unicode_node(self):
|
||
|
node = 'node' + unichr(169)
|
||
|
G = nx.Graph()
|
||
|
G.add_node(node)
|
||
|
fobj = tempfile.NamedTemporaryFile()
|
||
|
nx.write_gml(G, fobj)
|
||
|
fobj.seek(0)
|
||
|
# Should be bytes in 2.x and 3.x
|
||
|
data = fobj.read().strip().decode('ascii')
|
||
|
answer = """graph [
|
||
|
node [
|
||
|
id 0
|
||
|
label "node©"
|
||
|
]
|
||
|
]"""
|
||
|
assert data == answer
|
||
|
|
||
|
def test_float_label(self):
|
||
|
node = 1.0
|
||
|
G = nx.Graph()
|
||
|
G.add_node(node)
|
||
|
fobj = tempfile.NamedTemporaryFile()
|
||
|
nx.write_gml(G, fobj)
|
||
|
fobj.seek(0)
|
||
|
# Should be bytes in 2.x and 3.x
|
||
|
data = fobj.read().strip().decode('ascii')
|
||
|
answer = """graph [
|
||
|
node [
|
||
|
id 0
|
||
|
label "1.0"
|
||
|
]
|
||
|
]"""
|
||
|
assert data == answer
|
||
|
|
||
|
def test_name(self):
|
||
|
G = nx.parse_gml('graph [ name "x" node [ id 0 label "x" ] ]')
|
||
|
assert 'x' == G.graph['name']
|
||
|
G = nx.parse_gml('graph [ node [ id 0 label "x" ] ]')
|
||
|
assert '' == G.name
|
||
|
assert 'name' not in G.graph
|
||
|
|
||
|
def test_graph_types(self):
|
||
|
for directed in [None, False, True]:
|
||
|
for multigraph in [None, False, True]:
|
||
|
gml = 'graph ['
|
||
|
if directed is not None:
|
||
|
gml += ' directed ' + str(int(directed))
|
||
|
if multigraph is not None:
|
||
|
gml += ' multigraph ' + str(int(multigraph))
|
||
|
gml += ' node [ id 0 label "0" ]'
|
||
|
gml += ' edge [ source 0 target 0 ]'
|
||
|
gml += ' ]'
|
||
|
G = nx.parse_gml(gml)
|
||
|
assert bool(directed) == G.is_directed()
|
||
|
assert bool(multigraph) == G.is_multigraph()
|
||
|
gml = 'graph [\n'
|
||
|
if directed is True:
|
||
|
gml += ' directed 1\n'
|
||
|
if multigraph is True:
|
||
|
gml += ' multigraph 1\n'
|
||
|
gml += """ node [
|
||
|
id 0
|
||
|
label "0"
|
||
|
]
|
||
|
edge [
|
||
|
source 0
|
||
|
target 0
|
||
|
"""
|
||
|
if multigraph:
|
||
|
gml += ' key 0\n'
|
||
|
gml += ' ]\n]'
|
||
|
assert gml == '\n'.join(nx.generate_gml(G))
|
||
|
|
||
|
def test_data_types(self):
|
||
|
data = [True, False, 10 ** 20, -2e33, "'", '"&&&""',
|
||
|
[{(b'\xfd',): '\x7f', unichr(0x4444): (1, 2)}, (2, "3")]]
|
||
|
try: # fails under IronPython
|
||
|
data.append(unichr(0x14444))
|
||
|
except ValueError:
|
||
|
data.append(unichr(0x1444))
|
||
|
try: # fails under Python 2.7
|
||
|
data.append(literal_eval('{2.3j, 1 - 2.3j, ()}'))
|
||
|
except ValueError:
|
||
|
data.append([2.3j, 1 - 2.3j, ()])
|
||
|
G = nx.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(nx.generate_gml(G, stringizer=literal_stringizer))
|
||
|
G = nx.parse_gml(gml, destringizer=literal_destringizer)
|
||
|
assert data == G.name
|
||
|
assert {'name': data, unicode('data'): data} == G.graph
|
||
|
assert (list(G.nodes(data=True)) ==
|
||
|
[(0, dict(int=-1, data=dict(data=data)))])
|
||
|
assert (list(G.edges(data=True)) ==
|
||
|
[(0, 0, dict(float=-2.5, data=data))])
|
||
|
G = nx.Graph()
|
||
|
G.graph['data'] = 'frozenset([1, 2, 3])'
|
||
|
G = nx.parse_gml(nx.generate_gml(G), destringizer=literal_eval)
|
||
|
assert G.graph['data'] == 'frozenset([1, 2, 3])'
|
||
|
|
||
|
def test_escape_unescape(self):
|
||
|
gml = """graph [
|
||
|
name "&"䑄��&unknown;"
|
||
|
]"""
|
||
|
G = nx.parse_gml(gml)
|
||
|
assert (
|
||
|
'&"\x0f' + unichr(0x4444) +
|
||
|
'��&unknown;' ==
|
||
|
G.name)
|
||
|
gml = '\n'.join(nx.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 + 'graph[]'.encode('ascii'))
|
||
|
f.seek(0)
|
||
|
pytest.raises(nx.NetworkXError, nx.read_gml, f)
|
||
|
|
||
|
def assert_parse_error(gml):
|
||
|
pytest.raises(nx.NetworkXError, nx.parse_gml, gml)
|
||
|
|
||
|
assert_parse_error(['graph [\n\n', unicode(']')])
|
||
|
assert_parse_error('')
|
||
|
assert_parse_error('Creator ""')
|
||
|
assert_parse_error('0')
|
||
|
assert_parse_error('graph ]')
|
||
|
assert_parse_error('graph [ 1 ]')
|
||
|
assert_parse_error('graph [ 1.E+2 ]')
|
||
|
assert_parse_error('graph [ "A" ]')
|
||
|
assert_parse_error('graph [ ] graph ]')
|
||
|
assert_parse_error('graph [ ] graph [ ]')
|
||
|
assert_parse_error('graph [ data [1, 2, 3] ]')
|
||
|
assert_parse_error('graph [ node [ ] ]')
|
||
|
assert_parse_error('graph [ node [ id 0 ] ]')
|
||
|
nx.parse_gml('graph [ node [ id "a" ] ]', label='id')
|
||
|
assert_parse_error(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 0 label 1 ] ]')
|
||
|
assert_parse_error(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 0 ] ]')
|
||
|
assert_parse_error('graph [ node [ id 0 label 0 ] edge [ ] ]')
|
||
|
assert_parse_error('graph [ node [ id 0 label 0 ] edge [ source 0 ] ]')
|
||
|
nx.parse_gml(
|
||
|
'graph [edge [ source 0 target 0 ] node [ id 0 label 0 ] ]')
|
||
|
assert_parse_error(
|
||
|
'graph [ node [ id 0 label 0 ] edge [ source 1 target 0 ] ]')
|
||
|
assert_parse_error(
|
||
|
'graph [ node [ id 0 label 0 ] edge [ source 0 target 1 ] ]')
|
||
|
assert_parse_error(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] '
|
||
|
'edge [ source 0 target 1 ] edge [ source 1 target 0 ] ]')
|
||
|
nx.parse_gml(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] '
|
||
|
'edge [ source 0 target 1 ] edge [ source 1 target 0 ] '
|
||
|
'directed 1 ]')
|
||
|
nx.parse_gml(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] '
|
||
|
'edge [ source 0 target 1 ] edge [ source 0 target 1 ]'
|
||
|
'multigraph 1 ]')
|
||
|
nx.parse_gml(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] '
|
||
|
'edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 ]'
|
||
|
'multigraph 1 ]')
|
||
|
assert_parse_error(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] '
|
||
|
'edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 key 0 ]'
|
||
|
'multigraph 1 ]')
|
||
|
nx.parse_gml(
|
||
|
'graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] '
|
||
|
'edge [ source 0 target 1 key 0 ] edge [ source 1 target 0 key 0 ]'
|
||
|
'directed 1 multigraph 1 ]')
|
||
|
|
||
|
# Tests for string convertable alphanumeric id and label values
|
||
|
nx.parse_gml(
|
||
|
'graph [edge [ source a target a ] node [ id a label b ] ]')
|
||
|
nx.parse_gml(
|
||
|
'graph [ node [ id n42 label 0 ] node [ id x43 label 1 ]'
|
||
|
'edge [ source n42 target x43 key 0 ]'
|
||
|
'edge [ source x43 target n42 key 0 ]'
|
||
|
'directed 1 multigraph 1 ]')
|
||
|
assert_parse_error(
|
||
|
"graph [edge [ source u'u\4200' target u'u\4200' ] " +
|
||
|
"node [ id u'u\4200' label b ] ]")
|
||
|
|
||
|
def assert_generate_error(*args, **kwargs):
|
||
|
pytest.raises(nx.NetworkXError,
|
||
|
lambda: list(nx.generate_gml(*args, **kwargs)))
|
||
|
|
||
|
G = nx.Graph()
|
||
|
G.graph[3] = 3
|
||
|
assert_generate_error(G)
|
||
|
G = nx.Graph()
|
||
|
G.graph['3'] = 3
|
||
|
assert_generate_error(G)
|
||
|
G = nx.Graph()
|
||
|
G.graph['data'] = frozenset([1, 2, 3])
|
||
|
assert_generate_error(G, stringizer=literal_stringizer)
|
||
|
G = nx.Graph()
|
||
|
G.graph['data'] = []
|
||
|
assert_generate_error(G)
|
||
|
assert_generate_error(G, stringizer=len)
|
||
|
|
||
|
def test_label_kwarg(self):
|
||
|
G = nx.parse_gml(self.simple_data, label='id')
|
||
|
assert sorted(G.nodes) == [1, 2, 3]
|
||
|
labels = [G.nodes[n]['label'] for n in sorted(G.nodes)]
|
||
|
assert labels == ['Node 1', 'Node 2', 'Node 3']
|
||
|
|
||
|
G = nx.parse_gml(self.simple_data, label=None)
|
||
|
assert sorted(G.nodes) == [1, 2, 3]
|
||
|
labels = [G.nodes[n]['label'] for n in sorted(G.nodes)]
|
||
|
assert labels == ['Node 1', 'Node 2', 'Node 3']
|
||
|
|
||
|
def test_outofrange_integers(self):
|
||
|
# GML restricts integers to 32 signed bits.
|
||
|
# Check that we honor this restriction on export
|
||
|
G = nx.Graph()
|
||
|
# Test export for numbers that barely fit or don't fit into 32 bits,
|
||
|
# and 3 numbers in the middle
|
||
|
numbers = {'toosmall': (-2**31)-1,
|
||
|
'small': -2**31,
|
||
|
'med1': -4,
|
||
|
'med2': 0,
|
||
|
'med3': 17,
|
||
|
'big': (2**31)-1,
|
||
|
'toobig': 2**31}
|
||
|
G.add_node('Node', **numbers)
|
||
|
|
||
|
fd, fname = tempfile.mkstemp()
|
||
|
try:
|
||
|
nx.write_gml(G, fname)
|
||
|
# Check that the export wrote the nonfitting numbers as strings
|
||
|
G2 = nx.read_gml(fname)
|
||
|
for attr, value in G2.nodes['Node'].items():
|
||
|
if attr == 'toosmall' or attr == 'toobig':
|
||
|
assert type(value) == str
|
||
|
else:
|
||
|
assert type(value) == int
|
||
|
finally:
|
||
|
os.close(fd)
|
||
|
os.unlink(fname)
|