Repository: qBraid/pyqasm

Issue:
[BUG]  Inconsistent `draw()` and `mpl_draw()` behavior
There are inconsistent display issues when using `pyqasm.draw()` and `pyqasm.printer.mpl_draw()` in Jupyter notebooks:

**`pyqasm.draw()` behavior**:

- The circuit diagram displays correctly the first time it’s called.
- Subsequent calls produce no diagram output.
- Restarting the kernel resets this behavior — it works once and then fails on following calls.

https://github.com/user-attachments/assets/68e7c33b-3f44-4adc-b050-3812d81efb63

**`pyqasm.printer.mpl_draw()` behavior**:

- The circuit diagram displays twice on the first call.
- Subsequent calls show the diagram only once, as expected.
- After restarting the kernel, the double-display issue repeats on the first call.

https://github.com/user-attachments/assets/f4f0df77-ff67-4b36-bad5-a7c0c6bf4da0

Discussion:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/qBraid/pyqasm/pull/168?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qBraid) Report
Attention: Patch coverage is `42.85714%` with `4 lines` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/qBraid/pyqasm/pull/168?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qBraid) | Patch % | Lines |
|---|---|---|
| [src/pyqasm/printer.py](https://app.codecov.io/gh/qBraid/pyqasm/pull/168?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qBraid#diff-c3JjL3B5cWFzbS9wcmludGVyLnB5) | 42.85% | [4 Missing :warning: ](https://app.codecov.io/gh/qBraid/pyqasm/pull/168?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=qBraid) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)

Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48cef08..0d4cea9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,7 @@ barrier q3[0];
 ### Removed
 
 ### Fixed
+- Resolved the inconsistency in `pyqasm.printer.draw` and `pyqasm.printer.mpl_draw` behaviour for multiple function calls. See issue [#165](https://github.com/qBraid/pyqasm/issues/165) for bug details. ([#168](https://github.com/qBraid/pyqasm/pull/168))
 
 ### Dependencies
 
diff --git a/src/pyqasm/printer.py b/src/pyqasm/printer.py
index 4560e59..5da4d67 100644
--- a/src/pyqasm/printer.py
+++ b/src/pyqasm/printer.py
@@ -89,13 +89,21 @@ def draw(
         program = loads(program)
 
     if output == "mpl":
-        mpl_draw(program, idle_wires=idle_wires, **kwargs)
+        _ = mpl_draw(program, idle_wires=idle_wires, external_draw=False, **kwargs)
+
+        import matplotlib.pyplot as plt
+
+        if not plt.isinteractive():
+            plt.show()
     else:
         raise ValueError(f"Unsupported output format: {output}")
 
 
-def mpl_draw(
-    program: str | QasmModule, idle_wires: bool = True, filename: str | Path | None = None
+def mpl_draw(  # pylint: disable=too-many-locals
+    program: str | QasmModule,
+    idle_wires: bool = True,
+    filename: str | Path | None = None,
+    external_draw: bool = True,
 ) -> plt.Figure:
     """Internal matplotlib drawing implementation."""
     if isinstance(program, str):
@@ -142,6 +150,9 @@ def mpl_draw(
     if filename is not None:
         plt.savefig(filename, bbox_inches="tight", dpi=300)
 
+    if external_draw:
+        plt.close(fig)
+
     return fig
 
 


Test changes:
diff --git a/tests/visualization/test_mpl_draw.py b/tests/visualization/test_mpl_draw.py
index 543a3e1..c5b2b38 100644
--- a/tests/visualization/test_mpl_draw.py
+++ b/tests/visualization/test_mpl_draw.py
@@ -16,11 +16,13 @@
 Tests for the QASM printer module.
 """
 
+import os
+import unittest.mock
+
 import pytest
 
-from pyqasm import draw
 from pyqasm.entrypoint import loads
-from pyqasm.printer import mpl_draw
+from pyqasm.printer import draw, mpl_draw
 
 pytest.importorskip("matplotlib", reason="Matplotlib not installed.")
 
@@ -111,7 +113,7 @@ def test_draw_bell():
     cnot q[0], q[1];
     b = measure q;
     """
-    fig = draw(qasm3, output="mpl")
+    fig = mpl_draw(qasm3)
     return fig
 
 
@@ -134,5 +136,72 @@ def test_draw_misc_ops():
     measure q;
     reset q;
     """
-    fig = draw(qasm3, output="mpl")
+    fig = mpl_draw(qasm3)
     return fig
+
+
+def test_draw_raises_unsupported_format_error():
+    """Test that an error is raised for unsupported formats."""
+    qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    h q;
+    cnot q[0], q[1];
+    measure q;
+    """
+    circ = loads(qasm)
+
+    with pytest.raises(ValueError, match=r"Unsupported output format"):
+        draw(circ, output="unsupported_format")
+
+
+def test_saved_figure():
+    """Test that the saved figure is not None."""
+    qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    h q;
+    cnot q[0], q[1];
+    measure q;
+    """
+
+    circ = loads(qasm)
+    fig = mpl_draw(circ, filename="test_img.png")
+    assert fig is not None
+
+    # check the file exists
+    assert os.path.exists("test_img.png")
+
+    # clean up the file
+    os.remove("test_img.png")
+
+
+@pytest.mark.parametrize("is_interactive", [True, False])
+def test_matplotlib_interactive_behavior(is_interactive):
+    """Test plt.show() behavior depending on matplotlib's interactive mode."""
+    qasm = """
+    OPENQASM 3.0;
+    include "stdgates.inc";
+    qubit[2] q;
+    h q;
+    cnot q[0], q[1];
+    measure q;
+    """
+
+    with unittest.mock.patch(
+        "matplotlib.pyplot.isinteractive", return_value=is_interactive
+    ) as mock_isinteractive:
+        with unittest.mock.patch("matplotlib.pyplot.show") as mock_show:
+            # Call draw function
+            draw(qasm, output="mpl")
+
+            # Verify isinteractive was called
+            mock_isinteractive.assert_called()
+
+            # When not interactive, show should be called; otherwise it shouldn't
+            if is_interactive:
+                mock_show.assert_not_called()
+            else:
+                mock_show.assert_called_once()