test_gml.py 21 KB

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