Context manager

Context managers are used to open and close resources as Python enters and exits a code block respectively. Some examples of the resources it can manage are files, database connections and sockets. In this module, we simulate how a context manager can handle open and close operations of a file-like object called StringIO.

from contextlib import contextmanager
from io import StringIO

# Simple directory with file contents
_FILESYSTEM = {
    "a.txt": "Hello World",
    "b.xml": "<message>Hello World</message>",
    "c.out": "10101010",
}


@contextmanager
def file(filename):
    """File context manager.

    This is the function variant of the context manager. Context managers
    are useful for resources that need to be opened and closed such as
    files, database connections and sockets.
    """
    io_buffer = StringIO(_FILESYSTEM[filename])
    try:
        # Pass the buffer to the context block
        yield io_buffer
    finally:
        # Close the buffer unconditionally
        io_buffer.close()


class FileHandler:
    """File handler context manager.

    This is the class variant of the context manager. Just like with
    iterators, it depends on context and preference that we design a
    class or simply write a function.
    """

    def __init__(self, filename):
        self.io_buffer = StringIO(_FILESYSTEM[filename])

    def __enter__(self):
        """Pass the buffer to the context block."""
        return self.io_buffer

    def __exit__(self, *args):
        """Close the buffer unconditionally."""
        self.io_buffer.close()


def main():
    # An example of a function-based context manager
    with file("a.txt") as txt_buffer:
        assert txt_buffer.read() == "Hello World"

    # An example of a class-based context manager
    with FileHandler("b.xml") as xml_buffer:
        assert xml_buffer.read() == "<message>Hello World</message>"

    # Examples of context manager failures
    for context_obj in (file, FileHandler):
        call_failed = False
        try:
            # Whenever any error happens in the context block, the buffer
            # in the context manager gets closed automatically and the
            # error gets raised to the outer block
            with context_obj("c.out"):
                raise RuntimeError("System crash. Abort!")
        except RuntimeError:
            call_failed = True
        assert call_failed is True


if __name__ == "__main__":
    main()