test_gml.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. import codecs
  2. import io
  3. import math
  4. import os
  5. import tempfile
  6. from ast import literal_eval
  7. from contextlib import contextmanager
  8. from textwrap import dedent
  9. import pytest
  10. import networkx as nx
  11. from networkx.readwrite.gml import literal_destringizer, literal_stringizer
  12. class TestGraph:
  13. @classmethod
  14. def setup_class(cls):
  15. cls.simple_data = """Creator "me"
  16. Version "xx"
  17. graph [
  18. comment "This is a sample graph"
  19. directed 1
  20. IsPlanar 1
  21. pos [ x 0 y 1 ]
  22. node [
  23. id 1
  24. label "Node 1"
  25. pos [ x 1 y 1 ]
  26. ]
  27. node [
  28. id 2
  29. pos [ x 1 y 2 ]
  30. label "Node 2"
  31. ]
  32. node [
  33. id 3
  34. label "Node 3"
  35. pos [ x 1 y 3 ]
  36. ]
  37. edge [
  38. source 1
  39. target 2
  40. label "Edge from node 1 to node 2"
  41. color [line "blue" thickness 3]
  42. ]
  43. edge [
  44. source 2
  45. target 3
  46. label "Edge from node 2 to node 3"
  47. ]
  48. edge [
  49. source 3
  50. target 1
  51. label "Edge from node 3 to node 1"
  52. ]
  53. ]
  54. """
  55. def test_parse_gml_cytoscape_bug(self):
  56. # example from issue #321, originally #324 in trac
  57. cytoscape_example = """
  58. Creator "Cytoscape"
  59. Version 1.0
  60. graph [
  61. node [
  62. root_index -3
  63. id -3
  64. graphics [
  65. x -96.0
  66. y -67.0
  67. w 40.0
  68. h 40.0
  69. fill "#ff9999"
  70. type "ellipse"
  71. outline "#666666"
  72. outline_width 1.5
  73. ]
  74. label "node2"
  75. ]
  76. node [
  77. root_index -2
  78. id -2
  79. graphics [
  80. x 63.0
  81. y 37.0
  82. w 40.0
  83. h 40.0
  84. fill "#ff9999"
  85. type "ellipse"
  86. outline "#666666"
  87. outline_width 1.5
  88. ]
  89. label "node1"
  90. ]
  91. node [
  92. root_index -1
  93. id -1
  94. graphics [
  95. x -31.0
  96. y -17.0
  97. w 40.0
  98. h 40.0
  99. fill "#ff9999"
  100. type "ellipse"
  101. outline "#666666"
  102. outline_width 1.5
  103. ]
  104. label "node0"
  105. ]
  106. edge [
  107. root_index -2
  108. target -2
  109. source -1
  110. graphics [
  111. width 1.5
  112. fill "#0000ff"
  113. type "line"
  114. Line [
  115. ]
  116. source_arrow 0
  117. target_arrow 3
  118. ]
  119. label "DirectedEdge"
  120. ]
  121. edge [
  122. root_index -1
  123. target -1
  124. source -3
  125. graphics [
  126. width 1.5
  127. fill "#0000ff"
  128. type "line"
  129. Line [
  130. ]
  131. source_arrow 0
  132. target_arrow 3
  133. ]
  134. label "DirectedEdge"
  135. ]
  136. ]
  137. """
  138. nx.parse_gml(cytoscape_example)
  139. def test_parse_gml(self):
  140. G = nx.parse_gml(self.simple_data, label="label")
  141. assert sorted(G.nodes()) == ["Node 1", "Node 2", "Node 3"]
  142. assert sorted(G.edges()) == [
  143. ("Node 1", "Node 2"),
  144. ("Node 2", "Node 3"),
  145. ("Node 3", "Node 1"),
  146. ]
  147. assert sorted(G.edges(data=True)) == [
  148. (
  149. "Node 1",
  150. "Node 2",
  151. {
  152. "color": {"line": "blue", "thickness": 3},
  153. "label": "Edge from node 1 to node 2",
  154. },
  155. ),
  156. ("Node 2", "Node 3", {"label": "Edge from node 2 to node 3"}),
  157. ("Node 3", "Node 1", {"label": "Edge from node 3 to node 1"}),
  158. ]
  159. def test_read_gml(self):
  160. (fd, fname) = tempfile.mkstemp()
  161. fh = open(fname, "w")
  162. fh.write(self.simple_data)
  163. fh.close()
  164. Gin = nx.read_gml(fname, label="label")
  165. G = nx.parse_gml(self.simple_data, label="label")
  166. assert sorted(G.nodes(data=True)) == sorted(Gin.nodes(data=True))
  167. assert sorted(G.edges(data=True)) == sorted(Gin.edges(data=True))
  168. os.close(fd)
  169. os.unlink(fname)
  170. def test_labels_are_strings(self):
  171. # GML requires labels to be strings (i.e., in quotes)
  172. answer = """graph [
  173. node [
  174. id 0
  175. label "1203"
  176. ]
  177. ]"""
  178. G = nx.Graph()
  179. G.add_node(1203)
  180. data = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer))
  181. assert data == answer
  182. def test_relabel_duplicate(self):
  183. data = """
  184. graph
  185. [
  186. label ""
  187. directed 1
  188. node
  189. [
  190. id 0
  191. label "same"
  192. ]
  193. node
  194. [
  195. id 1
  196. label "same"
  197. ]
  198. ]
  199. """
  200. fh = io.BytesIO(data.encode("UTF-8"))
  201. fh.seek(0)
  202. pytest.raises(nx.NetworkXError, nx.read_gml, fh, label="label")
  203. def test_tuplelabels(self):
  204. # https://github.com/networkx/networkx/pull/1048
  205. # Writing tuple labels to GML failed.
  206. G = nx.Graph()
  207. G.add_edge((0, 1), (1, 0))
  208. data = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer))
  209. answer = """graph [
  210. node [
  211. id 0
  212. label "(0,1)"
  213. ]
  214. node [
  215. id 1
  216. label "(1,0)"
  217. ]
  218. edge [
  219. source 0
  220. target 1
  221. ]
  222. ]"""
  223. assert data == answer
  224. def test_quotes(self):
  225. # https://github.com/networkx/networkx/issues/1061
  226. # Encoding quotes as HTML entities.
  227. G = nx.path_graph(1)
  228. G.name = "path_graph(1)"
  229. attr = 'This is "quoted" and this is a copyright: ' + chr(169)
  230. G.nodes[0]["demo"] = attr
  231. fobj = tempfile.NamedTemporaryFile()
  232. nx.write_gml(G, fobj)
  233. fobj.seek(0)
  234. # Should be bytes in 2.x and 3.x
  235. data = fobj.read().strip().decode("ascii")
  236. answer = """graph [
  237. name "path_graph(1)"
  238. node [
  239. id 0
  240. label "0"
  241. demo "This is "quoted" and this is a copyright: ©"
  242. ]
  243. ]"""
  244. assert data == answer
  245. def test_unicode_node(self):
  246. node = "node" + chr(169)
  247. G = nx.Graph()
  248. G.add_node(node)
  249. fobj = tempfile.NamedTemporaryFile()
  250. nx.write_gml(G, fobj)
  251. fobj.seek(0)
  252. # Should be bytes in 2.x and 3.x
  253. data = fobj.read().strip().decode("ascii")
  254. answer = """graph [
  255. node [
  256. id 0
  257. label "node©"
  258. ]
  259. ]"""
  260. assert data == answer
  261. def test_float_label(self):
  262. node = 1.0
  263. G = nx.Graph()
  264. G.add_node(node)
  265. fobj = tempfile.NamedTemporaryFile()
  266. nx.write_gml(G, fobj)
  267. fobj.seek(0)
  268. # Should be bytes in 2.x and 3.x
  269. data = fobj.read().strip().decode("ascii")
  270. answer = """graph [
  271. node [
  272. id 0
  273. label "1.0"
  274. ]
  275. ]"""
  276. assert data == answer
  277. def test_special_float_label(self):
  278. special_floats = [float("nan"), float("+inf"), float("-inf")]
  279. try:
  280. import numpy as np
  281. special_floats += [np.nan, np.inf, np.inf * -1]
  282. except ImportError:
  283. special_floats += special_floats
  284. G = nx.cycle_graph(len(special_floats))
  285. attrs = dict(enumerate(special_floats))
  286. nx.set_node_attributes(G, attrs, "nodefloat")
  287. edges = list(G.edges)
  288. attrs = {edges[i]: value for i, value in enumerate(special_floats)}
  289. nx.set_edge_attributes(G, attrs, "edgefloat")
  290. fobj = tempfile.NamedTemporaryFile()
  291. nx.write_gml(G, fobj)
  292. fobj.seek(0)
  293. # Should be bytes in 2.x and 3.x
  294. data = fobj.read().strip().decode("ascii")
  295. answer = """graph [
  296. node [
  297. id 0
  298. label "0"
  299. nodefloat NAN
  300. ]
  301. node [
  302. id 1
  303. label "1"
  304. nodefloat +INF
  305. ]
  306. node [
  307. id 2
  308. label "2"
  309. nodefloat -INF
  310. ]
  311. node [
  312. id 3
  313. label "3"
  314. nodefloat NAN
  315. ]
  316. node [
  317. id 4
  318. label "4"
  319. nodefloat +INF
  320. ]
  321. node [
  322. id 5
  323. label "5"
  324. nodefloat -INF
  325. ]
  326. edge [
  327. source 0
  328. target 1
  329. edgefloat NAN
  330. ]
  331. edge [
  332. source 0
  333. target 5
  334. edgefloat +INF
  335. ]
  336. edge [
  337. source 1
  338. target 2
  339. edgefloat -INF
  340. ]
  341. edge [
  342. source 2
  343. target 3
  344. edgefloat NAN
  345. ]
  346. edge [
  347. source 3
  348. target 4
  349. edgefloat +INF
  350. ]
  351. edge [
  352. source 4
  353. target 5
  354. edgefloat -INF
  355. ]
  356. ]"""
  357. assert data == answer
  358. fobj.seek(0)
  359. graph = nx.read_gml(fobj)
  360. for indx, value in enumerate(special_floats):
  361. node_value = graph.nodes[str(indx)]["nodefloat"]
  362. if math.isnan(value):
  363. assert math.isnan(node_value)
  364. else:
  365. assert node_value == value
  366. edge = edges[indx]
  367. string_edge = (str(edge[0]), str(edge[1]))
  368. edge_value = graph.edges[string_edge]["edgefloat"]
  369. if math.isnan(value):
  370. assert math.isnan(edge_value)
  371. else:
  372. assert edge_value == value
  373. def test_name(self):
  374. G = nx.parse_gml('graph [ name "x" node [ id 0 label "x" ] ]')
  375. assert "x" == G.graph["name"]
  376. G = nx.parse_gml('graph [ node [ id 0 label "x" ] ]')
  377. assert "" == G.name
  378. assert "name" not in G.graph
  379. def test_graph_types(self):
  380. for directed in [None, False, True]:
  381. for multigraph in [None, False, True]:
  382. gml = "graph ["
  383. if directed is not None:
  384. gml += " directed " + str(int(directed))
  385. if multigraph is not None:
  386. gml += " multigraph " + str(int(multigraph))
  387. gml += ' node [ id 0 label "0" ]'
  388. gml += " edge [ source 0 target 0 ]"
  389. gml += " ]"
  390. G = nx.parse_gml(gml)
  391. assert bool(directed) == G.is_directed()
  392. assert bool(multigraph) == G.is_multigraph()
  393. gml = "graph [\n"
  394. if directed is True:
  395. gml += " directed 1\n"
  396. if multigraph is True:
  397. gml += " multigraph 1\n"
  398. gml += """ node [
  399. id 0
  400. label "0"
  401. ]
  402. edge [
  403. source 0
  404. target 0
  405. """
  406. if multigraph:
  407. gml += " key 0\n"
  408. gml += " ]\n]"
  409. assert gml == "\n".join(nx.generate_gml(G))
  410. def test_data_types(self):
  411. data = [
  412. True,
  413. False,
  414. 10**20,
  415. -2e33,
  416. "'",
  417. '"&&&""',
  418. [{(b"\xfd",): "\x7f", chr(0x4444): (1, 2)}, (2, "3")],
  419. ]
  420. data.append(chr(0x14444))
  421. data.append(literal_eval("{2.3j, 1 - 2.3j, ()}"))
  422. G = nx.Graph()
  423. G.name = data
  424. G.graph["data"] = data
  425. G.add_node(0, int=-1, data={"data": data})
  426. G.add_edge(0, 0, float=-2.5, data=data)
  427. gml = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer))
  428. G = nx.parse_gml(gml, destringizer=literal_destringizer)
  429. assert data == G.name
  430. assert {"name": data, "data": data} == G.graph
  431. assert list(G.nodes(data=True)) == [(0, {"int": -1, "data": {"data": data}})]
  432. assert list(G.edges(data=True)) == [(0, 0, {"float": -2.5, "data": data})]
  433. G = nx.Graph()
  434. G.graph["data"] = "frozenset([1, 2, 3])"
  435. G = nx.parse_gml(nx.generate_gml(G), destringizer=literal_eval)
  436. assert G.graph["data"] == "frozenset([1, 2, 3])"
  437. def test_escape_unescape(self):
  438. gml = """graph [
  439. name "&"䑄��&unknown;"
  440. ]"""
  441. G = nx.parse_gml(gml)
  442. assert (
  443. '&"\x0f' + chr(0x4444) + "��&unknown;"
  444. == G.name
  445. )
  446. gml = "\n".join(nx.generate_gml(G))
  447. alnu = "#1234567890;&#x1234567890abcdef"
  448. answer = (
  449. """graph [
  450. name "&"䑄&"""
  451. + alnu
  452. + """;&unknown;"
  453. ]"""
  454. )
  455. assert answer == gml
  456. def test_exceptions(self):
  457. pytest.raises(ValueError, literal_destringizer, "(")
  458. pytest.raises(ValueError, literal_destringizer, "frozenset([1, 2, 3])")
  459. pytest.raises(ValueError, literal_destringizer, literal_destringizer)
  460. pytest.raises(ValueError, literal_stringizer, frozenset([1, 2, 3]))
  461. pytest.raises(ValueError, literal_stringizer, literal_stringizer)
  462. with tempfile.TemporaryFile() as f:
  463. f.write(codecs.BOM_UTF8 + b"graph[]")
  464. f.seek(0)
  465. pytest.raises(nx.NetworkXError, nx.read_gml, f)
  466. def assert_parse_error(gml):
  467. pytest.raises(nx.NetworkXError, nx.parse_gml, gml)
  468. assert_parse_error(["graph [\n\n", "]"])
  469. assert_parse_error("")
  470. assert_parse_error('Creator ""')
  471. assert_parse_error("0")
  472. assert_parse_error("graph ]")
  473. assert_parse_error("graph [ 1 ]")
  474. assert_parse_error("graph [ 1.E+2 ]")
  475. assert_parse_error('graph [ "A" ]')
  476. assert_parse_error("graph [ ] graph ]")
  477. assert_parse_error("graph [ ] graph [ ]")
  478. assert_parse_error("graph [ data [1, 2, 3] ]")
  479. assert_parse_error("graph [ node [ ] ]")
  480. assert_parse_error("graph [ node [ id 0 ] ]")
  481. nx.parse_gml('graph [ node [ id "a" ] ]', label="id")
  482. assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 0 label 1 ] ]")
  483. assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 1 label 0 ] ]")
  484. assert_parse_error("graph [ node [ id 0 label 0 ] edge [ ] ]")
  485. assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 ] ]")
  486. nx.parse_gml("graph [edge [ source 0 target 0 ] node [ id 0 label 0 ] ]")
  487. assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 1 target 0 ] ]")
  488. assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 target 1 ] ]")
  489. assert_parse_error(
  490. "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
  491. "edge [ source 0 target 1 ] edge [ source 1 target 0 ] ]"
  492. )
  493. nx.parse_gml(
  494. "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
  495. "edge [ source 0 target 1 ] edge [ source 1 target 0 ] "
  496. "directed 1 ]"
  497. )
  498. nx.parse_gml(
  499. "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
  500. "edge [ source 0 target 1 ] edge [ source 0 target 1 ]"
  501. "multigraph 1 ]"
  502. )
  503. nx.parse_gml(
  504. "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
  505. "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 ]"
  506. "multigraph 1 ]"
  507. )
  508. assert_parse_error(
  509. "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
  510. "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 key 0 ]"
  511. "multigraph 1 ]"
  512. )
  513. nx.parse_gml(
  514. "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] "
  515. "edge [ source 0 target 1 key 0 ] edge [ source 1 target 0 key 0 ]"
  516. "directed 1 multigraph 1 ]"
  517. )
  518. # Tests for string convertible alphanumeric id and label values
  519. nx.parse_gml("graph [edge [ source a target a ] node [ id a label b ] ]")
  520. nx.parse_gml(
  521. "graph [ node [ id n42 label 0 ] node [ id x43 label 1 ]"
  522. "edge [ source n42 target x43 key 0 ]"
  523. "edge [ source x43 target n42 key 0 ]"
  524. "directed 1 multigraph 1 ]"
  525. )
  526. assert_parse_error(
  527. "graph [edge [ source u'u\4200' target u'u\4200' ] "
  528. + "node [ id u'u\4200' label b ] ]"
  529. )
  530. def assert_generate_error(*args, **kwargs):
  531. pytest.raises(
  532. nx.NetworkXError, lambda: list(nx.generate_gml(*args, **kwargs))
  533. )
  534. G = nx.Graph()
  535. G.graph[3] = 3
  536. assert_generate_error(G)
  537. G = nx.Graph()
  538. G.graph["3"] = 3
  539. assert_generate_error(G)
  540. G = nx.Graph()
  541. G.graph["data"] = frozenset([1, 2, 3])
  542. assert_generate_error(G, stringizer=literal_stringizer)
  543. def test_label_kwarg(self):
  544. G = nx.parse_gml(self.simple_data, label="id")
  545. assert sorted(G.nodes) == [1, 2, 3]
  546. labels = [G.nodes[n]["label"] for n in sorted(G.nodes)]
  547. assert labels == ["Node 1", "Node 2", "Node 3"]
  548. G = nx.parse_gml(self.simple_data, label=None)
  549. assert sorted(G.nodes) == [1, 2, 3]
  550. labels = [G.nodes[n]["label"] for n in sorted(G.nodes)]
  551. assert labels == ["Node 1", "Node 2", "Node 3"]
  552. def test_outofrange_integers(self):
  553. # GML restricts integers to 32 signed bits.
  554. # Check that we honor this restriction on export
  555. G = nx.Graph()
  556. # Test export for numbers that barely fit or don't fit into 32 bits,
  557. # and 3 numbers in the middle
  558. numbers = {
  559. "toosmall": (-(2**31)) - 1,
  560. "small": -(2**31),
  561. "med1": -4,
  562. "med2": 0,
  563. "med3": 17,
  564. "big": (2**31) - 1,
  565. "toobig": 2**31,
  566. }
  567. G.add_node("Node", **numbers)
  568. fd, fname = tempfile.mkstemp()
  569. try:
  570. nx.write_gml(G, fname)
  571. # Check that the export wrote the nonfitting numbers as strings
  572. G2 = nx.read_gml(fname)
  573. for attr, value in G2.nodes["Node"].items():
  574. if attr == "toosmall" or attr == "toobig":
  575. assert type(value) == str
  576. else:
  577. assert type(value) == int
  578. finally:
  579. os.close(fd)
  580. os.unlink(fname)
  581. @contextmanager
  582. def byte_file():
  583. _file_handle = io.BytesIO()
  584. yield _file_handle
  585. _file_handle.seek(0)
  586. class TestPropertyLists:
  587. def test_writing_graph_with_multi_element_property_list(self):
  588. g = nx.Graph()
  589. g.add_node("n1", properties=["element", 0, 1, 2.5, True, False])
  590. with byte_file() as f:
  591. nx.write_gml(g, f)
  592. result = f.read().decode()
  593. assert result == dedent(
  594. """\
  595. graph [
  596. node [
  597. id 0
  598. label "n1"
  599. properties "element"
  600. properties 0
  601. properties 1
  602. properties 2.5
  603. properties 1
  604. properties 0
  605. ]
  606. ]
  607. """
  608. )
  609. def test_writing_graph_with_one_element_property_list(self):
  610. g = nx.Graph()
  611. g.add_node("n1", properties=["element"])
  612. with byte_file() as f:
  613. nx.write_gml(g, f)
  614. result = f.read().decode()
  615. assert result == dedent(
  616. """\
  617. graph [
  618. node [
  619. id 0
  620. label "n1"
  621. properties "_networkx_list_start"
  622. properties "element"
  623. ]
  624. ]
  625. """
  626. )
  627. def test_reading_graph_with_list_property(self):
  628. with byte_file() as f:
  629. f.write(
  630. dedent(
  631. """
  632. graph [
  633. node [
  634. id 0
  635. label "n1"
  636. properties "element"
  637. properties 0
  638. properties 1
  639. properties 2.5
  640. ]
  641. ]
  642. """
  643. ).encode("ascii")
  644. )
  645. f.seek(0)
  646. graph = nx.read_gml(f)
  647. assert graph.nodes(data=True)["n1"] == {"properties": ["element", 0, 1, 2.5]}
  648. def test_reading_graph_with_single_element_list_property(self):
  649. with byte_file() as f:
  650. f.write(
  651. dedent(
  652. """
  653. graph [
  654. node [
  655. id 0
  656. label "n1"
  657. properties "_networkx_list_start"
  658. properties "element"
  659. ]
  660. ]
  661. """
  662. ).encode("ascii")
  663. )
  664. f.seek(0)
  665. graph = nx.read_gml(f)
  666. assert graph.nodes(data=True)["n1"] == {"properties": ["element"]}
  667. @pytest.mark.parametrize("coll", ([], ()))
  668. def test_stringize_empty_list_tuple(coll):
  669. G = nx.path_graph(2)
  670. G.nodes[0]["test"] = coll # test serializing an empty collection
  671. f = io.BytesIO()
  672. nx.write_gml(G, f) # Smoke test - should not raise
  673. f.seek(0)
  674. H = nx.read_gml(f)
  675. assert H.nodes["0"]["test"] == coll # Check empty list round-trips properly
  676. # Check full round-tripping. Note that nodes are loaded as strings by
  677. # default, so there needs to be some remapping prior to comparison
  678. H = nx.relabel_nodes(H, {"0": 0, "1": 1})
  679. assert nx.utils.graphs_equal(G, H)
  680. # Same as above, but use destringizer for node remapping. Should have no
  681. # effect on node attr
  682. f.seek(0)
  683. H = nx.read_gml(f, destringizer=int)
  684. assert nx.utils.graphs_equal(G, H)