Repository: PennyLaneAI/pennylane
Pull request #7133 (2025-03-21 15:49:50)

Prepare `tape_text` for further extension
**Context:**

Over time, `tape_text` accumulated complexity and grew into an unreadable, unmaintainable state. As it is harder to read, it is difficult to extend.

**Description of the Change:**

Pulls out code into helper functions and adds properties to private data structures for managing state.

**Benefits:**

Code is easier to read and maintain. We can demonstrate this with the cyclomatic complexity checker radon.

Before changes:

```
(pl) ➜  pennylane git:(master) radon cc pennylane/drawer/tape_text.py
pennylane/drawer/tape_text.py
    F 248:0 tape_text - E
    F 131:0 _add_op - C
    F 83:0 _add_cond_grouping_symbols - B
    F 221:0 _add_measurement - B
    F 68:0 _add_grouping_symbols - A
    F 110:0 _add_mid_measure_grouping_symbols - A
    F 187:0 _add_cwire_measurement_grouping_symbols - A
    F 204:0 _add_cwire_measurement - A
    F 164:0 _add_mid_measure_op - A
    C 46:0 _Config - A
```


After changes:
```
(pl) ➜  pennylane git:(clean-tape-text) radon cc pennylane/drawer/tape_text.py
pennylane/drawer/tape_text.py
    F 170:0 tape_text - B
    F 74:0 _initialize_wire_and_bit_totals - B
    F 105:0 _left_justify - A
    F 141:0 _add_layer_str_to_totals - A
    F 156:0 _finalize_layers - A
    F 90:0 _initialize_layer_str - A
    F 123:0 _add_to_finished_lines - A
    C 40:0 _Config - A
    M 64:4 _Config.wire_filler - A
    M 69:4 _Config.bit_filler - A
    C 33:0 _CurrentTotals - A
(pl) ➜  pennylane git:(master) radon cc pennylane/drawer/_add_obj.py
pennylane/drawer/_add_obj.py
    F 33:0 _add_cond_grouping_symbols - B
    F 216:0 _add_measurement - B
    F 60:0 _add_grouping_symbols - A
    F 75:0 _add_mid_measure_grouping_symbols - A
    F 124:0 _add_op - A
    F 181:0 _add_cwire_measurement_grouping_symbols - A
    F 198:0 _add_cwire_measurement - A
    F 113:0 _add_controlled - A
    F 144:0 _add_mid_measure_op - A
    F 159:0 _add_tape - A
    F 98:0 _add_obj - A
    F 105:0 _add_cond - A
```

So it's been moved from a Failing `E` grade to an acceptable `B` grade.

**Possible Drawbacks:**


**Related GitHub Issues:**

[sc-87637]


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7133?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `96.55172%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 99.62%. Comparing base [(`248120f`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/248120f01aec920c75260be4ca935141a153bc25?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`4237f4f`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/4237f4fa3cd18f23118a2eef0382221b79e77b1d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 5 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7133?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/drawer/\_add\_obj.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7133?src=pr&el=tree&filepath=pennylane%2Fdrawer%2F_add_obj.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RyYXdlci9fYWRkX29iai5weQ==) | 94.26% | [7 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7133?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7133      +/-   ##
==========================================
+ Coverage   99.60%   99.62%   +0.01%     
==========================================
  Files         499      504       +5     
  Lines       47878    49063    +1185     
==========================================
+ Hits        47687    48877    +1190     
+ Misses        191      186       -5     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/7133?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 057401da0..44e77e5d3 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -775,6 +775,9 @@
 
 <h3>Internal changes ⚙️</h3>
 
+* Clean up logic in `qml.drawer.tape_text`
+  [(#7133)](https://github.com/PennyLaneAI/pennylane/pull/7133)
+
 * Add intermediate caching to `null.qubit` zero value generation to improve memory consumption for larger workloads.
   [(#7155)](https://github.com/PennyLaneAI/pennylane/pull/7155)
 
diff --git a/pennylane/drawer/_add_obj.py b/pennylane/drawer/_add_obj.py
new file mode 100644
index 000000000..3d3b51d40
--- /dev/null
+++ b/pennylane/drawer/_add_obj.py
@@ -0,0 +1,244 @@
+# Copyright 2018-2025 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+This module contains the _add_obj function for adding an object to the text drawer.
+"""
+# pylint: disable=unused-argument
+from functools import singledispatch
+
+import pennylane as qml
+from pennylane.measurements import (
+    CountsMP,
+    DensityMatrixMP,
+    ExpectationMP,
+    MidMeasureMP,
+    ProbabilityMP,
+    SampleMP,
+    StateMP,
+    VarianceMP,
+)
+
+
+def _add_cond_grouping_symbols(op, layer_str, config):
+    """Adds symbols indicating the extent of a given object for conditional
+    operators"""
+    n_wires = len(config.wire_map)
+
+    mapped_wires = [config.wire_map[w] for w in op.wires]
+    mapped_bits = [config.bit_map[m] for m in op.meas_val.measurements]
+    max_w = max(mapped_wires)
+    max_b = max(mapped_bits) + n_wires
+
+    ctrl_symbol = "╩" if config.cur_layer != config.cwire_layers[max(mapped_bits)][-1] else "╝"
+    layer_str[max_b] = f"═{ctrl_symbol}"
+
+    for w in range(max_w + 1, max(config.wire_map.values()) + 1):
+        layer_str[w] = "─║"
+
+    for b in range(n_wires, max_b):
+        if b - n_wires in mapped_bits:
+            intersection = "╣" if config.cur_layer == config.cwire_layers[b - n_wires][-1] else "╬"
+            layer_str[b] = f"═{intersection}"
+        else:
+            filler = " " if layer_str[b][-1] == " " else "═"
+            layer_str[b] = f"{filler}║"
+
+    return layer_str
+
+
+def _add_grouping_symbols(op, layer_str, config):  # pylint: disable=unused-argument
+    """Adds symbols indicating the extent of a given object."""
+
+    if len(op.wires) > 1:
+        mapped_wires = [config.wire_map[w] for w in op.wires]
+        min_w, max_w = min(mapped_wires), max(mapped_wires)
+        layer_str[min_w] = "╭"
+        layer_str[max_w] = "╰"
+
+        for w in range(min_w + 1, max_w):
+            layer_str[w] = "├" if w in mapped_wires else "│"
+
+    return layer_str
+
+
+def _add_mid_measure_grouping_symbols(op, layer_str, config):
+    """Adds symbols indicating the extent of a given object for mid-measure
+    operators"""
+    if op not in config.bit_map:
+        return layer_str
+
+    n_wires = len(config.wire_map)
+    mapped_wire = config.wire_map[op.wires[0]]
+    bit = config.bit_map[op] + n_wires
+    layer_str[bit] += " ╚"
+
+    for w in range(mapped_wire + 1, n_wires):
+        layer_str[w] += "─║"
+
+    for b in range(n_wires, bit):
+        filler = " " if layer_str[b][-1] == " " else "═"
+        layer_str[b] += f"{filler}║"
+
+    return layer_str
+
+
+@singledispatch
+def _add_obj(
+    obj, layer_str: list[str], config, tape_cache=None, skip_grouping_symbols=False
+) -> list[str]:
+    raise NotImplementedError(f"unable to draw object {obj}")
+
+
+@_add_obj.register
+def _add_cond(
+    obj: qml.ops.Conditional, layer_str, config, tape_cache=None, skip_grouping_symbols=False
+):
+    layer_str = _add_cond_grouping_symbols(obj, layer_str, config)
+    return _add_obj(obj.base, layer_str, config)
+
+
+@_add_obj.register
+def _add_controlled(
+    obj: qml.ops.Controlled, layer_str, config, tape_cache=None, skip_grouping_symbols=False
+):
+    layer_str = _add_grouping_symbols(obj, layer_str, config)
+    for w, val in zip(obj.control_wires, obj.control_values):
+        layer_str[config.wire_map[w]] += "●" if val else "○"
+
+    return _add_obj(obj.base, layer_str, config, skip_grouping_symbols=True)
+
+
+@_add_obj.register
+def _add_op(
+    obj: qml.operation.Operator, layer_str, config, tape_cache=None, skip_grouping_symbols=False
+):
+    """Updates ``layer_str`` with ``op`` operation."""
+    if not skip_grouping_symbols:
+        layer_str = _add_grouping_symbols(obj, layer_str, config)
+
+    label = obj.label(decimals=config.decimals, cache=config.cache).replace("\n", "")
+    if len(obj.wires) == 0:  # operation (e.g. barrier, snapshot) across all wires
+        n_wires = len(config.wire_map)
+        for i, s in enumerate(layer_str[:n_wires]):
+            layer_str[i] = s + label
+    else:
+        for w in obj.wires:
+            layer_str[config.wire_map[w]] += label
+
+    return layer_str
+
+
+@_add_obj.register
+def _add_mid_measure_op(
+    op: MidMeasureMP, layer_str, config, tape_cache=None, skip_grouping_symbols=False
+):
+    """Updates ``layer_str`` with ``op`` operation when ``op`` is a
+    ``qml.measurements.MidMeasureMP``."""
+    layer_str = _add_mid_measure_grouping_symbols(op, layer_str, config)
+    label = op.label(decimals=config.decimals, cache=config.cache).replace("\n", "")
+
+    for w in op.wires:
+        layer_str[config.wire_map[w]] += label
+
+    return layer_str
+
+
+@_add_obj.register
+def _add_tape(
+    obj: qml.tape.QuantumScript, layer_str, config, tape_cache, skip_grouping_symbols=False
+):
+    layer_str = _add_grouping_symbols(obj, layer_str, config)
+    label = f"Tape:{config.cache['tape_offset']+len(tape_cache)}"
+    for w in obj.wires:
+        layer_str[config.wire_map[w]] += label
+    tape_cache.append(obj)
+    return layer_str
+
+
+measurement_label_map = {
+    ExpectationMP: lambda label: f"<{label}>",
+    ProbabilityMP: lambda label: f"Probs[{label}]" if label else "Probs",
+    SampleMP: lambda label: f"Sample[{label}]" if label else "Sample",
+    CountsMP: lambda label: f"Counts[{label}]" if label else "Counts",
+    VarianceMP: lambda label: f"Var[{label}]",
+    StateMP: lambda label: "State",
+    DensityMatrixMP: lambda label: "DensityMatrix",
+}
+
+
+def _add_cwire_measurement_grouping_symbols(mcms, layer_str, config):
+    """Adds symbols indicating the extent of a given object for mid-circuit measurement
+    statistics."""
+    if len(mcms) > 1:
+        n_wires = len(config.wire_map)
+        mapped_bits = [config.bit_map[m] for m in mcms]
+        min_b, max_b = min(mapped_bits) + n_wires, max(mapped_bits) + n_wires
+
+        layer_str[min_b] = "╭"
+        layer_str[max_b] = "╰"
+
+        for b in range(min_b + 1, max_b):
+            layer_str[b] = "├" if b - n_wires in mapped_bits else "│"
+
+    return layer_str
+
+
+def _add_cwire_measurement(m, layer_str, config):
+    """Updates ``layer_str`` with the ``m`` measurement when it is used
+    for collecting mid-circuit measurement statistics."""
+    mcms = [v.measurements[0] for v in m.mv] if isinstance(m.mv, list) else m.mv.measurements
+    layer_str = _add_cwire_measurement_grouping_symbols(mcms, layer_str, config)
+
+    mv_label = "MCM"
+    meas_label = measurement_label_map[type(m)](mv_label)  # pylint: disable=protected-access
+
+    n_wires = len(config.wire_map)
+    for mcm in mcms:
+        ind = config.bit_map[mcm] + n_wires
+        layer_str[ind] += meas_label
+
+    return layer_str
+
+
+@_add_obj.register
+def _add_measurement(
+    m: qml.measurements.MeasurementProcess,
+    layer_str,
+    config,
+    tape_cache=None,
+    skip_grouping_symbols=False,
+):
+    """Updates ``layer_str`` with the ``m`` measurement."""
+    if m.mv is not None:
+        return _add_cwire_measurement(m, layer_str, config)
+
+    layer_str = _add_grouping_symbols(m, layer_str, config)
+
+    if m.obs is None:
+        obs_label = None
+    else:
+        obs_label = m.obs.label(decimals=config.decimals, cache=config.cache).replace("\n", "")
+    if type(m) in measurement_label_map:
+        meas_label = measurement_label_map[type(m)](obs_label)
+    else:
+        meas_label = str(m)
+
+    if len(m.wires) == 0:  # state or probability across all wires
+        n_wires = len(config.wire_map)
+        for i, s in enumerate(layer_str[:n_wires]):
+            layer_str[i] = s + meas_label
+
+    for w in m.wires:
+        layer_str[config.wire_map[w]] += meas_label
+    return layer_str
diff --git a/pennylane/drawer/tape_text.py b/pennylane/drawer/tape_text.py
index c8577432e..e884765fa 100644
--- a/pennylane/drawer/tape_text.py
+++ b/pennylane/drawer/tape_text.py
@@ -14,34 +14,28 @@
 """
 This module contains logic for the text based circuit drawer through the ``tape_text`` function.
 """
-# TODO: Fix the latter two pylint warnings
-# pylint: disable=too-many-arguments, too-many-branches, too-many-statements, protected-access
+# pylint: disable=too-many-arguments
 
-from dataclasses import dataclass
+from dataclasses import dataclass, field
 from typing import Optional
 
-import pennylane as qml
-from pennylane.measurements import (
-    CountsMP,
-    DensityMatrixMP,
-    ExpectationMP,
-    MidMeasureMP,
-    ProbabilityMP,
-    SampleMP,
-    StateMP,
-    VarianceMP,
-)
-
+from ._add_obj import _add_obj
 from .drawable_layers import drawable_layers
 from .utils import (
     convert_wire_order,
     cwire_connections,
     default_bit_map,
     transform_deferred_measurements_tape,
-    unwrap_controls,
 )
 
 
+@dataclass
+class _CurrentTotals:
+    finished_lines: list[str]
+    wire_totals: list[str]
+    bit_totals: list[str]
+
+
 @dataclass
 class _Config:
     """Dataclass containing attributes needed for updating the strings to be drawn for each layer"""
@@ -52,10 +46,12 @@ class _Config:
     bit_map: dict
     """Map between mid-circuit measurements and their corresponding bit in order"""
 
-    cur_layer: Optional[int] = None
+    num_op_layers: int
+
+    cur_layer: int = -1
     """Current layer index that is being updated"""
 
-    cwire_layers: Optional[list] = None
+    cwire_layers: list = field(default_factory=list)
     """A list of layers used (mid measure or conditional) for each classical wire."""
 
     decimals: Optional[int] = None
@@ -64,190 +60,152 @@ class _Config:
     cache: Optional[dict] = None
     """dictionary that carries information between label calls in the same drawing"""
 
+    @property
+    def wire_filler(self) -> str:
+        """The filler character for wires at the current layer."""
+        return "─" if self.cur_layer < self.num_op_layers else " "
 
-def _add_grouping_symbols(op, layer_str, config):  # pylint: disable=unused-argument
-    """Adds symbols indicating the extent of a given object."""
-
-    if len(op.wires) > 1:
-        mapped_wires = [config.wire_map[w] for w in op.wires]
-        min_w, max_w = min(mapped_wires), max(mapped_wires)
-        layer_str[min_w] = "╭"
-        layer_str[max_w] = "╰"
-
-        for w in range(min_w + 1, max_w):
-            layer_str[w] = "├" if w in mapped_wires else "│"
-
-    return layer_str
-
-
-def _add_cond_grouping_symbols(op, layer_str, config):
-    """Adds symbols indicating the extent of a given object for conditional
-    operators"""
-    n_wires = len(config.wire_map)
+    def bit_filler(self, bit, next_layer: bool = False) -> str:
+        """The filler character for bits at the current layer and the designated bit."""
+        layer = self.cur_layer + 1 if next_layer else self.cur_layer
+        b_is_occupied = self.cwire_layers[bit][0] < layer <= self.cwire_layers[bit][-1]
+        return "═" if self.cur_layer < self.num_op_layers and b_is_occupied else " "
 
-    mapped_wires = [config.wire_map[w] for w in op.wires]
-    mapped_bits = [config.bit_map[m] for m in op.meas_val.measurements]
-    max_w = max(mapped_wires)
-    max_b = max(mapped_bits) + n_wires
+    @property
+    def n_bits(self) -> int:
+        """The number of bits."""
+        return len(self.bit_map)
 
-    ctrl_symbol = "╩" if config.cur_layer != config.cwire_layers[max(mapped_bits)][-1] else "╝"
-    layer_str[max_b] = f"═{ctrl_symbol}"
+    @property
+    def n_wires(self) -> int:
+        """The number of wires."""
+        return len(self.wire_map)
 
-    for w in range(max_w + 1, max(config.wire_map.values()) + 1):
-        layer_str[w] = "─║"
 
-    for b in range(n_wires, max_b):
-        if b - n_wires in mapped_bits:
-            intersection = "╣" if config.cur_layer == config.cwire_layers[b - n_wires][-1] else "╬"
-            layer_str[b] = f"═{intersection}"
-        else:
-            filler = " " if layer_str[b][-1] == " " else "═"
-            layer_str[b] = f"{filler}║"
+def _initialize_wire_and_bit_totals(
+    config: _Config, show_wire_labels: bool
+) -> tuple[list[str], list[str]]:
+    """Initialize the wire totals and bit_totals with the required wire labels."""
+    if show_wire_labels:
+        wire_totals = [f"{wire}: " for wire in config.wire_map]
+    else:
+        wire_totals = [""] * config.n_wires
+    line_length = max(len(s) for s in wire_totals)
 
-    return layer_str
+    wire_totals = [s.rjust(line_length, " ") for s in wire_totals]
+    bit_totals = [" " * line_length for _ in config.bit_map]
+    return wire_totals, bit_totals
 
 
-def _add_mid_measure_grouping_symbols(op, layer_str, config):
-    """Adds symbols indicating the extent of a given object for mid-measure
-    operators"""
-    if op not in config.bit_map:
-        return layer_str
+def _initialize_layer_str(config: _Config) -> list[str]:
+    """Initialize the list of strings for a new layer.
 
-    n_wires = len(config.wire_map)
-    mapped_wire = config.wire_map[op.wires[0]]
-    bit = config.bit_map[op] + n_wires
-    layer_str[bit] += " ╚"
+    For example, if we have three wires and two classical wires, we will get:
 
-    for w in range(mapped_wire + 1, n_wires):
-        layer_str[w] += "─║"
+    .. code-block::
 
-    for b in range(n_wires, bit):
-        filler = " " if layer_str[b][-1] == " " else "═"
-        layer_str[b] += f"{filler}║"
-
-    return layer_str
+        ['─', '─', '─', ' ', ' ']
 
+    """
+    return [config.wire_filler] * config.n_wires + [
+        config.bit_filler(b) for b in range(config.n_bits)
+    ]
 
-def _add_op(op, layer_str, config):
-    """Updates ``layer_str`` with ``op`` operation."""
-    if isinstance(op, qml.ops.Conditional):  # pylint: disable=no-member
-        layer_str = _add_cond_grouping_symbols(op, layer_str, config)
-        return _add_op(op.base, layer_str, config)
 
-    if isinstance(op, MidMeasureMP):
-        return _add_mid_measure_op(op, layer_str, config)
+def _left_justify(layer_str: list[str], config: _Config) -> list[str]:
+    """Add filler characters to layer_str so that everything has the same length.
 
-    layer_str = _add_grouping_symbols(op, layer_str, config)
+    If we initialize with:
 
-    control_wires, control_values = unwrap_controls(op)
+    .. code-block::
 
-    if control_values:
-        for w, val in zip(control_wires, control_values):
-            layer_str[config.wire_map[w]] += "●" if val else "○"
-    else:
-        for w in control_wires:
-            layer_str[config.wire_map[w]] += "●"
-
-    label = op.label(decimals=config.decimals, cache=config.cache).replace("\n", "")
-    if len(op.wires) == 0:  # operation (e.g. barrier, snapshot) across all wires
-        n_wires = len(config.wire_map)
-        for i, s in enumerate(layer_str[:n_wires]):
-            layer_str[i] = s + label
-    else:
-        for w in op.wires:
-            if w not in control_wires:
-                layer_str[config.wire_map[w]] += label
+        ['─Rot', '─', '─', ' ']
 
-    return layer_str
+    We will get out:
 
+    .. code-block::
 
-def _add_mid_measure_op(op, layer_str, config):
-    """Updates ``layer_str`` with ``op`` operation when ``op`` is a
-    ``qml.measurements.MidMeasureMP``."""
-    layer_str = _add_mid_measure_grouping_symbols(op, layer_str, config)
-    label = op.label(decimals=config.decimals, cache=config.cache).replace("\n", "")
+        ['─Rot', '────', '────', '    ']
 
-    for w in op.wires:
-        layer_str[config.wire_map[w]] += label
+    where every entry in the layer now has the same length.
 
-    return layer_str
-
-
-measurement_label_map = {
-    ExpectationMP: lambda label: f"<{label}>",
-    ProbabilityMP: lambda label: f"Probs[{label}]" if label else "Probs",
-    SampleMP: lambda label: f"Sample[{label}]" if label else "Sample",
-    CountsMP: lambda label: f"Counts[{label}]" if label else "Counts",
-    VarianceMP: lambda label: f"Var[{label}]",
-    StateMP: lambda label: "State",
-    DensityMatrixMP: lambda label: "DensityMatrix",
-}
-
-
-def _add_cwire_measurement_grouping_symbols(mcms, layer_str, config):
-    """Adds symbols indicating the extent of a given object for mid-circuit measurement
-    statistics."""
-    if len(mcms) > 1:
-        n_wires = len(config.wire_map)
-        mapped_bits = [config.bit_map[m] for m in mcms]
-        min_b, max_b = min(mapped_bits) + n_wires, max(mapped_bits) + n_wires
+    """
+    max_label_len = max(len(s) for s in layer_str)
 
-        layer_str[min_b] = "╭"
-        layer_str[max_b] = "╰"
+    for w in range(config.n_wires):
+        layer_str[w] = layer_str[w].ljust(max_label_len, config.wire_filler)
 
-        for b in range(min_b + 1, max_b):
-            layer_str[b] = "├" if b - n_wires in mapped_bits else "│"
+    # Adjust width for bit filler on unused bits
+    for b in range(config.n_bits):
+        # needs filler for next layer, as adding to the right of this one
+        cur_b_filler = config.bit_filler(b, next_layer=True)
+        layer_str[b + config.n_wires] = layer_str[b + config.n_wires].ljust(
+            max_label_len, cur_b_filler
+        )
 
     return layer_str
 
 
-def _add_cwire_measurement(m, layer_str, config):
-    """Updates ``layer_str`` with the ``m`` measurement when it is used
-    for collecting mid-circuit measurement statistics."""
-    mcms = [v.measurements[0] for v in m.mv] if isinstance(m.mv, list) else m.mv.measurements
-    layer_str = _add_cwire_measurement_grouping_symbols(mcms, layer_str, config)
+def _add_to_finished_lines(totals: _CurrentTotals, config: _Config) -> _CurrentTotals:
+    """Add current totals to the finished lines and initialize new totals."""
+    totals.finished_lines += totals.wire_totals + totals.bit_totals
+    totals.finished_lines[-1] += "\n"
 
-    mv_label = "MCM"
-    meas_label = measurement_label_map[type(m)](mv_label)  # pylint: disable=protected-access
+    totals.wire_totals = [config.wire_filler] * config.n_wires
 
-    n_wires = len(config.wire_map)
-    for mcm in mcms:
-        ind = config.bit_map[mcm] + n_wires
-        layer_str[ind] += meas_label
+    # Bit totals for new lines for warped drawings need to be consistent with the
+    # current bit filler
+    totals.bit_totals = [config.bit_filler(b) for b in range(config.n_bits)]
 
-    return layer_str
+    return totals
 
 
-def _add_measurement(m, layer_str, config):
-    """Updates ``layer_str`` with the ``m`` measurement."""
-    if m.mv is not None:
-        return _add_cwire_measurement(m, layer_str, config)
+def _add_layer_str_to_totals(totals: _CurrentTotals, layer_str, config) -> _CurrentTotals:
+    """Combine the current layer's string representation with the accumulated circuit representation.
 
-    layer_str = _add_grouping_symbols(m, layer_str, config)
+    This function joins each wire and bit string in the current layer with the corresponding
+    accumulated string in totals, using appropriate filler characters.
 
-    if m.obs is None:
-        obs_label = None
-    else:
-        obs_label = m.obs.label(decimals=config.decimals, cache=config.cache).replace("\n", "")
-    if type(m) in measurement_label_map:
-        meas_label = measurement_label_map[type(m)](obs_label)
-    else:
-        meas_label = str(m)
+    Args:
+        totals: Object containing the current state of the circuit representation
+        layer_str: List of strings representing the current layer to be added
+        config: Configuration object with drawing settings and current state
 
-    if len(m.wires) == 0:  # state or probability across all wires
-        n_wires = len(config.wire_map)
-        for i, s in enumerate(layer_str[:n_wires]):
-            layer_str[i] = s + meas_label
+    Returns:
+        Updated totals with the current layer added
+    """
+    # Process quantum wires - join accumulated wire strings with current layer strings
+    totals.wire_totals = [
+        config.wire_filler.join([t, s])
+        for t, s in zip(totals.wire_totals, layer_str[: config.n_wires])
+    ]
+
+    # Process classical bits - join accumulated bit strings with current layer strings
+    for j, (bt, s) in enumerate(
+        zip(totals.bit_totals, layer_str[config.n_wires : config.n_wires + config.n_bits])
+    ):
+        totals.bit_totals[j] = config.bit_filler(j).join([bt, s])
+
+    return totals
+
+
+def _finalize_layers(totals: _CurrentTotals, config: _Config) -> _CurrentTotals:
+    """Add ending characters to separate the operation layers from the measurement layers"""
+    totals.wire_totals = [f"{s}─┤" for s in totals.wire_totals]
+    for b in range(config.n_bits):
+        if config.cwire_layers[b][-1] >= config.num_op_layers:
+            totals.bit_totals[b] += "═╡"
+        else:
+            totals.bit_totals[b] += "  "
 
-    for w in m.wires:
-        layer_str[config.wire_map[w]] += meas_label
-    return layer_str
+    return totals
 
 
 # pylint: disable=too-many-arguments
 def tape_text(
     tape,
     wire_order=None,
+    *,
     show_all_wires=False,
     decimals=None,
     max_length=100,
@@ -444,127 +402,47 @@ def tape_text(
 
     wire_map = convert_wire_order(tape, wire_order=wire_order, show_all_wires=show_all_wires)
     bit_map = default_bit_map(tape)
-    n_wires = len(wire_map)
-    n_bits = len(bit_map)
-    if n_wires == 0:
+    if len(wire_map) == 0:
         return ""
 
-    # Used to store lines that are hitting the maximum length
-    finished_lines = []
-
     layers = drawable_layers(tape.operations, wire_map=wire_map, bit_map=bit_map)
-    final_operations_layer = len(layers) - 1
+    num_op_layers = len(layers)
     layers += drawable_layers(tape.measurements, wire_map=wire_map, bit_map=bit_map)
 
     # Update bit map and collect information about connections between mid-circuit measurements,
     # classical conditions, and terminal measurements for processing mid-circuit measurements.
     cwire_layers, _ = cwire_connections(layers, bit_map)
 
-    if show_wire_labels:
-        wire_totals = [f"{wire}: " for wire in wire_map]
-    else:
-        wire_totals = [""] * n_wires
-    bit_totals = [""] * n_bits
-    line_length = max(len(s) for s in wire_totals)
-
-    wire_totals = [s.rjust(line_length, " ") for s in wire_totals]
-    bit_totals = [s.rjust(line_length, " ") for s in bit_totals]
-
     # Collect information needed for drawing layers
     config = _Config(
         wire_map=wire_map,
         bit_map=bit_map,
-        cur_layer=-1,
+        num_op_layers=num_op_layers,
         cwire_layers=cwire_layers,
         decimals=decimals,
         cache=cache,
     )
 
-    for i, layer in enumerate(layers):
-        # Update fillers and helper function
-        w_filler = "─" if i <= final_operations_layer else " "
-        b_filler = "═" if i <= final_operations_layer else " "
-        add_fn = _add_op if i <= final_operations_layer else _add_measurement
-
-        # Create initial strings for the current layer using wire and cwire fillers
-        layer_str = [w_filler] * n_wires + [" "] * n_bits
-        for b in bit_map.values():
-            cur_b_filler = b_filler if min(cwire_layers[b]) < i < max(cwire_layers[b]) else " "
-            layer_str[b + n_wires] = cur_b_filler
+    totals = _CurrentTotals([], *(_initialize_wire_and_bit_totals(config, show_wire_labels)))
 
-        config.cur_layer = i
+    for cur_layer, layer in enumerate(layers):
+        config.cur_layer = cur_layer
+        layer_str = _initialize_layer_str(config)
 
-        ##########################################
-        # Update current layer strings with labels
-        ##########################################
         for op in layer:
-            if isinstance(op, qml.tape.QuantumScript):
-                layer_str = _add_grouping_symbols(op, layer_str, config)
-                label = f"Tape:{cache['tape_offset']+len(tape_cache)}"
-                for w in op.wires:
-                    layer_str[wire_map[w]] += label
-                tape_cache.append(op)
-            else:
-                layer_str = add_fn(op, layer_str, config)
-
-        #################################################
-        # Left justify layer strings and pad on the right
-        #################################################
-        # Adjust width for wire filler on unused wires
-        max_label_len = max(len(s) for s in layer_str)
-        for w in range(n_wires):
-            layer_str[w] = layer_str[w].ljust(max_label_len, w_filler)
-
-        # Adjust width for bit filler on unused bits
-        for b in range(n_bits):
-            cur_b_filler = b_filler if cwire_layers[b][0] <= i < cwire_layers[b][-1] else " "
-            layer_str[b + n_wires] = layer_str[b + n_wires].ljust(max_label_len, cur_b_filler)
-
-        line_length += max_label_len + 1  # one for the filler character
-
-        ##################
-        # Create new lines
-        ##################
-        if line_length > max_length:
-            # move totals into finished_lines and reset totals
-            finished_lines += wire_totals + bit_totals
-            finished_lines[-1] += "\n"
-            wire_totals = [w_filler] * n_wires
-
-            # Bit totals for new lines for warped drawings need to be consistent with the
-            # current bit filler
-            bit_totals = []
-            for b in range(n_bits):
-                cur_b_filler = b_filler if cwire_layers[b][0] < i <= cwire_layers[b][-1] else " "
-                bit_totals.append(cur_b_filler)
-
-            line_length = 2 + max_label_len
-
-        ###################################################
-        # Join current layer with lines for previous layers
-        ###################################################
-        # Joining is done by adding a filler at the end of the previous layer
-        wire_totals = [w_filler.join([t, s]) for t, s in zip(wire_totals, layer_str[:n_wires])]
-
-        for j, (bt, s) in enumerate(zip(bit_totals, layer_str[n_wires : n_wires + n_bits])):
-            cur_b_filler = b_filler if cwire_layers[j][0] < i <= cwire_layers[j][-1] else " "
-            bit_totals[j] = cur_b_filler.join([bt, s])
-
-        ################################################
-        # Add ender characters to final operations layer
-        ################################################
-        if i == final_operations_layer:
-            wire_totals = [f"{s}─┤" for s in wire_totals]
-            for b in range(n_bits):
-                if cwire_layers[b][-1] > final_operations_layer:
-                    bit_totals[b] += "═╡"
-                else:
-                    bit_totals[b] += "  "
-
-            line_length += 2
+            layer_str = _add_obj(op, layer_str, config, tape_cache)
+        layer_str = _left_justify(layer_str, config)
+
+        if len(totals.wire_totals[0]) + len(layer_str[0]) > max_length - 1:
+            totals = _add_to_finished_lines(totals, config)
+
+        totals = _add_layer_str_to_totals(totals, layer_str, config)
+
+        if config.cur_layer == config.num_op_layers - 1:
+            totals = _finalize_layers(totals, config)
 
     # Recursively handle nested tapes #
-    tape_totals = "\n".join(finished_lines + wire_totals + bit_totals)
+    tape_totals = "\n".join(totals.finished_lines + totals.wire_totals + totals.bit_totals)
     current_tape_offset = cache["tape_offset"]
     cache["tape_offset"] += len(tape_cache)
     for i, nested_tape in enumerate(tape_cache):
@@ -572,18 +450,16 @@ def tape_text(
         tape_str = tape_text(
             nested_tape,
             wire_order,
-            show_all_wires,
-            decimals,
-            max_length,
+            show_all_wires=show_all_wires,
+            decimals=decimals,
+            max_length=max_length,
             show_matrices=False,
             cache=cache,
         )
         tape_totals = "\n".join([tape_totals, label, tape_str])
 
     if show_matrices:
-        mat_str = ""
-        for i, mat in enumerate(cache["matrices"]):
-            mat_str += f"\nM{i} = \n{mat}"
+        mat_str = "".join(f"\nM{i} = \n{mat}" for i, mat in enumerate(cache["matrices"]))
         return tape_totals + mat_str
 
     return tape_totals


Test patch:
diff --git a/tests/drawer/test_draw_mpl.py b/tests/drawer/test_draw_mpl.py
index c8c162cce..f39e8d501 100644
--- a/tests/drawer/test_draw_mpl.py
+++ b/tests/drawer/test_draw_mpl.py
@@ -68,7 +68,7 @@ def test_standard_use():
     texts = [t.get_text() for t in ax.texts[3:]]
     assert "RX" in texts
     assert "RY" in texts
-    plt.close()
+    plt.close("all")
 
 
 def test_fig_argument():
@@ -78,6 +78,7 @@ def test_fig_argument():
     output_fig, ax = qml.draw_mpl(circuit1, fig=fig)(1.23, 2.34)
     assert ax.get_figure() == fig
     assert output_fig == fig
+    plt.close("all")
 
 
 class TestLevelExpansionStrategy:
@@ -152,6 +153,7 @@ class TestLevelExpansionStrategy:
 
         with pytest.warns(UserWarning, match="Multiple tapes constructed"):
             qml.draw_mpl(circuit)()
+        plt.close("all")
 
 
 class TestKwargs:
@@ -163,7 +165,7 @@ class TestKwargs:
         _, ax = qml.draw_mpl(circuit1, fontsize=20)(1.234, 1.234)
         for t in ax.texts:
             assert t.get_fontsize() == 20
-        plt.close()
+        plt.close("all")
 
     def test_decimals(self):
         """Test decimals changes operation labelling"""
@@ -173,7 +175,7 @@ class TestKwargs:
         texts = [t.get_text() for t in ax.texts[3:]]
         assert "RX\n(1.23)" in texts
         assert "RY\n(2.34)" in texts
-        plt.close()
+        plt.close("all")
 
     def test_label_options(self):
         """Test label options modifies label style."""
@@ -185,7 +187,7 @@ class TestKwargs:
         for l in ax.texts[:3]:  # three labels
             assert l.get_color() == "purple"
             assert l.get_fontsize() == 20
-        plt.close()
+        plt.close("all")
 
     def test_hide_wire_labels(self):
         """Test that wire labels are skipped with show_wire_labels=False."""
@@ -200,7 +202,7 @@ class TestKwargs:
         assert fig.get_figwidth() == 4
         assert fig_with_labels.get_figwidth() == 4 + 1
 
-        plt.close()
+        plt.close("all")
 
     @pytest.mark.parametrize(
         "notches, n_patches",
@@ -220,7 +222,7 @@ class TestKwargs:
         _, ax = qml.draw_mpl(temp_circ, show_all_wires=True, active_wire_notches=notches)()
 
         assert len(ax.patches) == n_patches
-        plt.close()
+        plt.close("all")
 
     def test_black_white_is_default_style(self):
         """Test that if no style is specified, the black_white style is the default for mpl_draw,
@@ -278,7 +280,7 @@ class TestWireBehaviour:
         assert ax.texts[0].get_text() == "1.23"
         assert ax.texts[1].get_text() == "a"
         assert ax.texts[2].get_text() == "0"
-        plt.close()
+        plt.close("all")
 
     def test_empty_wires(self):
         """Test empty wires do not appear by default"""
@@ -289,7 +291,7 @@ class TestWireBehaviour:
         assert len(ax.texts) == 2  # one wire label and one gate label
         assert ax.texts[0].get_text() == "0"
         assert ax.texts[1].get_text() == "RX"
-        plt.close()
+        plt.close("all")
 
     def test_show_all_wires(self):
         """Test show_all_wires=True displays empty wires."""
@@ -302,7 +304,7 @@ class TestWireBehaviour:
         assert ax.texts[0].get_text() == "0"
         assert ax.texts[1].get_text() == "a"
         assert ax.texts[2].get_text() == "1.23"
-        plt.close()
+        plt.close("all")
 
     def test_wire_order_not_on_device(self):
         """Test when ``wire_order`` priority by requesting ``show_all_wires`` with
@@ -317,7 +319,7 @@ class TestWireBehaviour:
         assert ax.texts[0].get_text() == "2"
         assert ax.texts[1].get_text() == "a"
         assert ax.texts[2].get_text() == "0"
-        plt.close()
+        plt.close("all")
 
     def test_uniform_wire_options(self):
         """Test wire options modifies wire styling"""
@@ -328,7 +330,7 @@ class TestWireBehaviour:
             assert w.get_color() == "black"
             assert w.get_linewidth() == 4
 
-        plt.close()
+        plt.close("all")
 
     def test_individual_wire_options(self):
         """Test wire option styling when individual wires have their own options specified"""
@@ -402,7 +404,7 @@ class TestWireBehaviour:
                 assert w.get_linestyle() == "-"
                 assert w.get_linewidth() == 5
 
-        plt.close()
+        plt.close("all")
 
     def test_individual_wire_options_with_string_labels(self):
         """Test that individual wire options work with string wire labels"""
@@ -477,7 +479,7 @@ class TestMPLIntegration:
             assert l.get_color() == rgba_green
 
         qml.drawer.use_style("black_white")
-        plt.close()
+        plt.close("all")
 
     def test_style_with_matplotlib(self):
         """Test matplotlib styles impact figure styling for draw_mpl(circuit, style="rcParams")."""
@@ -495,7 +497,7 @@ class TestMPLIntegration:
             assert mpl.colors.to_rgba(l.get_color()) == expected_linecolor
 
         qml.drawer.use_style("black_white")
-        plt.close()
+        plt.close("all")
 
     def test_style_restores_settings(self):
         """Test that selecting style as draw_mpl(circuit, style=None) does not modify the users
@@ -542,7 +544,7 @@ def test_draw_mpl_supports_qfuncs():
     assert len(ax.texts) == 2
     assert ax.texts[0].get_text() == "0"
     assert ax.texts[1].get_text() == "RX"
-    plt.close()
+    plt.close("all")
 
 
 def test_draw_mpl_with_qfunc_warns_with_level():
@@ -626,7 +628,7 @@ def test_applied_transforms():
     assert len(ax.patches) == 1  # single pauli x gate
     assert len(ax.texts) == 2  # one wire label, one gate label
 
-    plt.close()
+    plt.close("all")
 
 
 @pytest.mark.parametrize("use_qnode", (True, False))
diff --git a/tests/drawer/test_mpldrawer.py b/tests/drawer/test_mpldrawer.py
index 1dd5f00a2..224be0ba8 100644
--- a/tests/drawer/test_mpldrawer.py
+++ b/tests/drawer/test_mpldrawer.py
@@ -56,7 +56,7 @@ class TestInitialization:
         for wire, line in enumerate(lines):
             assert line.get_xdata() == (-1, n_layers)
             assert line.get_ydata() == (wire, wire)
-        plt.close()
+        plt.close("all")
 
     def test_figsize_classical_wires(self):
         """Test the figsize is correct if classical wires are present."""
@@ -81,7 +81,7 @@ class TestInitialization:
 
         assert drawer.fig.get_figwidth() == 5
         assert drawer.fig.get_figheight() == 5
-        plt.close()
+        plt.close("all")
 
     def test_customfigure(self):
         """Tests a custom figure is used"""
@@ -90,7 +90,7 @@ class TestInitialization:
         drawer = MPLDrawer(1, {0: 0}, fig=fig)
 
         assert drawer.fig == fig
-        plt.close()
+        plt.close("all")
 
     def test_config_params_set(self):
         """Tests sizing hidden variables are set."""
@@ -108,7 +108,7 @@ class TestInitialization:
         assert drawer._notch_width == 0.04
         assert drawer._notch_height == 0.25
         assert drawer._notch_style == "round, pad=0.05"
-        plt.close()
+        plt.close("all")
 
     def test_wires_formatting(self):
         """Tests wires formatting with options"""
@@ -121,7 +121,7 @@ class TestInitialization:
             assert line.get_linewidth() == 3
             assert line.get_color() == rgba_red
 
-        plt.close()
+        plt.close("all")
 
     def test_fontsize(self):
         """Test fontsize can be get and set via property."""
@@ -134,7 +134,7 @@ class TestInitialization:
 
         assert drawer._fontsize == 10
         assert drawer.fontsize == 10
-        plt.close()
+        plt.close("all")
 
 
 class TestLabels:
@@ -157,7 +157,7 @@ class TestLabels:
 
             assert actual_label.get_position() == (-1.5, wire)
 
-        plt.close()
+        plt.close("all")
 
     def test_labels_formatting(self):
         """Test labels are formatted with text options."""
@@ -175,7 +175,7 @@ class TestLabels:
             assert text.get_fontsize() == 10
             assert text.get_color() == rgba_red
 
-        plt.close()
+        plt.close("all")
 
     @pytest.mark.parametrize("n_layers", [1, 4])
     def test_crop_wire_labels(self, n_layers):
@@ -236,7 +236,7 @@ class TestBoxGate:
         assert text.get_text() == "X"
         assert text.get_position() == (0, 0)
         assert text.get_zorder() == 3
-        plt.close()
+        plt.close("all")
 
     def test_multiwire_box(self):
         """tests a gate spanning multiple wires."""
@@ -257,7 +257,7 @@ class TestBoxGate:
         assert text.get_text() == "Tall Gate"
         assert text.get_position() == (0, 1.0)
         assert text.get_zorder() == 3
-        plt.close()
+        plt.close("all")
 
     def test_notch_standard_styling(self):
         """Test notch styling is correct"""
@@ -276,7 +276,7 @@ class TestBoxGate:
             assert notch.get_height() == drawer._notch_height
             assert notch.get_zorder() == 1
             assert notch.get_boxstyle().pad == 0.05
-        plt.close()
+        plt.close("all")
 
     @pytest.mark.parametrize(
         "wires, n_notches", [((0, 1, 2, 3), 0), ((0,), 0), ((0, 2), 4), ((0, 1, 3), 6)]
@@ -288,7 +288,7 @@ class TestBoxGate:
         drawer.box_gate(layer=0, wires=wires)
 
         assert len(drawer.ax.patches) == (n_notches + 1)
-        plt.close()
+        plt.close("all")
 
     def test_no_active_wire_notches(self):
         """Test active wire notches deactivated by keyword."""
@@ -297,7 +297,7 @@ class TestBoxGate:
 
         # only one patch for big box, no patches for notches
         assert len(drawer.ax.patches) == 1
-        plt.close()
+        plt.close("all")
 
     def test_extra_width(self):
         """tests a box with added width."""
@@ -321,7 +321,7 @@ class TestBoxGate:
         for x, notch in zip(xs, drawer.ax.patches[1:]):
             assert notch.get_x() == x
 
-        plt.close()
+        plt.close("all")
 
     def test_box_formatting(self):
         """Tests that box_options influences the rectangle"""
@@ -341,7 +341,7 @@ class TestBoxGate:
         assert drawer.ax.patches[0].get_zorder() == 5
         for n in drawer.ax.patches[1:]:
             assert n.get_zorder() == 4
-        plt.close()
+        plt.close("all")
 
     def test_text_formatting(self):
         """Tests rotated text"""
@@ -354,7 +354,7 @@ class TestBoxGate:
         text = drawer.ax.texts[0]
         assert text.get_rotation() == 90.0
         assert text.get_color() == rgba_red
-        plt.close()
+        plt.close("all")
 
 
 class TestCTRL:
@@ -377,7 +377,7 @@ class TestCTRL:
 
         assert circle.width == 0.2
         assert circle.center == (0, 0)
-        plt.close()
+        plt.close("all")
 
     def test_ctrl_multi_wires(self):
         """Tests two control wires with no target."""
@@ -398,7 +398,7 @@ class TestCTRL:
         for wire, circle in zip(ctrl_wires, circles):
             assert circle.width == 0.2
             assert circle.center == (0, wire)
-        plt.close()
+        plt.close("all")
 
     def test_ctrl_on_zero(self):
         """Tests a control on zero circle is open"""
@@ -415,7 +415,7 @@ class TestCTRL:
 
         assert circ.center == (0, 0)
         assert circ.width == 0.2
-        plt.close()
+        plt.close("all")
 
     def test_ctrl_control_values_error(self):
         """Tests a ValueError is raised if different number of wires and control_values."""
@@ -425,7 +425,7 @@ class TestCTRL:
         with pytest.raises(ValueError, match="`control_values` must be the same length"):
             drawer.ctrl(0, (0, 1), control_values=True)
 
-        plt.close()
+        plt.close("all")
 
     def test_ctrl_formatting(self):
         """Tests two control wires with no target."""
@@ -449,7 +449,7 @@ class TestCTRL:
         assert open_circ.get_facecolor() == to_rgba(plt.rcParams["axes.facecolor"])
         assert open_circ.get_linewidth() == 4
 
-        plt.close()
+        plt.close("all")
 
     def test_ctrl_circ(self):
         """Test only the ``_ctrl_circ`` private method."""
@@ -463,7 +463,7 @@ class TestCTRL:
         assert circ.center == (0, 0)
         assert circ.width == 0.2
 
-        plt.close()
+        plt.close("all")
 
     def test_ctrlo_circ(self):
         """Test only the ``ctrlo_circ`` private method."""
@@ -475,7 +475,7 @@ class TestCTRL:
         assert circ.get_facecolor() == to_rgba(plt.rcParams["axes.facecolor"])
         assert circ.get_edgecolor() == to_rgba(plt.rcParams["lines.color"])
         assert circ.get_linewidth() == plt.rcParams["lines.linewidth"]
-        plt.close()
+        plt.close("all")
 
     def test_ctrl_target(self):
         """Tests target impacts line extent"""
@@ -495,7 +495,7 @@ class TestCTRL:
 
         assert circle.width == 0.2
         assert circle.center == (0, 0)
-        plt.close()
+        plt.close("all")
 
     @pytest.mark.parametrize(
         "control_wires,target_wires",
@@ -511,7 +511,7 @@ class TestCTRL:
         drawer = MPLDrawer(1, {i: i for i in range(4)})
         with pytest.warns(UserWarning, match="control indicators are hidden behind an operator"):
             drawer.ctrl(0, control_wires, target_wires)
-        plt.close()
+        plt.close("all")
 
     @pytest.mark.parametrize("control_wires,target_wires", [((0,), (1, 2)), ((2,), (0, 1))])
     def test_ctrl_no_warning_without_overlap(self, control_wires, target_wires):
@@ -519,7 +519,7 @@ class TestCTRL:
         with warnings.catch_warnings(record=True) as w:
             drawer.ctrl(0, control_wires, target_wires)
         assert len(w) == 0
-        plt.close()
+        plt.close("all")
 
     def test_target_x(self):
         """Tests hidden target_x drawing method"""
@@ -540,7 +540,7 @@ class TestCTRL:
         assert circle.width == 0.6
         assert circle.get_facecolor() == to_rgba(plt.rcParams["axes.facecolor"])
         assert to_rgba(plt.rcParams["lines.color"]) == to_rgba(circle.get_edgecolor())
-        plt.close()
+        plt.close("all")
 
     def test_target_x_color(self):
         """Test the color of target_x."""
@@ -559,7 +559,7 @@ class TestCTRL:
         assert circle.get_facecolor() == to_rgba(plt.rcParams["axes.facecolor"])
         assert circle.get_edgecolor() == rgba_red
 
-        plt.close()
+        plt.close("all")
 
     def test_CNOT(self):
         """Tests the CNOT method"""
@@ -584,7 +584,7 @@ class TestCTRL:
         assert target_circle.width == 0.6
         assert target_circle.get_facecolor() == to_rgba(plt.rcParams["axes.facecolor"])
         assert to_rgba(plt.rcParams["lines.color"]) == to_rgba(target_circle.get_edgecolor())
-        plt.close()
+        plt.close("all")
 
     def test_CNOT_control_values(self):
         """Tests the ``control_values`` keyword for CNOT."""
@@ -600,7 +600,7 @@ class TestCTRL:
         assert ctrl_circ1.get_facecolor() == to_rgba(plt.rcParams["lines.color"])
         # second facecolor should match the background
         assert ctrl_circ2.get_facecolor() == to_rgba(plt.rcParams["axes.facecolor"])
-        plt.close()
+        plt.close("all")
 
     def test_CNOT_color(self):
         """Tests the color of CNOT."""
@@ -620,7 +620,7 @@ class TestCTRL:
         target_circle = drawer.ax.patches[1]
         assert target_circle.get_edgecolor() == rgba_red
 
-        plt.close()
+        plt.close("all")
 
 
 class TestSWAP:
@@ -637,7 +637,7 @@ class TestSWAP:
 
         assert l1.get_data() == ((-0.2, 0.2), (-0.2, 0.2))
         assert l2.get_data() == ((-0.2, 0.2), (0.2, -0.2))
-        plt.close()
+        plt.close("all")
 
     def test_SWAP(self):
         """Tests the SWAP method."""
@@ -653,7 +653,7 @@ class TestSWAP:
         assert x_lines[1].get_data() == ((-0.2, 0.2), (0.2, -0.2))
         assert x_lines[2].get_data() == ((-0.2, 0.2), (1.8, 2.2))
         assert x_lines[3].get_data() == ((-0.2, 0.2), (2.2, 1.8))
-        plt.close()
+        plt.close("all")
 
     def test_SWAP_options(self):
         """Tests that SWAP can be colored."""
@@ -667,7 +667,7 @@ class TestSWAP:
             assert line.get_color() == rgba_red
             assert line.get_linewidth() == 3
 
-        plt.close()
+        plt.close("all")
 
 
 class TestMeasure:
@@ -694,7 +694,7 @@ class TestMeasure:
         arrow = drawer.ax.patches[2]
         assert isinstance(arrow, FancyArrow)
         assert len(drawer.ax.texts) == 0
-        plt.close()
+        plt.close("all")
 
     def test_measure_multiple_wires(self):
         """Tests the measure method when multiple wires are provided."""
@@ -718,7 +718,7 @@ class TestMeasure:
         arrow = drawer.ax.patches[2]
         assert isinstance(arrow, FancyArrow)
         assert len(drawer.ax.texts) == 0
-        plt.close()
+        plt.close("all")
 
     def test_measure_classical_wires(self):
         """Tests the measure method when multiple wires are provided."""
@@ -742,7 +742,7 @@ class TestMeasure:
         arrow = drawer.ax.patches[2]
         assert isinstance(arrow, FancyArrow)
         assert len(drawer.ax.texts) == 0
-        plt.close()
+        plt.close("all")
 
     def test_measure_text(self):
         """Test adding a postselection label to a measure box."""
@@ -751,7 +751,7 @@ class TestMeasure:
         assert len(drawer.ax.texts) == 1
         assert drawer.ax.texts[0].get_text() == "0"
         assert drawer.ax.texts[0].get_position() == (0.05 * 0.75, 0.225)
-        plt.close()
+        plt.close("all")
 
     def test_measure_formatted(self):
         """Tests you can color the measure box"""
@@ -775,7 +775,7 @@ class TestMeasure:
         assert arrow.get_edgecolor() == rgba_green
         assert arrow.get_linewidth() == 0.5
 
-        plt.close()
+        plt.close("all")
 
 
 class TestAutosize:
@@ -812,7 +812,7 @@ class TestAutosize:
         assert t.get_rotation() == 0
         assert t.get_fontsize() == drawer._fontsize
 
-        plt.close()
+        plt.close("all")
 
     def test_autosize_one_wire(self):
         """Test case where the box is on only one wire.  The text should still
@@ -826,7 +826,7 @@ class TestAutosize:
 
         assert self.text_in_box(drawer)
 
-        plt.close()
+        plt.close("all")
 
     def test_autosize_multiwires(self):
         """Test case where the box is on multiple wires.  The text should
@@ -840,7 +840,7 @@ class TestAutosize:
 
         assert self.text_in_box(drawer)
 
-        plt.close()
+        plt.close("all")
 
     def test_multiline_text_single_wire(self):
         """Test case where the box is on one wire and the text is skinny and tall.
@@ -854,7 +854,7 @@ class TestAutosize:
 
         assert self.text_in_box(drawer)
 
-        plt.close()
+        plt.close("all")
 
     def text_tall_multitline_text_multiwires(self):
         """Test case where the box is on mutiple wires and the text is skinny and tall.
@@ -874,7 +874,7 @@ class TestAutosize:
 
         assert self.text_in_box(drawer)
 
-        plt.close()
+        plt.close("all")
 
     def test_wide_multline_text_multiwires(self):
         """Test case where the box is on multiple wires and text is fat, tall,
@@ -885,7 +885,7 @@ class TestAutosize:
 
         assert self.text_in_box(drawer)
 
-        plt.close()
+        plt.close("all")
 
 
 class TestClassicalWires:
@@ -913,7 +913,7 @@ class TestClassicalWires:
             "foreground": plt.rcParams["figure.facecolor"],
         }
 
-        plt.close()
+        plt.close("all")
 
     def test_cwire_join(self):
         """Test the cwire join method."""
@@ -927,7 +927,7 @@ class TestClassicalWires:
         assert eraser.get_ydata() == (1.25, 1.25)
         assert eraser.get_color() == plt.rcParams["figure.facecolor"]
         assert eraser.get_linewidth() == 3 * plt.rcParams["lines.linewidth"]
-        plt.close()
+        plt.close("all")
 
     def test_cwire_join_erase_right(self):
         """Test the cwire join method."""
@@ -941,7 +941,7 @@ class TestClassicalWires:
         assert eraser.get_ydata() == (1, 1)
         assert eraser.get_color() == plt.rcParams["figure.facecolor"]
         assert eraser.get_linewidth() == 3 * plt.rcParams["lines.linewidth"]
-        plt.close()
+        plt.close("all")
 
 
 class TestCond:
@@ -964,7 +964,7 @@ class TestCond:
             ((0.97, 0.97), (0.03, 1)),
         ]
         assert [line.get_data() for line in drawer._wire_lines] == wire_data_before
-        plt.close()
+        plt.close("all")
 
     def test_cond_two_ctrl_wires(self):
         """Tests cond from two separated wires."""
@@ -985,7 +985,7 @@ class TestCond:
             ((0.375, 0.97), (1.97, 1.97)),
             ((0.375, 0.97), (2.03, 2.03)),
         ]
-        plt.close()
+        plt.close("all")
 
     def test_cond_two_ctrl_wires_upward(self):
         """Test cond when the conditional operation is above the control wires."""
@@ -1004,7 +1004,7 @@ class TestCond:
             ((0.375, 0.97), (0.97, 0.97)),
             ((0.375, 0.97), (1.03, 1.03)),
         ]
-        plt.close()
+        plt.close("all")
 
     @pytest.mark.parametrize(
         "ctrl_wires, target_wires",
@@ -1020,4 +1020,4 @@ class TestCond:
         drawer = MPLDrawer(wire_map={i: i for i in range(4)}, n_layers=2)
         with pytest.raises(ValueError, match="Cannot draw interspersed mid-circuit measurements"):
             drawer.cond(layer=1, measured_layer=0, wires=ctrl_wires, wires_target=target_wires)
-        plt.close()
+        plt.close("all")
diff --git a/tests/drawer/test_tape_text.py b/tests/drawer/test_tape_text.py
index 631b645b2..b291f27e7 100644
--- a/tests/drawer/test_tape_text.py
+++ b/tests/drawer/test_tape_text.py
@@ -21,16 +21,16 @@ import pytest
 import pennylane as qml
 from pennylane import numpy as np
 from pennylane.drawer import tape_text
-from pennylane.drawer.tape_text import (
+from pennylane.drawer._add_obj import (
     _add_cond_grouping_symbols,
     _add_cwire_measurement,
     _add_cwire_measurement_grouping_symbols,
     _add_grouping_symbols,
     _add_measurement,
     _add_mid_measure_grouping_symbols,
-    _add_op,
-    _Config,
+    _add_obj,
 )
+from pennylane.drawer.tape_text import _Config
 from pennylane.tape import QuantumScript
 
 default_wire_map = {0: 0, 1: 1, 2: 2, 3: 3}
@@ -71,6 +71,19 @@ with qml.queuing.AnnotatedQueue() as q_tape:
 tape = qml.tape.QuantumScript.from_queue(q_tape)
 
 
+def test_error_if_unsupported_object_in_tape():
+    """Test an error is raised if there's an unsupported object in the tape."""
+
+    # pylint: disable=too-few-public-methods
+    class DummyObj:
+        wires = qml.wires.Wires(2)
+
+    _tape = qml.tape.QuantumScript([DummyObj()], [])
+
+    with pytest.raises(NotImplementedError, match="unable to draw object"):
+        qml.drawer.tape_text(_tape)
+
+
 class TestHelperFunctions:  # pylint: disable=too-many-arguments
     """Test helper functions for the tape text."""
 
@@ -84,8 +97,13 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     )
     def test_add_grouping_symbols(self, op, out):
         """Test private _add_grouping_symbols function renders as expected."""
+        config = _Config(
+            wire_map=default_wire_map, bit_map=default_bit_map, num_op_layers=4, cur_layer=0
+        )
         assert out == _add_grouping_symbols(
-            op, ["", "", "", ""], _Config(wire_map=default_wire_map, bit_map=default_bit_map)
+            op,
+            ["", "", "", ""],
+            config,
         )
 
     @pytest.mark.parametrize(
@@ -109,7 +127,9 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     def test_add_mid_measure_grouping_symbols(self, op, layer_str, bit_map, out):
         """Test private _add_grouping_symbols function renders as expected for MidMeasureMPs."""
         assert out == _add_mid_measure_grouping_symbols(
-            op, layer_str, _Config(wire_map=default_wire_map, bit_map=bit_map)
+            op,
+            layer_str,
+            _Config(wire_map=default_wire_map, bit_map=bit_map, num_op_layers=4, cur_layer=1),
         )
 
     @pytest.mark.parametrize(
@@ -167,15 +187,18 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
         op = get_conditional_op(mv, cond_op, *args, **kwargs)
         layer_str = ["─", "─", "─", ""] + [" "] * len(bit_map)
 
+        config = _Config(
+            wire_map=default_wire_map,
+            bit_map=bit_map,
+            cur_layer=cur_layer,
+            cwire_layers=[[0], [1]],
+            num_op_layers=4,
+        )
+
         assert out == _add_cond_grouping_symbols(
             op,
             layer_str,
-            _Config(
-                wire_map=default_wire_map,
-                bit_map=bit_map,
-                cur_layer=cur_layer,
-                cwire_layers=[[0], [1]],
-            ),
+            config,
         )
 
     @pytest.mark.parametrize(
@@ -197,9 +220,12 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     def test_add_cwire_measurement_grouping_symbols(self, mps, bit_map, out):
         """Test private _add_cwire_measurement_grouping_symbols renders as expected."""
         layer_str = [" "] * (len(default_wire_map) + len(bit_map))
+        config = _Config(wire_map=default_wire_map, bit_map=bit_map, num_op_layers=4, cur_layer=1)
 
         assert out == _add_cwire_measurement_grouping_symbols(
-            mps, layer_str, _Config(wire_map=default_wire_map, bit_map=bit_map)
+            mps,
+            layer_str,
+            config,
         )
 
     @pytest.mark.parametrize(
@@ -229,9 +255,11 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     def test_add_cwire_measurement(self, mp, bit_map, out):
         """Test private _add_cwire_measurement renders as expected."""
         layer_str = [" "] * (len(default_wire_map) + len(bit_map))
-
+        config = _Config(wire_map=default_wire_map, bit_map=bit_map, num_op_layers=4, cur_layer=1)
         assert out == _add_cwire_measurement(
-            mp, layer_str, _Config(wire_map=default_wire_map, bit_map=bit_map)
+            mp,
+            layer_str,
+            config,
         )
 
     @pytest.mark.parametrize(
@@ -249,16 +277,30 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     )
     def test_add_measurements(self, op, out):
         """Test private _add_measurement function renders as expected."""
+        config = _Config(
+            wire_map=default_wire_map, bit_map=default_bit_map, num_op_layers=4, cur_layer=1
+        )
         assert out == _add_measurement(
-            op, [""] * 4, _Config(wire_map=default_wire_map, bit_map=default_bit_map)
+            op,
+            [""] * 4,
+            config,
         )
 
     def test_add_measurements_cache(self):
         """Test private _add_measurement function with a matrix cache."""
         cache = {"matrices": []}
         op = qml.expval(qml.Hermitian(np.eye(2), wires=0))
+        config = _Config(
+            wire_map={0: 0, 1: 1},
+            bit_map=default_bit_map,
+            cache=cache,
+            num_op_layers=4,
+            cur_layer=1,
+        )
         assert _add_measurement(
-            op, ["", ""], _Config(wire_map={0: 0, 1: 1}, bit_map=default_bit_map, cache=cache)
+            op,
+            ["", ""],
+            config,
         ) == [
             "<𝓗(M0)>",
             "",
@@ -269,7 +311,9 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
         op2 = qml.expval(qml.Hermitian(np.eye(2), wires=1))
         # new op with same matrix, should have same M0 designation
         assert _add_measurement(
-            op2, ["", ""], _Config(wire_map={0: 0, 1: 1}, bit_map=default_bit_map, cache=cache)
+            op2,
+            ["", ""],
+            config,
         ) == [
             "",
             "<𝓗(M0)>",
@@ -287,10 +331,15 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
             (qml.S(0) @ qml.T(0), ["─S@T", "─", "─", "─"]),
         ],
     )
-    def test_add_op(self, op, out):
+    def test_add_obj(self, op, out):
         """Test adding the first operation to array of strings"""
-        assert out == _add_op(
-            op, ["─"] * 4, _Config(wire_map=default_wire_map, bit_map=default_bit_map)
+        config = _Config(
+            wire_map=default_wire_map, bit_map=default_bit_map, num_op_layers=4, cur_layer=1
+        )
+        assert out == _add_obj(
+            op,
+            ["─"] * 4,
+            config,
         )
 
     @pytest.mark.parametrize(
@@ -313,7 +362,8 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     )
     def test_add_mid_measure_op(self, op, layer_str, bit_map, out):
         """Test adding the first MidMeasureMP to array of strings"""
-        assert out == _add_op(op, layer_str, _Config(wire_map=default_wire_map, bit_map=bit_map))
+        config = _Config(wire_map=default_wire_map, bit_map=bit_map, num_op_layers=4, cur_layer=0)
+        assert out == _add_obj(op, layer_str, config)
 
     @pytest.mark.parametrize(
         "cond_op, args, kwargs, out, bit_map, mv",
@@ -348,13 +398,18 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
         """Test adding the first Conditional to array of strings"""
         op = get_conditional_op(mv, cond_op, *args, **kwargs)
         layer_str = ["─", "─", "─", ""] + [" "] * len(bit_map)
+        config = _Config(
+            wire_map=default_wire_map,
+            bit_map=bit_map,
+            cur_layer=1,
+            cwire_layers=[[0], [1]],
+            num_op_layers=4,
+        )
 
-        assert out == _add_op(
+        assert out == _add_obj(
             op,
             layer_str,
-            _Config(
-                wire_map=default_wire_map, bit_map=bit_map, cur_layer=1, cwire_layers=[[0], [1]]
-            ),
+            config,
         )
 
     @pytest.mark.parametrize(
@@ -367,25 +422,39 @@ class TestHelperFunctions:  # pylint: disable=too-many-arguments
     )
     def test_add_second_op(self, op, out):
         """Test adding a second operation to the array of strings"""
-        start = _add_op(
-            qml.PauliX(0), ["─"] * 4, _Config(wire_map=default_wire_map, bit_map=default_bit_map)
+        config = _Config(
+            wire_map=default_wire_map, bit_map=default_bit_map, num_op_layers=4, cur_layer=1
         )
-        assert out == _add_op(
-            op, start, _Config(wire_map=default_wire_map, bit_map=default_bit_map)
+        start = _add_obj(
+            qml.PauliX(0),
+            ["─"] * 4,
+            config,
         )
+        assert out == _add_obj(op, start, config)
 
-    def test_add_op_cache(self):
-        """Test private _add_op method functions with a matrix cache."""
+    def test_add_obj_cache(self):
+        """Test private _add_obj method functions with a matrix cache."""
         cache = {"matrices": []}
         op1 = qml.QubitUnitary(np.eye(2), wires=0)
-        assert _add_op(
-            op1, ["", ""], _Config(wire_map={0: 0, 1: 1}, bit_map=default_bit_map, cache=cache)
+        config = _Config(
+            wire_map={0: 0, 1: 1},
+            bit_map=default_bit_map,
+            cache=cache,
+            num_op_layers=4,
+            cur_layer=1,
+        )
+        assert _add_obj(
+            op1,
+            ["", ""],
+            config,
         ) == ["U(M0)", ""]
 
         assert qml.math.allclose(cache["matrices"][0], np.eye(2))
         op2 = qml.QubitUnitary(np.eye(2), wires=1)
-        assert _add_op(
-            op2, ["", ""], _Config(wire_map={0: 0, 1: 1}, bit_map=default_bit_map, cache=cache)
+        assert _add_obj(
+            op2,
+            ["", ""],
+            config,
         ) == ["", "U(M0)"]
 
 
@@ -707,3 +776,28 @@ class TestShowMatrices:
         )
 
         assert tape_text(tape_matrices, show_matrices=True, cache=cache) == expected
+
+
+def test_nested_tapes():
+    """Test nested tapes inside the qnode."""
+
+    def circ():
+        with qml.tape.QuantumTape():
+            qml.PauliX(0)
+            with qml.tape.QuantumTape():
+                qml.PauliY(0)
+        with qml.tape.QuantumTape():
+            qml.PauliZ(0)
+            with qml.tape.QuantumTape():
+                qml.PauliX(0)
+        return qml.expval(qml.PauliZ(0))
+
+    expected = (
+        "0: ──Tape:0──Tape:1─┤  <Z>\n\n"
+        "Tape:0\n0: ──X──Tape:2─┤  \n\n"
+        "Tape:2\n0: ──Y─┤  \n\n"
+        "Tape:1\n0: ──Z──Tape:3─┤  \n\n"
+        "Tape:3\n0: ──X─┤  "
+    )
+
+    assert qml.draw(circ)() == expected