Search code examples
pythoncontextmanager

How does contextmanager forward a raised exception to the function that it decorates?


How do I write a class that can be used in place of contextmanager in this example?

from contextlib import contextmanager

@contextmanager
def f():
    try: 
        yield None
    except Exception as e:
        print(e) # This statement is executed!

with f() as _:
    raise Exception("foo")

My attempt:

class MyOwnContextManager:
    def __init__(self, f):
        self.f = f

    def __enter__(self):
        return self.g.__next__()        

    def __exit__(self, type, value, traceback):
        pass # What exactly do I put here?

    def __call__(self, *args, **kwds):
        self.g = self.f(*args, **kwds)
        return self

@MyOwnContextManager
def f():
    try: 
        yield None
    except Exception as e:
        print(e) # This statement is NOT executed!

with f() as _:
    raise Exception("foo")

MyOwnContextManager doesn't let f handle the raised Exception, unlike the built-in contextmanager, which calls print(e). How do I handle the exception that is raised inside of the context with the function that is decorated by MyOwnContextManager?


Solution

  • The magic happens with self.g.throw where you pass it back to your own block.

    
    class MyOwnContextManager:
        def __init__(self, f):
            self.f = f
    
        def __enter__(self):
            return self.g.__next__()        
    
        def __exit__(self, type, value, traceback):
            try:
                self.g.throw(type, value, traceback) # go back to your function
                raise RuntimeError("generator didn't stop after throw()")
            except StopIteration as exc:
                print("stopping itter returning", exc is not value, value)
                return exc is not value
    
        def __call__(self, *args, **kwds):
            self.g = self.f(*args, **kwds)
            return self
    
    @MyOwnContextManager
    def f():
        print("init")
        try: 
            print("yielding")
            yield None
            print("yiedled") # will not happen
        except Exception as e:
            print("handler", e) 
    

    Output

    init  
    yielding  
    go  
    handler foo
    stopping itter returning True <class 'Exception'> foo