test_agraph.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """Unit tests for PyGraphviz interface."""
  2. import warnings
  3. import pytest
  4. import networkx as nx
  5. from networkx.utils import edges_equal, graphs_equal, nodes_equal
  6. pygraphviz = pytest.importorskip("pygraphviz")
  7. class TestAGraph:
  8. def build_graph(self, G):
  9. edges = [("A", "B"), ("A", "C"), ("A", "C"), ("B", "C"), ("A", "D")]
  10. G.add_edges_from(edges)
  11. G.add_node("E")
  12. G.graph["metal"] = "bronze"
  13. return G
  14. def assert_equal(self, G1, G2):
  15. assert nodes_equal(G1.nodes(), G2.nodes())
  16. assert edges_equal(G1.edges(), G2.edges(), directed=G1.is_directed())
  17. assert G1.graph["metal"] == G2.graph["metal"]
  18. @pytest.mark.parametrize(
  19. "G", (nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph())
  20. )
  21. def test_agraph_roundtripping(self, G, tmp_path):
  22. G = self.build_graph(G)
  23. A = nx.nx_agraph.to_agraph(G)
  24. H = nx.nx_agraph.from_agraph(A)
  25. self.assert_equal(G, H)
  26. fname = tmp_path / "test.dot"
  27. nx.drawing.nx_agraph.write_dot(H, fname)
  28. Hin = nx.nx_agraph.read_dot(fname)
  29. self.assert_equal(H, Hin)
  30. fname = tmp_path / "fh_test.dot"
  31. with open(fname, "w") as fh:
  32. nx.drawing.nx_agraph.write_dot(H, fh)
  33. with open(fname) as fh:
  34. Hin = nx.nx_agraph.read_dot(fh)
  35. self.assert_equal(H, Hin)
  36. def test_from_agraph_name(self):
  37. G = nx.Graph(name="test")
  38. A = nx.nx_agraph.to_agraph(G)
  39. H = nx.nx_agraph.from_agraph(A)
  40. assert G.name == "test"
  41. @pytest.mark.parametrize(
  42. "graph_class", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
  43. )
  44. def test_from_agraph_create_using(self, graph_class):
  45. G = nx.path_graph(3)
  46. A = nx.nx_agraph.to_agraph(G)
  47. H = nx.nx_agraph.from_agraph(A, create_using=graph_class)
  48. assert isinstance(H, graph_class)
  49. def test_from_agraph_named_edges(self):
  50. # Create an AGraph from an existing (non-multi) Graph
  51. G = nx.Graph()
  52. G.add_nodes_from([0, 1])
  53. A = nx.nx_agraph.to_agraph(G)
  54. # Add edge (+ name, given by key) to the AGraph
  55. A.add_edge(0, 1, key="foo")
  56. # Verify a.name roundtrips out to 'key' in from_agraph
  57. H = nx.nx_agraph.from_agraph(A)
  58. assert isinstance(H, nx.Graph)
  59. assert ("0", "1", {"key": "foo"}) in H.edges(data=True)
  60. def test_to_agraph_with_nodedata(self):
  61. G = nx.Graph()
  62. G.add_node(1, color="red")
  63. A = nx.nx_agraph.to_agraph(G)
  64. assert dict(A.nodes()[0].attr) == {"color": "red"}
  65. @pytest.mark.parametrize("graph_class", (nx.Graph, nx.MultiGraph))
  66. def test_to_agraph_with_edgedata(self, graph_class):
  67. G = graph_class()
  68. G.add_nodes_from([0, 1])
  69. G.add_edge(0, 1, color="yellow")
  70. A = nx.nx_agraph.to_agraph(G)
  71. assert dict(A.edges()[0].attr) == {"color": "yellow"}
  72. def test_view_pygraphviz_path(self, tmp_path):
  73. G = nx.complete_graph(3)
  74. input_path = str(tmp_path / "graph.png")
  75. out_path, A = nx.nx_agraph.view_pygraphviz(G, path=input_path, show=False)
  76. assert out_path == input_path
  77. # Ensure file is not empty
  78. with open(input_path, "rb") as fh:
  79. data = fh.read()
  80. assert len(data) > 0
  81. def test_view_pygraphviz_file_suffix(self, tmp_path):
  82. G = nx.complete_graph(3)
  83. path, A = nx.nx_agraph.view_pygraphviz(G, suffix=1, show=False)
  84. assert path[-6:] == "_1.png"
  85. def test_view_pygraphviz(self):
  86. G = nx.Graph() # "An empty graph cannot be drawn."
  87. pytest.raises(nx.NetworkXException, nx.nx_agraph.view_pygraphviz, G)
  88. G = nx.barbell_graph(4, 6)
  89. nx.nx_agraph.view_pygraphviz(G, show=False)
  90. def test_view_pygraphviz_edgelabel(self):
  91. G = nx.Graph()
  92. G.add_edge(1, 2, weight=7)
  93. G.add_edge(2, 3, weight=8)
  94. path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="weight", show=False)
  95. for edge in A.edges():
  96. assert edge.attr["weight"] in ("7", "8")
  97. def test_view_pygraphviz_callable_edgelabel(self):
  98. G = nx.complete_graph(3)
  99. def foo_label(data):
  100. return "foo"
  101. path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel=foo_label, show=False)
  102. for edge in A.edges():
  103. assert edge.attr["label"] == "foo"
  104. def test_view_pygraphviz_multigraph_edgelabels(self):
  105. G = nx.MultiGraph()
  106. G.add_edge(0, 1, key=0, name="left_fork")
  107. G.add_edge(0, 1, key=1, name="right_fork")
  108. path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="name", show=False)
  109. edges = A.edges()
  110. assert len(edges) == 2
  111. for edge in edges:
  112. assert edge.attr["label"].strip() in ("left_fork", "right_fork")
  113. def test_graph_with_reserved_keywords(self):
  114. # test attribute/keyword clash case for #1582
  115. # node: n
  116. # edges: u,v
  117. G = nx.Graph()
  118. G = self.build_graph(G)
  119. G.nodes["E"]["n"] = "keyword"
  120. G.edges[("A", "B")]["u"] = "keyword"
  121. G.edges[("A", "B")]["v"] = "keyword"
  122. A = nx.nx_agraph.to_agraph(G)
  123. def test_view_pygraphviz_no_added_attrs_to_input(self):
  124. G = nx.complete_graph(2)
  125. path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
  126. assert G.graph == {}
  127. @pytest.mark.xfail(reason="known bug in clean_attrs")
  128. def test_view_pygraphviz_leaves_input_graph_unmodified(self):
  129. G = nx.complete_graph(2)
  130. # Add entries to graph dict that to_agraph handles specially
  131. G.graph["node"] = {"width": "0.80"}
  132. G.graph["edge"] = {"fontsize": "14"}
  133. path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
  134. assert G.graph == {"node": {"width": "0.80"}, "edge": {"fontsize": "14"}}
  135. def test_graph_with_AGraph_attrs(self):
  136. G = nx.complete_graph(2)
  137. # Add entries to graph dict that to_agraph handles specially
  138. G.graph["node"] = {"width": "0.80"}
  139. G.graph["edge"] = {"fontsize": "14"}
  140. path, A = nx.nx_agraph.view_pygraphviz(G, show=False)
  141. # Ensure user-specified values are not lost
  142. assert dict(A.node_attr)["width"] == "0.80"
  143. assert dict(A.edge_attr)["fontsize"] == "14"
  144. def test_round_trip_empty_graph(self):
  145. G = nx.Graph()
  146. A = nx.nx_agraph.to_agraph(G)
  147. H = nx.nx_agraph.from_agraph(A)
  148. assert graphs_equal(G, H)
  149. AA = nx.nx_agraph.to_agraph(H)
  150. HH = nx.nx_agraph.from_agraph(AA)
  151. assert graphs_equal(H, HH)
  152. assert graphs_equal(G, HH)
  153. @pytest.mark.xfail(reason="integer->string node conversion in round trip")
  154. def test_round_trip_integer_nodes(self):
  155. G = nx.complete_graph(3)
  156. A = nx.nx_agraph.to_agraph(G)
  157. H = nx.nx_agraph.from_agraph(A)
  158. assert graphs_equal(G, H)
  159. def test_graphviz_alias(self):
  160. G = self.build_graph(nx.Graph())
  161. pos_graphviz = nx.nx_agraph.graphviz_layout(G)
  162. pos_pygraphviz = nx.nx_agraph.pygraphviz_layout(G)
  163. assert pos_graphviz == pos_pygraphviz
  164. @pytest.mark.parametrize("root", range(5))
  165. def test_pygraphviz_layout_root(self, root):
  166. # NOTE: test depends on layout prog being deterministic
  167. G = nx.complete_graph(5)
  168. A = nx.nx_agraph.to_agraph(G)
  169. # Get layout with root arg is not None
  170. pygv_layout = nx.nx_agraph.pygraphviz_layout(G, prog="circo", root=root)
  171. # Equivalent layout directly on AGraph
  172. A.layout(args=f"-Groot={root}", prog="circo")
  173. # Parse AGraph layout
  174. a1_pos = tuple(float(v) for v in dict(A.get_node("1").attr)["pos"].split(","))
  175. assert pygv_layout[1] == a1_pos
  176. def test_2d_layout(self):
  177. G = nx.Graph()
  178. G = self.build_graph(G)
  179. G.graph["dimen"] = 2
  180. pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato")
  181. pos = list(pos.values())
  182. assert len(pos) == 5
  183. assert len(pos[0]) == 2
  184. def test_3d_layout(self):
  185. G = nx.Graph()
  186. G = self.build_graph(G)
  187. G.graph["dimen"] = 3
  188. pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato")
  189. pos = list(pos.values())
  190. assert len(pos) == 5
  191. assert len(pos[0]) == 3
  192. def test_no_warnings_raised(self):
  193. # Test that no warnings are raised when Networkx graph
  194. # is converted to Pygraphviz graph and 'pos'
  195. # attribute is given
  196. G = nx.Graph()
  197. G.add_node(0, pos=(0, 0))
  198. G.add_node(1, pos=(1, 1))
  199. A = nx.nx_agraph.to_agraph(G)
  200. with warnings.catch_warnings(record=True) as record:
  201. A.layout()
  202. assert len(record) == 0