Skip to content

Commit ffcd38f

Browse files
authored
Fix dot file generation (#1229)
This commit fixes a regression introduced in #1203. That PR was attempting to fix missing character escaping in some string fields but it was doing so too eagerly. This would result in invalid dot files being produced for users upgrading from rustworkx < 0.15.0 that were wrapping strings in quotes as needed previously. For example if you were setting `'color="#aaaaaa"'` previously before this would become `color="\"#aaaaaa\""` after #1203. In order to quickly release a 0.15.1 this commit reverts the dot generation component of #1203 but updates the code to wrap tooltip in addition to label which was what the original bug reported. In 0.16.0 we should investigate adding a flag to control the escaping behavior of the function to either decide to wrap values in quotes or not.
1 parent 0d648b2 commit ffcd38f

File tree

5 files changed

+91
-20
lines changed

5 files changed

+91
-20
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed an issue in the :func:`~.graphviz_draw`, :meth:`.PyGraph.to_dot`, and
5+
:meth:`.PyDiGraph.to_dot` which was incorrectly escaping strings when
6+
upgrading to 0.15.0. In earlier versions of rustworkx if you manually
7+
placed quotes in a string for an attr callback get that to pass through to
8+
the output dot file this was incorrectly being converted in rustworkx 0.15.0
9+
to duplicate the quotes and escape them. For example, if you defined a
10+
callback like::
11+
12+
def color_node(_node):
13+
return {
14+
"color": '"#422952"'
15+
}
16+
17+
to set the color attribute in the output dot file with the string
18+
`"#422952"` (with the quotes) this was incorrectly being converted to
19+
`\"#422952\"`. This no longer occurs, in rustworkx 0.16.0 there will likely
20+
be additional options exposed in :func:`~.graphviz_draw`,
21+
:meth:`.PyGraph.to_dot`, and :meth:`.PyDiGraph.to_dot` to expose further
22+
options around this.

src/dot_utils.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ where
6464
Ok(())
6565
}
6666

67+
static ATTRS_TO_ESCAPE: [&str; 2] = ["label", "tooltip"];
68+
6769
/// Convert an attr map to an output string
6870
fn attr_map_to_string<'a>(
6971
py: Python,
@@ -85,15 +87,13 @@ fn attr_map_to_string<'a>(
8587
let attr_string = attrs
8688
.iter()
8789
.map(|(key, value)| {
88-
let escaped_value = serde_json::to_string(value).map_err(|_err| {
89-
pyo3::exceptions::PyValueError::new_err("could not escape character")
90-
})?;
91-
let escaped_value = &escaped_value.get(1..escaped_value.len() - 1).ok_or(
92-
pyo3::exceptions::PyValueError::new_err("could not escape character"),
93-
)?;
94-
Ok(format!("{}=\"{}\"", key, escaped_value))
90+
if ATTRS_TO_ESCAPE.contains(&key.as_str()) {
91+
format!("{}=\"{}\"", key, value)
92+
} else {
93+
format!("{}={}", key, value)
94+
}
9595
})
96-
.collect::<PyResult<Vec<String>>>()?
96+
.collect::<Vec<String>>()
9797
.join(", ");
9898
Ok(format!("[{}]", attr_string))
9999
}

tests/digraph/test_dot.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ def test_digraph_to_dot_to_file(self):
4343
)
4444
graph.add_edge(0, 1, dict(label="1", name="1"))
4545
expected = (
46-
'digraph {\n0 [color="black", fillcolor="green", label="a", '
47-
'style="filled"];\n1 [color="black", fillcolor="red", label="a", '
48-
'style="filled"];\n0 -> 1 [label="1", name="1"];\n}\n'
46+
'digraph {\n0 [color=black, fillcolor=green, label="a", '
47+
'style=filled];\n1 [color=black, fillcolor=red, label="a", '
48+
'style=filled];\n0 -> 1 [label="1", name=1];\n}\n'
4949
)
5050
res = graph.to_dot(lambda node: node, lambda edge: edge, filename=self.path)
5151
self.addCleanup(os.remove, self.path)

tests/graph/test_dot.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ def test_graph_to_dot(self):
4343
)
4444
graph.add_edge(0, 1, dict(label="1", name="1"))
4545
expected = (
46-
'graph {\n0 [color="black", fillcolor="green", label="a", style="filled"'
47-
'];\n1 [color="black", fillcolor="red", label="a", style="filled"];'
48-
'\n0 -- 1 [label="1", name="1"];\n}\n'
46+
'graph {\n0 [color=black, fillcolor=green, label="a", style=filled'
47+
'];\n1 [color=black, fillcolor=red, label="a", style=filled];'
48+
'\n0 -- 1 [label="1", name=1];\n}\n'
4949
)
5050
res = graph.to_dot(lambda node: node, lambda edge: edge)
5151
self.assertEqual(expected, res)
@@ -70,9 +70,9 @@ def test_digraph_to_dot(self):
7070
)
7171
graph.add_edge(0, 1, dict(label="1", name="1"))
7272
expected = (
73-
'digraph {\n0 [color="black", fillcolor="green", label="a", '
74-
'style="filled"];\n1 [color="black", fillcolor="red", label="a", '
75-
'style="filled"];\n0 -> 1 [label="1", name="1"];\n}\n'
73+
'digraph {\n0 [color=black, fillcolor=green, label="a", '
74+
'style=filled];\n1 [color=black, fillcolor=red, label="a", '
75+
'style=filled];\n0 -> 1 [label="1", name=1];\n}\n'
7676
)
7777
res = graph.to_dot(lambda node: node, lambda edge: edge)
7878
self.assertEqual(expected, res)
@@ -97,9 +97,9 @@ def test_graph_to_dot_to_file(self):
9797
)
9898
graph.add_edge(0, 1, dict(label="1", name="1"))
9999
expected = (
100-
'graph {\n0 [color="black", fillcolor="green", label="a", '
101-
'style="filled"];\n1 [color="black", fillcolor="red", label="a", '
102-
'style="filled"];\n0 -- 1 [label="1", name="1"];\n}\n'
100+
'graph {\n0 [color=black, fillcolor=green, label="a", '
101+
'style=filled];\n1 [color=black, fillcolor=red, label="a", '
102+
'style=filled];\n0 -- 1 [label="1", name=1];\n}\n'
103103
)
104104
res = graph.to_dot(lambda node: node, lambda edge: edge, filename=self.path)
105105
self.addCleanup(os.remove, self.path)

tests/visualization/test_graphviz.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,55 @@ def test_filename(self):
150150
if not SAVE_IMAGES:
151151
self.addCleanup(os.remove, "test_graphviz_filename.svg")
152152

153+
def test_qiskit_style_visualization(self):
154+
"""This test is to test visualizations like qiskit performs which regressed in 0.15.0."""
155+
graph = rustworkx.generators.cycle_graph(4)
156+
colors = ["#422952", "#492d58", "#4f305c", "#5e3767"]
157+
edge_colors = ["#4d2f5b", "#693d6f", "#995a88", "#382449"]
158+
pos = [(0, 0), (0, 1), (1, 0), (1, 1)]
159+
for node in graph.node_indices():
160+
graph[node] = node
161+
162+
for edge in graph.edge_indices():
163+
graph.update_edge_by_index(edge, edge)
164+
165+
def color_node(node):
166+
out_dict = {
167+
"label": str(node),
168+
"color": f'"{colors[node]}"',
169+
"pos": f'"{pos[node][0]}, {pos[node][1]}"',
170+
"fontname": '"DejaVu Sans"',
171+
"pin": "True",
172+
"shape": "circle",
173+
"style": "filled",
174+
"fillcolor": f'"{colors[node]}"',
175+
"fontcolor": "white",
176+
"fontsize": "10",
177+
"height": "0.322",
178+
"fixedsize": "True",
179+
}
180+
return out_dict
181+
182+
def color_edge(edge):
183+
out_dict = {
184+
"color": f'"{edge_colors[edge]}"',
185+
"fillcolor": f'"{edge_colors[edge]}"',
186+
"penwidth": str(5),
187+
}
188+
return out_dict
189+
190+
graphviz_draw(
191+
graph,
192+
node_attr_fn=color_node,
193+
edge_attr_fn=color_edge,
194+
filename="test_qiskit_style_visualization.png",
195+
image_type="png",
196+
method="neato",
197+
)
198+
self.assertTrue(os.path.isfile("test_qiskit_style_visualization.png"))
199+
if not SAVE_IMAGES:
200+
self.addCleanup(os.remove, "test_qiskit_style_visualization.png")
201+
153202
def test_escape_sequences(self):
154203
# Create a simple graph
155204
graph = rustworkx.generators.path_graph(2)

0 commit comments

Comments
 (0)