Why does my context manager not exit on exception
Asked Answered
J

1

6

I am learning about context managers and was trying to build one myself. The following is a dummy context manager that opens a file in read mode (I know I can just do with open(...): .... this is just an example I built to help me understand how to make my own context managers):

@contextmanager
def open_read(path: str):
    f = open(path, 'r')
    print('open')
    yield f
    f.close()
    print('closed')


def foo():
    try:
        with open_read('main.py') as f:
            print(f.readline())
            raise Exception('oopsie')
    except Exception:
        pass
    print(f.readline())


foo()

I expect this code to print:

open
<line 1 of a.txt>
closed
ValueError: I/O operation on closed file.

But instead it prints:

open
<line 1 of a.txt>
<line 2 of a.txt>

It didn't close the file!

This seems to contradict python's docs which state that __exit__ will be called whether the with statement exited successfully or with an exception:

object.exit(self, exc_type, exc_value, traceback)

Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.

Interestingly, when I reimplemented the context manager as shown below, it worked as expected:

class open_read(ContextDecorator):
    def __init__(self, path: str):
        self.path = path
        self.f = None

    def __enter__(self):
        self.f = open(self.path, 'r')
        print('open')
        return self.f

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.f.close()
        print('closed')

Why didn't my original implementation work?

Joannejoannes answered 9/2, 2023 at 22:11 Comment(1)
Your first attempt at a context manager is missing a try-finally block, so when the exception is re-raised inside the generator it just propagates out. See contextmanager in the docsCathar
T
7

The line f.close() is never reached (we exit that frame early due to unhandled exception), and then the exception was "handled" in the outer frame (i.e. within foo).

If you want it to close regardless, you'll have to implement it like that:

@contextmanager
def open_read(path: str):
    f = open(path, 'r')
    try:
        print('open')
        yield f
    finally:
        f.close()
        print('closed')

However, I'd like to point out that that the built-in open is already returning a context-manager, and you may be reinventing stdlib contextlib.closing.

Torrlow answered 9/2, 2023 at 22:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.