# Copyright 2018-2021 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 :class:`QueuingManager`.
"""

import copy
from collections import OrderedDict
from contextlib import contextmanager
from warnings import warn


def __getattr__(name):
    # for more information on overwriting `__getattr__`, see https://peps.python.org/pep-0562/
    if name == "QueuingContext":
        warn("QueuingContext has been renamed qml.queuing.QueuingManager.", UserWarning)
        return QueuingManager
    try:
        return globals()[name]
    except KeyError as e:
        raise AttributeError from e


class QueuingError(Exception):
    """Exception that is raised when there is a queuing error"""


class QueuingManager:
    """Singleton global entry point for managing active recording contexts.

    This class consists purely of class methods. It both maintains a list of
    recording queues and allows communication with the currently active object.

    Queueable objects, like :class:`~.operation.Operator` and :class:`~.measurements.MeasurementProcess`, should
    use ``QueuingManager`` as an entry point for accessing the active queue.

    See also: :class:`~.AnnotatedQueue`, :class:`~.tape.QuantumTape`, :meth:`~.operation.Operator.queue`.

    Recording queues, such as :class:`~.AnnotatedQueue`, must define the following methods:

    * ``append``: define an action to perform when an object append
      request is made.

    * ``remove``: define an action to perform when an object removal request is made.

    * ``get_info``: retrieve the object's metadata

    * ``update_info``: Update an object's metadata if it is already queued.

    To start and end recording, the recording queue can use the :meth:`add_active_queue` and
    :meth:`remove_active_queue` methods.

    """

    _active_contexts = []
    """The stack of contexts that are currently active."""

    @classmethod
    def add_active_queue(cls, queue):
        """Makes a queue the currently active recording context."""
        cls._active_contexts.append(queue)

    @classmethod
    def remove_active_queue(cls):
        """Ends recording on the currently active recording queue."""
        return cls._active_contexts.pop()

    @classmethod
    def recording(cls):
        """Whether a queuing context is active and recording operations"""
        return bool(cls._active_contexts)

    @classmethod
    def active_context(cls):
        """Returns the currently active queuing context."""
        return cls._active_contexts[-1] if cls.recording() else None

    @classmethod
    @contextmanager
    def stop_recording(cls):
        """A context manager and decorator to ensure that contained logic is non-recordable
        or non-queueable within a QNode or quantum tape context.

        **Example:**

        Consider the function:

        >>> def list_of_ops(params, wires):
        ...     return [
        ...         qml.RX(params[0], wires=wires),
        ...         qml.RY(params[1], wires=wires),
        ...         qml.RZ(params[2], wires=wires)
        ...     ]

        If executed in a recording context, the operations constructed in the function will be queued:

        >>> dev = qml.device("default.qubit", wires=2)
        >>> @qml.qnode(dev)
        ... def circuit(params):
        ...     ops = list_of_ops(params, wires=0)
        ...     qml.apply(ops[-1])  # apply the last operation from the list again
        ...     return qml.expval(qml.PauliZ(0))
        >>> print(qml.draw(circuit)([1, 2, 3]))
        0: ──RX(1.00)──RY(2.00)──RZ(3.00)──RZ(3.00)─┤  <Z>

        Using the ``stop_recording`` context manager, all logic contained inside is not queued or recorded.

        >>> @qml.qnode(dev)
        ... def circuit(params):
        ...     with qml.QueuingManager.stop_recording():
        ...         ops = list_of_ops(params, wires=0)
        ...     qml.apply(ops[-1])
        ...     return qml.expval(qml.PauliZ(0))
        >>> print(qml.draw(circuit)([1, 2, 3]))
        0: ──RZ(3.00)─┤  <Z>

        The context manager can also be used as a decorator on a function:

        >>> @qml.QueuingManager.stop_recording()
        ... def list_of_ops(params, wires):
        ...     return [
        ...         qml.RX(params[0], wires=wires),
        ...         qml.RY(params[1], wires=wires),
        ...         qml.RZ(params[2], wires=wires)
        ...     ]
        >>> @qml.qnode(dev)
        ... def circuit(params):
        ...     ops = list_of_ops(params, wires=0)
        ...     qml.apply(ops[-1])
        ...     return qml.expval(qml.PauliZ(0))
        >>> print(qml.draw(circuit)([1, 2, 3]))
        0: ──RZ(3.00)─┤  <Z>

        """
        previously_active_contexts = cls._active_contexts
        cls._active_contexts = []
        yield
        cls._active_contexts = previously_active_contexts

    @classmethod
    def append(cls, obj, **kwargs):
        """Append an object to the queue(s).

        Args:
            obj: the object to be appended
        """
        if cls.recording():
            cls.active_context().append(obj, **kwargs)

    @classmethod
    def remove(cls, obj):
        """Remove an object from the queue(s) if it is in the queue(s).

        Args:
            obj: the object to be removed
        """
        if cls.recording():
            cls.active_context().remove(obj)

    @classmethod
    def update_info(cls, obj, **kwargs):
        """Updates information of an object in the active queue if it is already in the queue.

        Args:
            obj: the object with metadata to be updated
        """
        if cls.recording():
            cls.active_context().update_info(obj, **kwargs)

    @classmethod
    def safe_update_info(cls, obj, **kwargs):
        """Updates information of an object in the active queue if it is already in the queue.

        Args:
            obj: the object with metadata to be updated
        """
        warn(
            "QueuingManager.safe_update_info is deprecated."
            "It's behavior has been moved to `update_info`.",
            UserWarning,
        )
        cls.update_info(obj, **kwargs)

    @classmethod
    def get_info(cls, obj):
        """Retrieves information of an object in the active queue.

        Args:
            obj: the object with metadata to be retrieved

        Returns:
            object metadata
        """
        return cls.active_context().get_info(obj) if cls.recording() else None


class AnnotatedQueue:
    """Lightweight class that maintains a basic queue of operations, in addition
    to metadata annotations."""

    def __init__(self):
        self._queue = OrderedDict()

    def __enter__(self):
        """Adds this instance to the global list of active contexts.

        Returns:
            AnnotatedQueue: this instance
        """
        QueuingManager.add_active_queue(self)

        return self

    def __exit__(self, exception_type, exception_value, traceback):
        """Remove this instance from the global list of active contexts."""
        QueuingManager.remove_active_queue()

    def append(self, obj, **kwargs):
        """Append ``obj`` into the queue with ``kwargs`` metadata."""
        self._queue[obj] = kwargs

    def remove(self, obj):
        """Remove ``obj`` from the queue.  Raises ``KeyError`` if ``obj`` is not already in the queue."""
        del self._queue[obj]

    def update_info(self, obj, **kwargs):
        """Update ``obj``'s metadata with ``kwargs`` if it exists in the queue."""
        if obj in self._queue:
            self._queue[obj].update(kwargs)

    def safe_update_info(self, obj, **kwargs):
        """Update ``obj``'s metadata with ``kwargs`` if it exists in the queue."""
        warn(
            "AnnotatedQueue.safe_update_info is deprecated."
            "It's behavior has been moved to `update_info`.",
            UserWarning,
        )
        self.update_info(obj, **kwargs)

    def get_info(self, obj):
        """Retrieve the metadata for ``obj``.  Raises a ``QueuingError`` if obj is not in the queue."""
        if obj not in self._queue:
            raise QueuingError(f"Object {obj} not in the queue.")

        return self._queue[obj]

    @property
    def queue(self):
        """Returns a list of objects in the annotated queue"""
        return list(self._queue.keys())


def apply(op, context=QueuingManager):
    """Apply an instantiated operator or measurement to a queuing context.

    Args:
        op (.Operator or .MeasurementProcess): the operator or measurement to apply/queue
        context (.QueuingManager): The queuing context to queue the operator to.
            Note that if no context is specified, the operator is
            applied to the currently active queuing context.
    Returns:
        .Operator or .MeasurementProcess: the input operator is returned for convenience

    **Example**

    In PennyLane, **operations and measurements are 'queued' or added to a circuit
    when they are instantiated**.

    The ``apply`` function can be used to add operations that might have
    already been instantiated elsewhere to the QNode:

    .. code-block:: python

        op = qml.RX(0.4, wires=0)
        dev = qml.device("default.qubit", wires=2)

        @qml.qnode(dev)
        def circuit(x):
            qml.RY(x, wires=0)  # applied during instantiation
            qml.apply(op)  # manually applied
            return qml.expval(qml.PauliZ(0))

    >>> print(qml.draw(circuit)(0.6))
    0: ──RY(0.6)──RX(0.4)──┤ ⟨Z⟩

    It can also be used to apply functions repeatedly:

    .. code-block:: python

        @qml.qnode(dev)
        def circuit(x):
            qml.apply(op)
            qml.RY(x, wires=0)
            qml.apply(op)
            return qml.expval(qml.PauliZ(0))

    >>> print(qml.draw(circuit)(0.6))
    0: ──RX(0.4)──RY(0.6)──RX(0.4)──┤ ⟨Z⟩

    .. details::
        :title: Usage Details

        Instantiated measurements can also be applied to queuing contexts
        using ``apply``:

        .. code-block:: python

            meas = qml.expval(qml.PauliZ(0) @ qml.PauliY(1))
            dev = qml.device("default.qubit", wires=2)

            @qml.qnode(dev)
            def circuit(x):
                qml.RY(x, wires=0)
                qml.CNOT(wires=[0, 1])
                return qml.apply(meas)

        >>> print(qml.draw(circuit)(0.6))
         0: ──RY(0.6)──╭●──╭┤ ⟨Z ⊗ Y⟩
         1: ───────────╰X──╰┤ ⟨Z ⊗ Y⟩

        By default, ``apply`` will queue operators to the currently
        active queuing context.

        When working with low-level queuing contexts such as quantum tapes,
        the desired context to queue the operation to can be explicitly
        passed:

        .. code-block:: python

            with qml.tape.QuantumTape() as tape1:
                qml.Hadamard(wires=1)

                with qml.tape.QuantumTape() as tape2:
                    # Due to the nesting behaviour of queuing contexts,
                    # tape2 will be queued to tape1.

                    # The following PauliX operation will be queued
                    # to the active queuing context, tape2, during instantiation.
                    op1 = qml.PauliX(wires=0)

                    # We can use qml.apply to apply the same operation to tape1
                    # without leaving the tape2 context.
                    qml.apply(op1, context=tape1)

                    qml.RZ(0.2, wires=0)

                qml.CNOT(wires=[0, 1])

        >>> tape1.operations
        [Hadamard(wires=[1]), <QuantumTape: wires=[0], params=1>, PauliX(wires=[0]), CNOT(wires=[0, 1])]
        >>> tape2.operations
        [PauliX(wires=[0]), RZ(0.2, wires=[0])]
    """
    if not QueuingManager.recording():
        raise RuntimeError("No queuing context available to append operation to.")

    if op in getattr(context, "queue", QueuingManager.active_context().queue):
        # Queuing contexts can only contain unique objects.
        # If the object to be queued already exists, copy it.
        op = copy.copy(op)

    if hasattr(op, "queue"):
        # operator provides its own logic for queuing
        op.queue(context=context)
    else:
        # append the operator directly to the relevant queuing context
        context.append(op)

    return op

pennylane/queuing.py
# Copyright 2018-2021 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 :class:`QueuingManager`.
"""

import copy
from collections import OrderedDict
from contextlib import contextmanager
from warnings import warn


def __getattr__(name):
    # for more information on overwriting `__getattr__`, see https://peps.python.org/pep-0562/
    if name == "QueuingContext":
        warn("QueuingContext has been renamed qml.queuing.QueuingManager.", UserWarning)
        return QueuingManager
    try:
        return globals()[name]
    except KeyError as e:
        raise AttributeError from e


class QueuingError(Exception):
    """Exception that is raised when there is a queuing error"""


class QueuingManager:
    """Singleton global entry point for managing active recording contexts.

    This class consists purely of class methods. It both maintains a list of
    recording queues and allows communication with the currently active object.

    Queueable objects, like :class:`~.operation.Operator` and :class:`~.measurements.MeasurementProcess`, should
    use ``QueuingManager`` as an entry point for accessing the active queue.

    See also: :class:`~.AnnotatedQueue`, :class:`~.tape.QuantumTape`, :meth:`~.operation.Operator.queue`.

    Recording queues, such as :class:`~.AnnotatedQueue`, must define the following methods:

    * ``append``: define an action to perform when an object append
      request is made.

    * ``remove``: define an action to perform when an object removal request is made.

    * ``get_info``: retrieve the object's metadata

    * ``update_info``: Update an object's metadata if it is already queued.

    To start and end recording, the recording queue can use the :meth:`add_active_queue` and
    :meth:`remove_active_queue` methods.

    """

    _active_contexts = []
    """The stack of contexts that are currently active."""

    @classmethod
    def add_active_queue(cls, queue):
        """Makes a queue the currently active recording context."""
        cls._active_contexts.append(queue)

    @classmethod
    def remove_active_queue(cls):
        """Ends recording on the currently active recording queue."""
        return cls._active_contexts.pop()

    @classmethod
    def recording(cls):
        """Whether a queuing context is active and recording operations"""
        return bool(cls._active_contexts)

    @classmethod
    def active_context(cls):
        """Returns the currently active queuing context."""
        return cls._active_contexts[-1] if cls.recording() else None

    @classmethod
    @contextmanager
    def stop_recording(cls):
        """A context manager and decorator to ensure that contained logic is non-recordable
        or non-queueable within a QNode or quantum tape context.

        **Example:**

        Consider the function:

        >>> def list_of_ops(params, wires):
        ...     return [
        ...         qml.RX(params[0], wires=wires),
        ...         qml.RY(params[1], wires=wires),
        ...         qml.RZ(params[2], wires=wires)
        ...     ]

        If executed in a recording context, the operations constructed in the function will be queued:

        >>> dev = qml.device("default.qubit", wires=2)
        >>> @qml.qnode(dev)
        ... def circuit(params):
        ...     ops = list_of_ops(params, wires=0)
        ...     qml.apply(ops[-1])  # apply the last operation from the list again
        ...     return qml.expval(qml.PauliZ(0))
        >>> print(qml.draw(circuit)([1, 2, 3]))
        0: ──RX(1.00)──RY(2.00)──RZ(3.00)──RZ(3.00)─┤  <Z>

        Using the ``stop_recording`` context manager, all logic contained inside is not queued or recorded.

        >>> @qml.qnode(dev)
        ... def circuit(params):
        ...     with qml.QueuingManager.stop_recording():
        ...         ops = list_of_ops(params, wires=0)
        ...     qml.apply(ops[-1])
        ...     return qml.expval(qml.PauliZ(0))
        >>> print(qml.draw(circuit)([1, 2, 3]))
        0: ──RZ(3.00)─┤  <Z>

        The context manager can also be used as a decorator on a function:

        >>> @qml.QueuingManager.stop_recording()
        ... def list_of_ops(params, wires):
        ...     return [
        ...         qml.RX(params[0], wires=wires),
        ...         qml.RY(params[1], wires=wires),
        ...         qml.RZ(params[2], wires=wires)
        ...     ]
        >>> @qml.qnode(dev)
        ... def circuit(params):
        ...     ops = list_of_ops(params, wires=0)
        ...     qml.apply(ops[-1])
        ...     return qml.expval(qml.PauliZ(0))
        >>> print(qml.draw(circuit)([1, 2, 3]))
        0: ──RZ(3.00)─┤  <Z>

        """
        previously_active_contexts = cls._active_contexts
        cls._active_contexts = []
        try:
            yield
        except Exception as e:
            cls._active_contexts = previously_active_contexts
            raise e
        cls._active_contexts = previously_active_contexts

    @classmethod
    def append(cls, obj, **kwargs):
        """Append an object to the queue(s).

        Args:
            obj: the object to be appended
        """
        if cls.recording():
            cls.active_context().append(obj, **kwargs)

    @classmethod
    def remove(cls, obj):
        """Remove an object from the queue(s) if it is in the queue(s).

        Args:
            obj: the object to be removed
        """
        if cls.recording():
            cls.active_context().remove(obj)

    @classmethod
    def update_info(cls, obj, **kwargs):
        """Updates information of an object in the active queue if it is already in the queue.

        Args:
            obj: the object with metadata to be updated
        """
        if cls.recording():
            cls.active_context().update_info(obj, **kwargs)

    @classmethod
    def safe_update_info(cls, obj, **kwargs):
        """Updates information of an object in the active queue if it is already in the queue.

        Args:
            obj: the object with metadata to be updated
        """
        warn(
            "QueuingManager.safe_update_info is deprecated."
            "It's behavior has been moved to `update_info`.",
            UserWarning,
        )
        cls.update_info(obj, **kwargs)

    @classmethod
    def get_info(cls, obj):
        """Retrieves information of an object in the active queue.

        Args:
            obj: the object with metadata to be retrieved

        Returns:
            object metadata
        """
        return cls.active_context().get_info(obj) if cls.recording() else None


class AnnotatedQueue:
    """Lightweight class that maintains a basic queue of operations, in addition
    to metadata annotations."""

    def __init__(self):
        self._queue = OrderedDict()

    def __enter__(self):
        """Adds this instance to the global list of active contexts.

        Returns:
            AnnotatedQueue: this instance
        """
        QueuingManager.add_active_queue(self)

        return self

    def __exit__(self, exception_type, exception_value, traceback):
        """Remove this instance from the global list of active contexts."""
        QueuingManager.remove_active_queue()

    def append(self, obj, **kwargs):
        """Append ``obj`` into the queue with ``kwargs`` metadata."""
        self._queue[obj] = kwargs

    def remove(self, obj):
        """Remove ``obj`` from the queue.  Raises ``KeyError`` if ``obj`` is not already in the queue."""
        del self._queue[obj]

    def update_info(self, obj, **kwargs):
        """Update ``obj``'s metadata with ``kwargs`` if it exists in the queue."""
        if obj in self._queue:
            self._queue[obj].update(kwargs)

    def safe_update_info(self, obj, **kwargs):
        """Update ``obj``'s metadata with ``kwargs`` if it exists in the queue."""
        warn(
            "AnnotatedQueue.safe_update_info is deprecated."
            "It's behavior has been moved to `update_info`.",
            UserWarning,
        )
        self.update_info(obj, **kwargs)

    def get_info(self, obj):
        """Retrieve the metadata for ``obj``.  Raises a ``QueuingError`` if obj is not in the queue."""
        if obj not in self._queue:
            raise QueuingError(f"Object {obj} not in the queue.")

        return self._queue[obj]

    @property
    def queue(self):
        """Returns a list of objects in the annotated queue"""
        return list(self._queue.keys())


def apply(op, context=QueuingManager):
    """Apply an instantiated operator or measurement to a queuing context.

    Args:
        op (.Operator or .MeasurementProcess): the operator or measurement to apply/queue
        context (.QueuingManager): The queuing context to queue the operator to.
            Note that if no context is specified, the operator is
            applied to the currently active queuing context.
    Returns:
        .Operator or .MeasurementProcess: the input operator is returned for convenience

    **Example**

    In PennyLane, **operations and measurements are 'queued' or added to a circuit
    when they are instantiated**.

    The ``apply`` function can be used to add operations that might have
    already been instantiated elsewhere to the QNode:

    .. code-block:: python

        op = qml.RX(0.4, wires=0)
        dev = qml.device("default.qubit", wires=2)

        @qml.qnode(dev)
        def circuit(x):
            qml.RY(x, wires=0)  # applied during instantiation
            qml.apply(op)  # manually applied
            return qml.expval(qml.PauliZ(0))

    >>> print(qml.draw(circuit)(0.6))
    0: ──RY(0.6)──RX(0.4)──┤ ⟨Z⟩

    It can also be used to apply functions repeatedly:

    .. code-block:: python

        @qml.qnode(dev)
        def circuit(x):
            qml.apply(op)
            qml.RY(x, wires=0)
            qml.apply(op)
            return qml.expval(qml.PauliZ(0))

    >>> print(qml.draw(circuit)(0.6))
    0: ──RX(0.4)──RY(0.6)──RX(0.4)──┤ ⟨Z⟩

    .. details::
        :title: Usage Details

        Instantiated measurements can also be applied to queuing contexts
        using ``apply``:

        .. code-block:: python

            meas = qml.expval(qml.PauliZ(0) @ qml.PauliY(1))
            dev = qml.device("default.qubit", wires=2)

            @qml.qnode(dev)
            def circuit(x):
                qml.RY(x, wires=0)
                qml.CNOT(wires=[0, 1])
                return qml.apply(meas)

        >>> print(qml.draw(circuit)(0.6))
         0: ──RY(0.6)──╭●──╭┤ ⟨Z ⊗ Y⟩
         1: ───────────╰X──╰┤ ⟨Z ⊗ Y⟩

        By default, ``apply`` will queue operators to the currently
        active queuing context.

        When working with low-level queuing contexts such as quantum tapes,
        the desired context to queue the operation to can be explicitly
        passed:

        .. code-block:: python

            with qml.tape.QuantumTape() as tape1:
                qml.Hadamard(wires=1)

                with qml.tape.QuantumTape() as tape2:
                    # Due to the nesting behaviour of queuing contexts,
                    # tape2 will be queued to tape1.

                    # The following PauliX operation will be queued
                    # to the active queuing context, tape2, during instantiation.
                    op1 = qml.PauliX(wires=0)

                    # We can use qml.apply to apply the same operation to tape1
                    # without leaving the tape2 context.
                    qml.apply(op1, context=tape1)

                    qml.RZ(0.2, wires=0)

                qml.CNOT(wires=[0, 1])

        >>> tape1.operations
        [Hadamard(wires=[1]), <QuantumTape: wires=[0], params=1>, PauliX(wires=[0]), CNOT(wires=[0, 1])]
        >>> tape2.operations
        [PauliX(wires=[0]), RZ(0.2, wires=[0])]
    """
    if not QueuingManager.recording():
        raise RuntimeError("No queuing context available to append operation to.")

    if op in getattr(context, "queue", QueuingManager.active_context().queue):
        # Queuing contexts can only contain unique objects.
        # If the object to be queued already exists, copy it.
        op = copy.copy(op)

    if hasattr(op, "queue"):
        # operator provides its own logic for queuing
        op.queue(context=context)
    else:
        # append the operator directly to the relevant queuing context
        context.append(op)

    return op

pennylane/queuing.py
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index e2beb942cdb..fcc47fcc3ae 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -206,6 +206,9 @@ keyword argument when using `GellMann`, which determines which of the 8 Gell-Man
 * Fixed a bug where `qml.math.fidelity(non_trainable_state, trainable_state)` failed unexpectedly.
   [(#3160)](https://github.com/PennyLaneAI/pennylane/pull/3160)
 
+* Fixed a bug where `qml.QueuingManager.stop_recording` did not clean up if yielded code raises an exception.
+  [(#3182)](https://github.com/PennyLaneAI/pennylane/pull/3182)
+
 <h3>Contributors</h3>
 
 This release contains contributions from (in alphabetical order):
diff --git a/pennylane/queuing.py b/pennylane/queuing.py
index f044d34f8d9..c6d480c51ed 100644
--- a/pennylane/queuing.py
+++ b/pennylane/queuing.py
@@ -145,7 +145,11 @@ def stop_recording(cls):
         """
         previously_active_contexts = cls._active_contexts
         cls._active_contexts = []
-        yield
+        try:
+            yield
+        except Exception as e:
+            cls._active_contexts = previously_active_contexts
+            raise e
         cls._active_contexts = previously_active_contexts
 
     @classmethod
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index 7fa109c4d6e..1ba8d1fce5f 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -1658,6 +1658,15 @@ def test_creating_scratch_tape(self):
         assert len(temp_tape.operations) == 1
         assert temp_tape.operations[0] == op1
 
+    def test_stop_recording_within_tape_cleans_up(self):
+        """Test if some error is raised within a stop_recording context, the previously
+        active contexts are still returned to avoid popping from an empty deque"""
+
+        with pytest.raises(ValueError):
+            with qml.queuing.AnnotatedQueue() as q:
+                with qml.QueuingManager.stop_recording():
+                    raise ValueError
+
 
 def test_get_active_tape():
     """Test that the get_active_tape() function returns the currently

[BUG] Inline transforms vs. Decorator transforms
### Expected behavior

This  script:

```
def circuit(circuit_param):
    qml.RY(circuit_param, wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)

def noise(noise_param, wires):
    qml.CRX(noise_param, wires=[0,1])
    qml.CNOT(wires=[1,0])
    
dev = qml.device("default.mixed", wires=2)

noise_param = 0.3
    
@qml.qnode(dev)
@qml.transforms.insert(noise, noise_param, position="all")
def noisy_circuit(circuit_param):
        
    circuit(circuit_param)
    
    return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))

noisy_circuit(0.4)
```
And this one
```
def circuit(circuit_param):
    qml.RY(circuit_param, wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)

def noise(noise_param, wires):
    qml.CRX(noise_param, wires=[0,1])
    qml.CNOT(wires=[1,0])
    
dev = qml.device("default.mixed", wires=2)

noise_param = 0.3
    
@qml.qnode(dev)
def noisy_circuit(circuit_param):
        
    qml.transforms.insert(noise, noise_param, position="all")(circuit)(circuit_param)
    
    return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))

noisy_circuit(0.4)
```
should both return `array([0.64497588, 0.61505021, 0.40503904])`

### Actual behavior

The second script works, but the first one returns errors.

### Additional information

I know this is an issue with how tapes are handled in either case. Please reconsider how this interacts with users.

### Source code

```shell
def circuit(circuit_param):
    qml.RY(circuit_param, wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)

def noise(noise_param, wires):
    qml.CRX(noise_param, wires=[0,1])
    qml.CNOT(wires=[1,0])
    
dev = qml.device("default.mixed", wires=2)

noise_param = 0.3
    
@qml.qnode(dev)
@qml.transforms.insert(noise, noise_param, position="all")
def noisy_circuit(circuit_param):
        
    circuit(circuit_param)
    
    return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))

noisy_circuit(0.4)
```


### Tracebacks

```shell
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\anaconda3\lib\site-packages\pennylane\tape\tape.py in expand_tape(tape, depth, stop_at, expand_measurements)
    156             try:
--> 157                 rotations, diag_obs = qml.grouping.diagonalize_qwc_pauli_words(
    158                     tape._obs_sharing_wires

~\anaconda3\lib\site-packages\pennylane\grouping\transformations.py in diagonalize_qwc_pauli_words(qwc_grouping)
    139     if not are_pauli_words_qwc(qwc_grouping):
--> 140         raise ValueError("The list of Pauli words are not qubit-wise commuting.")
    141 

ValueError: The list of Pauli words are not qubit-wise commuting.

The above exception was the direct cause of the following exception:

QuantumFunctionError                      Traceback (most recent call last)
~\anaconda3\lib\site-packages\pennylane\transforms\qfunc_transforms.py in __call__(self, tape, *args, **kwargs)
    169         with tape_class() as new_tape:
--> 170             self.transform_fn(tape, *args, **kwargs)
    171 

~\anaconda3\lib\site-packages\pennylane\transforms\insert_ops.py in insert(circuit, op, op_args, position, before)
    212     # TODO: change this to be cleaner and more robust
--> 213     circuit = circuit.expand(
    214         stop_at=lambda op: not hasattr(qml.templates, op.name) and not isinstance(op, Adjoint)

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in expand(self, depth, stop_at, expand_measurements)
    686         """
--> 687         new_tape = expand_tape(
    688             self, depth=depth, stop_at=stop_at, expand_measurements=expand_measurements

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in expand_tape(tape, depth, stop_at, expand_measurements)
    172 
--> 173                 raise qml.QuantumFunctionError(
    174                     "Only observables that are qubit-wise commuting "

QuantumFunctionError: Only observables that are qubit-wise commuting Pauli words can be returned on the same wire, some of the following measurements do not commute:
[expval(PauliX(wires=[0])), expval(PauliY(wires=[0])), expval(PauliZ(wires=[0]))]

During handling of the above exception, another exception occurred:

IndexError                                Traceback (most recent call last)
~\anaconda3\lib\site-packages\pennylane\qnode.py in construct(self, args, kwargs)
    525         with self.tape:
--> 526             self._qfunc_output = self.func(*args, **kwargs)
    527         self._tape._qfunc_output = self._qfunc_output

~\anaconda3\lib\site-packages\pennylane\transforms\qfunc_transforms.py in internal_wrapper(*args, **kwargs)
    201         tape = make_tape(fn)(*args, **kwargs)
--> 202         tape = tape_transform(tape, *transform_args, **transform_kwargs)
    203 

~\anaconda3\lib\site-packages\pennylane\transforms\qfunc_transforms.py in __call__(self, tape, *args, **kwargs)
    169         with tape_class() as new_tape:
--> 170             self.transform_fn(tape, *args, **kwargs)
    171 

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in __exit__(self, exception_type, exception_value, traceback)
    389         try:
--> 390             super().__exit__(exception_type, exception_value, traceback)
    391             # After other optimizations in #2963, #2986 and follow-up work, we should check whether

~\anaconda3\lib\site-packages\pennylane\queuing.py in __exit__(self, exception_type, exception_value, traceback)
    127         """Remove this instance from the global list of active contexts."""
--> 128         QueuingContext._active_contexts.pop()
    129 

IndexError: pop from an empty deque

During handling of the above exception, another exception occurred:

IndexError                                Traceback (most recent call last)
<ipython-input-33-37d3113aba14> in <module>
     20     return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))
     21 
---> 22 noisy_circuit(0.4)

~\anaconda3\lib\site-packages\pennylane\qnode.py in __call__(self, *args, **kwargs)
    609 
    610         # construct the tape
--> 611         self.construct(args, kwargs)
    612 
    613         cache = self.execute_kwargs.get("cache", False)

~\anaconda3\lib\site-packages\pennylane\qnode.py in construct(self, args, kwargs)
    524 
    525         with self.tape:
--> 526             self._qfunc_output = self.func(*args, **kwargs)
    527         self._tape._qfunc_output = self._qfunc_output
    528 

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in __exit__(self, exception_type, exception_value, traceback)
    388     def __exit__(self, exception_type, exception_value, traceback):
    389         try:
--> 390             super().__exit__(exception_type, exception_value, traceback)
    391             # After other optimizations in #2963, #2986 and follow-up work, we should check whether
    392             # calling `_process_queue` only if there is no `exception_type` saves time. This would

~\anaconda3\lib\site-packages\pennylane\queuing.py in __exit__(self, exception_type, exception_value, traceback)
    126     def __exit__(self, exception_type, exception_value, traceback):
    127         """Remove this instance from the global list of active contexts."""
--> 128         QueuingContext._active_contexts.pop()
    129 
    130     @abc.abstractmethod

IndexError: pop from an empty deque
```


### System information

```shell
Name: PennyLane
Version: 0.26.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: None
Author-email: None
License: Apache License 2.0
Location: c:\users\alvaro\anaconda3\lib\site-packages
Requires: numpy, appdirs, cachetools, retworkx, toml, autograd, semantic-version, pennylane-lightning, autoray, scipy, networkx
Required-by: PennyLane-SF, pennylane-qulacs, PennyLane-Qchem, PennyLane-Lightning

Platform info:           Windows-10-10.0.19041-SP0
Python version:          3.8.8
Numpy version:           1.22.1
Scipy version:           1.6.2
Installed devices:
- default.gaussian (PennyLane-0.26.0)
- default.mixed (PennyLane-0.26.0)
- default.qubit (PennyLane-0.26.0)
- default.qubit.autograd (PennyLane-0.26.0)
- default.qubit.jax (PennyLane-0.26.0)
- default.qubit.tf (PennyLane-0.26.0)
- default.qubit.torch (PennyLane-0.26.0)
- default.qutrit (PennyLane-0.26.0)
- strawberryfields.fock (PennyLane-SF-0.19.0)
- strawberryfields.gaussian (PennyLane-SF-0.19.0)
- strawberryfields.gbs (PennyLane-SF-0.19.0)
- strawberryfields.remote (PennyLane-SF-0.19.0)
- strawberryfields.tf (PennyLane-SF-0.19.0)
- qulacs.simulator (pennylane-qulacs-0.16.0)
- lightning.qubit (PennyLane-Lightning-0.26.0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.
