Another solution: by using my yield-from-as-an-iterator library, you can turn any yield from foo into
for value, handle_send, handle_throw in yield_from(foo):
    try:
        handle_send((yield value))
    except:
        if not handle_throw(*sys.exc_info()):
            raise
To make sure this answer stands alone even if the PyPI package is ever lost, here is an entire copy of that library's yieldfrom.py from the 1.0.0 release:
# SPDX-License-Identifier: 0BSD
# Copyright 2022 Alexander Kozhevnikov <mentalisttraceur@gmail.com>
"""A robust implementation of ``yield from`` behavior.
Allows transpilers, backpilers, and code that needs
to be portable to minimal or old Pythons to replace
    yield from ...
with
    for value, handle_send, handle_throw in yield_from(...):
        try:
            handle_send(yield value)
        except:
            if not handle_throw(*sys.exc_info()):
                raise
"""
__version__ = '1.0.0'
__all__ = ('yield_from',)
class yield_from(object):
    """Implementation of the logic that ``yield from`` adds around ``yield``."""
    __slots__ = ('_iterator', '_next', '_default_next')
    def __init__(self, iterable):
        """Initializes the yield_from instance.
        Arguments:
            iterable: The iterable to yield from and forward to.
        """
        # Mutates:
        #     self._next: Prepares to use built-in function next in __next__
        #         for the first iteration on the iterator.
        #     self._default_next: Saves initial self._next tuple for reuse.
        self._iterator = iter(iterable)
        self._next = self._default_next = next, (self._iterator,)
    def __repr__(self):
        """Represent the yield_from instance as a string."""
        return type(self).__name__ + '(' + repr(self._iterator) + ')'
    def __iter__(self):
        """Return the yield_from instance, which is itself an iterator."""
        return self
    def __next__(self):
        """Execute the next iteration of ``yield from`` on the iterator.
        Returns:
            Any: The next value from the iterator.
        Raises:
            StopIteration: If the iterator is exhausted.
            Any: If the iterator raises an error.
        """
        # Mutates:
        #     self._next: Resets to default, in case handle_send or
        #         or handle_throw changed it for this iteration.
        next_, arguments = self._next
        self._next = self._default_next
        value = next_(*arguments)
        return value, self.handle_send, self.handle_throw
    next = __next__  # Python 2 used `next` instead of ``__next__``
    def handle_send(self, value):
        """Handle a send method call for a yield.
        Arguments:
            value: The value sent through the yield.
        Raises:
            AttributeError: If the iterator has no send method.
        """
        # Mutates:
        #     self._next: If value is not None, prepares to use the
        #         iterator's send attribute instead of the built-in
        #         function next in the next iteration of __next__.
        if value is not None:
            self._next = self._iterator.send, (value,)
    def handle_throw(self, type, exception, traceback):
        """Handle a throw method call for a yield.
        Arguments:
            type: The type of the exception thrown through the yield.
                If this is GeneratorExit, the iterator will be closed
                by callings its close attribute if it has one.
            exception: The exception thrown through the yield.
            traceback: The traceback of the exception thrown through the yield.
        Returns:
            bool: Whether the exception will be forwarded to the iterator.
                If this is false, you should bubble up the exception.
                If this is true, the exception will be thrown into the
                iterator at the start of the next iteration, and will
                either be handled or bubble up at that time.
        Raises:
            TypeError: If type is not a class.
            GeneratorExit: Re-raised after successfully closing the iterator.
            Any: If raised by the close function on the iterator.
        """
        # Mutates:
        #     self._next: If type was not GeneratorExit and the iterator
        #         has a throw attribute, prepares to use that attribute
        #         instead of the built-in function next in the next
        #         iteration of __next__.
        iterator = self._iterator
        if issubclass(type, GeneratorExit):
            try:
                close = iterator.close
            except AttributeError:
                return False
            close()
            return False
        try:
            throw = iterator.throw
        except AttributeError:
            return False
        self._next = throw, (type, exception, traceback)
        return True
What I really like about this way is that:
- The implementation is much easier to fully think through and verify for correctness than the alternatives*.
- The usage is still simple, and doesn't require decorators or any other code changes anywhere other than just replacing the yield from ...line.
- It still has robust forwarding of .sendand.throwand handling of errors,StopIteration, andGeneratorExit.
- This yield_fromimplementation will work on any Python 3 and on Python 2 all the way back to Python 2.5**.
* The formal specification ends up entangling all the logic into one big loop, even with some duplication thrown in. All the fully-featured backport implementations I've seen further add complication on top of that. But we can do better by embracing manually implementing the iterator protocol:
- We get StopIterationhandling for free from Python itself around our__next__method.
- The logic can be split into separate pieces which are entirely decoupled except for the state-saving between them, which frees you from having to de-tangle the logic by yourself - fundamentally yield fromis just three simple ideas:
- call a method on the iterator to get the next element,
- how to handle .send(which may change the method called in step 1), and
- how to handle .throw(which may change the method called in step 1).
 
- By asking for modest boilerplate at each yield fromreplacement, we can avoid needing any hidden magic with special wrapper types, decorators, and so on.
** Python 2.5 is when PEP-342 made yield an expression and added GeneratorExit. Though if you are ever unfortunate enough to need to backport or "backpile" (transpile to an older version of the language) this yield_from would still do all the hard parts of building yield from on top of yield for you.
Also, this idea leaves a lot of freedom for how how usage boilerplate looks. For example,
- handle_throwcould be trivially refactored into a context manager, enabling usage like this:
 - for value, handle_send, handle_throw in yield_from(foo):
    with handle_throw:
        handle_send(yield value)
 - and 
- you could make - value, handle_send, handle_throwsomething like a named tuple if you find this usage nicer:
 - for step in yield_from(foo):
    with step.handle_throw:
        step.handle_send(yield step.value)