Repository: Qiskit/rustworkx (language: python)

Issue:
graphviz draw tooltip with special characters
### Information

- **rustworkx version**:  0.12.1
- **Python version**: 3.9.13
- **Rust version**: NA
- **Operating system**: Windows 10

### What is the current behavior?

Adding a `\n` or `:` character to the tooltip throws this error:

```
Error: <stdin>: syntax error in line 4 near ']'      
Traceback (most recent call last):
  File main.py", line 6, in <module>
    graphviz_draw(
  File "venv\lib\site-packages\rustworkx\visualization\graphviz.py", line 199, in graphviz_draw
    subprocess.run(
  File "AppData\Local\Programs\Python\Python39\lib\subprocess.py", line 528, in run
    raise CalledProcessError(retcode, process.args,  
subprocess.CalledProcessError: Command '['dot', '-T', 'svg', '-o', 'graph.svg']' returned non-zero exit status 1.
```

### What is the expected behavior?
I would like the tooltip to support special characters such as `\n` and `:`, like label does.
In http://magjac.com/graphviz-visual-editor/, I'm allowed to do that with:
```
digraph {
  label="Graph Label"
  Node1 [tooltip="Node1 Tooltip\nlinebreak in tooltip: it's fine"]
}
```

### Steps to reproduce the problem
```
import rustworkx as rx
from rustworkx.visualization import graphviz_draw

graphviz_draw(
    rx.generators.path_graph(2),
    filename="graph.svg",
    image_type="svg",
    node_attr_fn=lambda x: {"label": "the\nlabel", "tooltip": "the\ntooltip"},
)
```




Discussion / hints:
I took a quick look at this, the error is we're not wrapping `tooltip` in double quotes so it's getting inserted as a line break and graphviz isn't able to parse it. This is the output of calling `.to_dot()` in your example:

```dot
graph {
0 [label="the
label", tooltip=the
tooltip];
1 [label="the
label", tooltip=the
tooltip];
0 -- 1 ;
}
```

The simple fix is to update: https://github.com/Qiskit/rustworkx/blob/main/src/dot_utils.rs#L89 to be `label` or `tooltip`. But I wonder if there are more fields that take strings and need to be wrapped too?

For reference, the tooltip documentation https://graphviz.org/docs/attrs/tooltip/ 
Heyy I saw this issue at the unitaryhacks .
This seems like a straightforward fix, will start on this, kindly assign this to me, will ask for doubts if something comes up.


Fix patch:
diff --git a/releasenotes/notes/fix-graphviz-draw-tooltip-3f697d71c4b79e60.yaml b/releasenotes/notes/fix-graphviz-draw-tooltip-3f697d71c4b79e60.yaml
new file mode 100644
index 000000000..0d7b184c4
--- /dev/null
+++ b/releasenotes/notes/fix-graphviz-draw-tooltip-3f697d71c4b79e60.yaml
@@ -0,0 +1,14 @@
+---
+fixes:
+  - |
+      :func:`.graphviz_draw` can now handle special characters
+
+          .. jupyter-execute::
+
+                import rustworkx as rx
+                from rustworkx.visualization import graphviz_draw
+
+                graphviz_draw(
+                    rx.generators.path_graph(2),
+                    node_attr_fn=lambda x: {"label": "the\nlabel", "tooltip": "the\ntooltip"},
+                )
diff --git a/src/dot_utils.rs b/src/dot_utils.rs
index 3411c2f3e..af1f708eb 100644
--- a/src/dot_utils.rs
+++ b/src/dot_utils.rs
@@ -82,17 +82,18 @@ fn attr_map_to_string<'a>(
     if attrs.is_empty() {
         return Ok("".to_string());
     }
-
     let attr_string = attrs
         .iter()
         .map(|(key, value)| {
-            if key == "label" {
-                format!("{}=\"{}\"", key, value)
-            } else {
-                format!("{}={}", key, value)
-            }
+            let escaped_value = serde_json::to_string(value).map_err(|_err| {
+                pyo3::exceptions::PyValueError::new_err("could not escape character")
+            })?;
+            let escaped_value = &escaped_value.get(1..escaped_value.len() - 1).ok_or(
+                pyo3::exceptions::PyValueError::new_err("could not escape character"),
+            )?;
+            Ok(format!("{}=\"{}\"", key, escaped_value))
         })
-        .collect::<Vec<String>>()
+        .collect::<PyResult<Vec<String>>>()?
         .join(", ");
     Ok(format!("[{}]", attr_string))
 }


Test patch:
diff --git a/tests/digraph/test_dot.py b/tests/digraph/test_dot.py
index 3d893a892..8d3b8917f 100644
--- a/tests/digraph/test_dot.py
+++ b/tests/digraph/test_dot.py
@@ -43,9 +43,9 @@ def test_digraph_to_dot_to_file(self):
         )
         graph.add_edge(0, 1, dict(label="1", name="1"))
         expected = (
-            'digraph {\n0 [color=black, fillcolor=green, label="a", '
-            'style=filled];\n1 [color=black, fillcolor=red, label="a", '
-            'style=filled];\n0 -> 1 [label="1", name=1];\n}\n'
+            'digraph {\n0 [color="black", fillcolor="green", label="a", '
+            'style="filled"];\n1 [color="black", fillcolor="red", label="a", '
+            'style="filled"];\n0 -> 1 [label="1", name="1"];\n}\n'
         )
         res = graph.to_dot(lambda node: node, lambda edge: edge, filename=self.path)
         self.addCleanup(os.remove, self.path)
diff --git a/tests/graph/test_dot.py b/tests/graph/test_dot.py
index 63c98804e..c2fb275e4 100644
--- a/tests/graph/test_dot.py
+++ b/tests/graph/test_dot.py
@@ -43,9 +43,9 @@ def test_graph_to_dot(self):
         )
         graph.add_edge(0, 1, dict(label="1", name="1"))
         expected = (
-            'graph {\n0 [color=black, fillcolor=green, label="a", style=filled'
-            '];\n1 [color=black, fillcolor=red, label="a", style=filled];'
-            '\n0 -- 1 [label="1", name=1];\n}\n'
+            'graph {\n0 [color="black", fillcolor="green", label="a", style="filled"'
+            '];\n1 [color="black", fillcolor="red", label="a", style="filled"];'
+            '\n0 -- 1 [label="1", name="1"];\n}\n'
         )
         res = graph.to_dot(lambda node: node, lambda edge: edge)
         self.assertEqual(expected, res)
@@ -70,9 +70,9 @@ def test_digraph_to_dot(self):
         )
         graph.add_edge(0, 1, dict(label="1", name="1"))
         expected = (
-            'digraph {\n0 [color=black, fillcolor=green, label="a", '
-            'style=filled];\n1 [color=black, fillcolor=red, label="a", '
-            'style=filled];\n0 -> 1 [label="1", name=1];\n}\n'
+            'digraph {\n0 [color="black", fillcolor="green", label="a", '
+            'style="filled"];\n1 [color="black", fillcolor="red", label="a", '
+            'style="filled"];\n0 -> 1 [label="1", name="1"];\n}\n'
         )
         res = graph.to_dot(lambda node: node, lambda edge: edge)
         self.assertEqual(expected, res)
@@ -97,9 +97,9 @@ def test_graph_to_dot_to_file(self):
         )
         graph.add_edge(0, 1, dict(label="1", name="1"))
         expected = (
-            'graph {\n0 [color=black, fillcolor=green, label="a", '
-            'style=filled];\n1 [color=black, fillcolor=red, label="a", '
-            'style=filled];\n0 -- 1 [label="1", name=1];\n}\n'
+            'graph {\n0 [color="black", fillcolor="green", label="a", '
+            'style="filled"];\n1 [color="black", fillcolor="red", label="a", '
+            'style="filled"];\n0 -- 1 [label="1", name="1"];\n}\n'
         )
         res = graph.to_dot(lambda node: node, lambda edge: edge, filename=self.path)
         self.addCleanup(os.remove, self.path)
diff --git a/tests/visualization/test_graphviz.py b/tests/visualization/test_graphviz.py
index ca1aa4765..4a6a472dc 100644
--- a/tests/visualization/test_graphviz.py
+++ b/tests/visualization/test_graphviz.py
@@ -14,7 +14,6 @@
 import subprocess
 import tempfile
 import unittest
-
 import rustworkx
 from rustworkx.visualization import graphviz_draw
 
@@ -150,3 +149,36 @@ def test_filename(self):
         self.assertTrue(os.path.isfile("test_graphviz_filename.svg"))
         if not SAVE_IMAGES:
             self.addCleanup(os.remove, "test_graphviz_filename.svg")
+
+    def test_escape_sequences(self):
+        # Create a simple graph
+        graph = rustworkx.generators.path_graph(2)
+
+        escape_sequences = {
+            "\\n": "\n",  # Newline
+            "\\t": "\t",  # Horizontal tab
+            "\\'": "'",  # Single quote
+            '\\"': '"',  # Double quote
+            "\\\\": "\\",  # Backslash
+            "\\r": "\r",  # Carriage return
+            "\\b": "\b",  # Backspace
+            "\\f": "\f",  # Form feed
+        }
+
+        for escaped_seq, raw_seq in escape_sequences.items():
+
+            def node_attr(node):
+                """
+                Define node attributes including escape sequences for labels and tooltips.
+                """
+                label = f"label{escaped_seq}"
+                tooltip = f"tooltip{escaped_seq}"
+                return {"label": label, "tooltip": tooltip}
+
+            # Draw the graph using graphviz_draw
+            dot_str = graph.to_dot(node_attr)
+
+            # Assert that the escape sequences are correctly placed and escaped in the dot string
+            self.assertIn(
+                escaped_seq, dot_str, f"Escape sequence {escaped_seq} not found in dot output"
+            )