import codecs import io import math import os import tempfile from ast import literal_eval from contextlib import contextmanager from textwrap import dedent import pytest import networkx as nx from networkx.readwrite.gml import literal_destringizer, literal_stringizer class TestGraph: @classmethod def setup_class(cls): cls.simple_data = """Creator "me" Version "xx" graph [ comment "This is a sample graph" directed 1 IsPlanar 1 pos [ x 0 y 1 ] node [ id 1 label "Node 1" pos [ x 1 y 1 ] ] node [ id 2 pos [ x 1 y 2 ] label "Node 2" ] node [ id 3 label "Node 3" pos [ x 1 y 3 ] ] edge [ source 1 target 2 label "Edge from node 1 to node 2" color [line "blue" thickness 3] ] edge [ source 2 target 3 label "Edge from node 2 to node 3" ] edge [ source 3 target 1 label "Edge from node 3 to node 1" ] ] """ def test_parse_gml_cytoscape_bug(self): # example from issue #321, originally #324 in trac cytoscape_example = """ Creator "Cytoscape" Version 1.0 graph [ node [ root_index -3 id -3 graphics [ x -96.0 y -67.0 w 40.0 h 40.0 fill "#ff9999" type "ellipse" outline "#666666" outline_width 1.5 ] label "node2" ] node [ root_index -2 id -2 graphics [ x 63.0 y 37.0 w 40.0 h 40.0 fill "#ff9999" type "ellipse" outline "#666666" outline_width 1.5 ] label "node1" ] node [ root_index -1 id -1 graphics [ x -31.0 y -17.0 w 40.0 h 40.0 fill "#ff9999" type "ellipse" outline "#666666" outline_width 1.5 ] label "node0" ] edge [ root_index -2 target -2 source -1 graphics [ width 1.5 fill "#0000ff" type "line" Line [ ] source_arrow 0 target_arrow 3 ] label "DirectedEdge" ] edge [ root_index -1 target -1 source -3 graphics [ width 1.5 fill "#0000ff" type "line" Line [ ] source_arrow 0 target_arrow 3 ] label "DirectedEdge" ] ] """ nx.parse_gml(cytoscape_example) def test_parse_gml(self): G = nx.parse_gml(self.simple_data, label="label") assert sorted(G.nodes()) == ["Node 1", "Node 2", "Node 3"] assert [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: ' + chr(169) G.nodes[0]["demo"] = attr fobj = tempfile.NamedTemporaryFile() nx.write_gml(G, fobj) fobj.seek(0) # Should be bytes in 2.x and 3.x data = fobj.read().strip().decode("ascii") answer = """graph [ name "path_graph(1)" node [ id 0 label "0" demo "This is "quoted" and this is a copyright: ©" ] ]""" assert data == answer def test_unicode_node(self): node = "node" + chr(169) G = nx.Graph() G.add_node(node) fobj = tempfile.NamedTemporaryFile() nx.write_gml(G, fobj) fobj.seek(0) # Should be bytes in 2.x and 3.x data = fobj.read().strip().decode("ascii") answer = """graph [ node [ id 0 label "node©" ] ]""" assert data == answer def test_float_label(self): node = 1.0 G = nx.Graph() G.add_node(node) fobj = tempfile.NamedTemporaryFile() nx.write_gml(G, fobj) fobj.seek(0) # Should be bytes in 2.x and 3.x data = fobj.read().strip().decode("ascii") answer = """graph [ node [ id 0 label "1.0" ] ]""" assert data == answer def test_special_float_label(self): special_floats = [float("nan"), float("+inf"), float("-inf")] try: import numpy as np special_floats += [np.nan, np.inf, np.inf * -1] except ImportError: special_floats += special_floats G = nx.cycle_graph(len(special_floats)) attrs = dict(enumerate(special_floats)) nx.set_node_attributes(G, attrs, "nodefloat") edges = list(G.edges) attrs = {edges[i]: value for i, value in enumerate(special_floats)} nx.set_edge_attributes(G, attrs, "edgefloat") fobj = tempfile.NamedTemporaryFile() nx.write_gml(G, fobj) fobj.seek(0) # Should be bytes in 2.x and 3.x data = fobj.read().strip().decode("ascii") answer = """graph [ node [ id 0 label "0" nodefloat NAN ] node [ id 1 label "1" nodefloat +INF ] node [ id 2 label "2" nodefloat -INF ] node [ id 3 label "3" nodefloat NAN ] node [ id 4 label "4" nodefloat +INF ] node [ id 5 label "5" nodefloat -INF ] edge [ source 0 target 1 edgefloat NAN ] edge [ source 0 target 5 edgefloat +INF ] edge [ source 1 target 2 edgefloat -INF ] edge [ source 2 target 3 edgefloat NAN ] edge [ source 3 target 4 edgefloat +INF ] edge [ source 4 target 5 edgefloat -INF ] ]""" assert data == answer fobj.seek(0) graph = nx.read_gml(fobj) for indx, value in enumerate(special_floats): node_value = graph.nodes[str(indx)]["nodefloat"] if math.isnan(value): assert math.isnan(node_value) else: assert node_value == value edge = edges[indx] string_edge = (str(edge[0]), str(edge[1])) edge_value = graph.edges[string_edge]["edgefloat"] if math.isnan(value): assert math.isnan(edge_value) else: assert edge_value == value def test_name(self): G = nx.parse_gml('graph [ name "x" node [ id 0 label "x" ] ]') assert "x" == G.graph["name"] G = nx.parse_gml('graph [ node [ id 0 label "x" ] ]') assert "" == G.name assert "name" not in G.graph def test_graph_types(self): for directed in [None, False, True]: for multigraph in [None, False, True]: gml = "graph [" if directed is not None: gml += " directed " + str(int(directed)) if multigraph is not None: gml += " multigraph " + str(int(multigraph)) gml += ' node [ id 0 label "0" ]' gml += " edge [ source 0 target 0 ]" gml += " ]" G = nx.parse_gml(gml) assert bool(directed) == G.is_directed() assert bool(multigraph) == G.is_multigraph() gml = "graph [\n" if directed is True: gml += " directed 1\n" if multigraph is True: gml += " multigraph 1\n" gml += """ node [ id 0 label "0" ] edge [ source 0 target 0 """ if multigraph: gml += " key 0\n" gml += " ]\n]" assert gml == "\n".join(nx.generate_gml(G)) def test_data_types(self): data = [ True, False, 10**20, -2e33, "'", '"&&&""', [{(b"\xfd",): "\x7f", chr(0x4444): (1, 2)}, (2, "3")], ] data.append(chr(0x14444)) data.append(literal_eval("{2.3j, 1 - 2.3j, ()}")) G = nx.Graph() G.name = data G.graph["data"] = data G.add_node(0, int=-1, data=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, "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' + chr(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 + b"graph[]") f.seek(0) pytest.raises(nx.NetworkXError, nx.read_gml, f) def assert_parse_error(gml): pytest.raises(nx.NetworkXError, nx.parse_gml, gml) assert_parse_error(["graph [\n\n", "]"]) assert_parse_error("") assert_parse_error('Creator ""') assert_parse_error("0") assert_parse_error("graph ]") assert_parse_error("graph [ 1 ]") assert_parse_error("graph [ 1.E+2 ]") assert_parse_error('graph [ "A" ]') assert_parse_error("graph [ ] graph ]") assert_parse_error("graph [ ] graph [ ]") assert_parse_error("graph [ data [1, 2, 3] ]") assert_parse_error("graph [ node [ ] ]") assert_parse_error("graph [ node [ id 0 ] ]") nx.parse_gml('graph [ node [ id "a" ] ]', label="id") assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 0 label 1 ] ]") assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 1 label 0 ] ]") assert_parse_error("graph [ node [ id 0 label 0 ] edge [ ] ]") assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 ] ]") nx.parse_gml("graph [edge [ source 0 target 0 ] node [ id 0 label 0 ] ]") assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 1 target 0 ] ]") assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 target 1 ] ]") assert_parse_error( "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " "edge [ source 0 target 1 ] edge [ source 1 target 0 ] ]" ) nx.parse_gml( "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " "edge [ source 0 target 1 ] edge [ source 1 target 0 ] " "directed 1 ]" ) nx.parse_gml( "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " "edge [ source 0 target 1 ] edge [ source 0 target 1 ]" "multigraph 1 ]" ) nx.parse_gml( "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 ]" "multigraph 1 ]" ) assert_parse_error( "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 key 0 ]" "multigraph 1 ]" ) nx.parse_gml( "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " "edge [ source 0 target 1 key 0 ] edge [ source 1 target 0 key 0 ]" "directed 1 multigraph 1 ]" ) # Tests for string 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) @contextmanager def byte_file(): _file_handle = io.BytesIO() yield _file_handle _file_handle.seek(0) class TestPropertyLists: def test_writing_graph_with_multi_element_property_list(self): g = nx.Graph() g.add_node("n1", properties=["element", 0, 1, 2.5, True, False]) with byte_file() as f: nx.write_gml(g, f) result = f.read().decode() assert result == dedent( """\ graph [ node [ id 0 label "n1" properties "element" properties 0 properties 1 properties 2.5 properties 1 properties 0 ] ] """ ) def test_writing_graph_with_one_element_property_list(self): g = nx.Graph() g.add_node("n1", properties=["element"]) with byte_file() as f: nx.write_gml(g, f) result = f.read().decode() assert result == dedent( """\ graph [ node [ id 0 label "n1" properties "_networkx_list_start" properties "element" ] ] """ ) def test_reading_graph_with_list_property(self): with byte_file() as f: f.write( dedent( """ graph [ node [ id 0 label "n1" properties "element" properties 0 properties 1 properties 2.5 ] ] """ ).encode("ascii") ) f.seek(0) graph = nx.read_gml(f) assert graph.nodes(data=True)["n1"] == {"properties": ["element", 0, 1, 2.5]} def test_reading_graph_with_single_element_list_property(self): with byte_file() as f: f.write( dedent( """ graph [ node [ id 0 label "n1" properties "_networkx_list_start" properties "element" ] ] """ ).encode("ascii") ) f.seek(0) graph = nx.read_gml(f) assert graph.nodes(data=True)["n1"] == {"properties": ["element"]}