why is GeneratorExit raised by this combination of yield & exception suppressing ContextDecorator?

62 Views Asked by At

My goal is that instead of writing

for item in items:
    with MyCD():
       ... # processing logic

I would like to have a single line

    for item in MyCD.yield_each_item_wrapped(items):
        ... # processing logic

However, despite my ContextDecorator suppressing errors by returning True in __exit__ on error, the yielding the item after an item that raised error it immediately gets a GeneratorError.

So it seems that despite having handled the exception raised, the generator's .close() is called.

I'm running python 3.11.4

I would have expected to continue processing the remaining two items, as I deliberately suppressed the raised exception, i.e.: as the below snippet does

def yield2(items):
    for i, item in enumerate(items):
        with MyCD():
            if item:
                raise Exception("my error")
            yield i  # yield success


if __name__ == "__main__":
    raising_flags = [True, False, False, True]
    expected = [1, 2]
    actual = [i for i in yield2(raising_flags)]
    assert expected == actual, (expected, actual)

import traceback
from contextlib import ContextDecorator


class MyCD(ContextDecorator):
    @classmethod
    def yield_each_item_wrapped(cls, items, **kwargs):
        for i, item in enumerate(items):
            print(f"yield_each_item_wrapped {i=}")
            print(f"yield_each_item_wrapped {item=}")
            with cls(**kwargs):
                yield item

    def __enter__(self):
        print("__enter__")

    def __exit__(self, exc_type, exc, exc_tb):
        print("__exit__")
        if exc:
            traceback.print_exc()
            return True  # do not propagate


if __name__ == "__main__":
    raising_flags = [True, False, False, True]
    expected = [1, 2]
    actual = []
    for i, raising in enumerate(MyCD.yield_each_item_wrapped(raising_flags)):
        if raising:
            raise Exception("my error")
        actual.append(i)
    assert expected == actual, (expected, actual)

produces

yield_each_item_wrapped i=0
yield_each_item_wrapped item=True
__enter__
__exit__
Traceback (most recent call last):
  File "x.py", line 12, in yield_each_item_wrapped
    yield item
GeneratorExit
yield_each_item_wrapped i=1
yield_each_item_wrapped item=False
__enter__
Exception ignored in: <generator object MyCD.yield_each_item_wrapped at 0x7f42e625aac0>
Traceback (most recent call last):
  File "x.py", line 30, in <module>
    raise Exception("my error")
RuntimeError: generator ignored GeneratorExit
Traceback (most recent call last):
  File "x.py", line 30, in <module>
    raise Exception("my error")
Exception: my error
0

There are 0 best solutions below