Skip to content

Commit 70b7126

Browse files
Potential fix to the tooltip with special characters bug (#1203)
* Potential solution * Push update * Fixed linting issues * Fix to linting issues * minor fix * using serde_json instead of manually escaping * fix import, and added test case * Updated test logic * updated test to check to_dot * fix clippy issues * created release notes * fix release notes * fix linting issue in test * remove unused import * fixing quotes issue * fixing "\" issue and updating tests * updating more test to have quotes * Update dot_utils.rs * Apply suggestions from code review * Collect vector of results in Rust * Fix error handling * Update releasenotes/notes/fix-graphviz-draw-tooltip-3f697d71c4b79e60.yaml * Update releasenotes/notes/fix-graphviz-draw-tooltip-3f697d71c4b79e60.yaml * Update releasenotes/notes/fix-graphviz-draw-tooltip-3f697d71c4b79e60.yaml --------- Co-authored-by: Ivan Carvalho <[email protected]>
1 parent 77474a2 commit 70b7126

File tree

5 files changed

+67
-20
lines changed

5 files changed

+67
-20
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
fixes:
3+
- |
4+
:func:`.graphviz_draw` can now handle special characters
5+
6+
.. jupyter-execute::
7+
8+
import rustworkx as rx
9+
from rustworkx.visualization import graphviz_draw
10+
11+
graphviz_draw(
12+
rx.generators.path_graph(2),
13+
node_attr_fn=lambda x: {"label": "the\nlabel", "tooltip": "the\ntooltip"},
14+
)

src/dot_utils.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,18 @@ fn attr_map_to_string<'a>(
8282
if attrs.is_empty() {
8383
return Ok("".to_string());
8484
}
85-
8685
let attr_string = attrs
8786
.iter()
8887
.map(|(key, value)| {
89-
if key == "label" {
90-
format!("{}=\"{}\"", key, value)
91-
} else {
92-
format!("{}={}", key, value)
93-
}
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))
9495
})
95-
.collect::<Vec<String>>()
96+
.collect::<PyResult<Vec<String>>>()?
9697
.join(", ");
9798
Ok(format!("[{}]", attr_string))
9899
}

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: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import subprocess
1515
import tempfile
1616
import unittest
17-
1817
import rustworkx
1918
from rustworkx.visualization import graphviz_draw
2019

@@ -150,3 +149,36 @@ def test_filename(self):
150149
self.assertTrue(os.path.isfile("test_graphviz_filename.svg"))
151150
if not SAVE_IMAGES:
152151
self.addCleanup(os.remove, "test_graphviz_filename.svg")
152+
153+
def test_escape_sequences(self):
154+
# Create a simple graph
155+
graph = rustworkx.generators.path_graph(2)
156+
157+
escape_sequences = {
158+
"\\n": "\n", # Newline
159+
"\\t": "\t", # Horizontal tab
160+
"\\'": "'", # Single quote
161+
'\\"': '"', # Double quote
162+
"\\\\": "\\", # Backslash
163+
"\\r": "\r", # Carriage return
164+
"\\b": "\b", # Backspace
165+
"\\f": "\f", # Form feed
166+
}
167+
168+
for escaped_seq, raw_seq in escape_sequences.items():
169+
170+
def node_attr(node):
171+
"""
172+
Define node attributes including escape sequences for labels and tooltips.
173+
"""
174+
label = f"label{escaped_seq}"
175+
tooltip = f"tooltip{escaped_seq}"
176+
return {"label": label, "tooltip": tooltip}
177+
178+
# Draw the graph using graphviz_draw
179+
dot_str = graph.to_dot(node_attr)
180+
181+
# Assert that the escape sequences are correctly placed and escaped in the dot string
182+
self.assertIn(
183+
escaped_seq, dot_str, f"Escape sequence {escaped_seq} not found in dot output"
184+
)

0 commit comments

Comments
 (0)