Search code examples
pythoncontextmanager

abort execution of with statement from context manager


I am relatively new to decorators, context-managers etc in python. Basically I want to do something like the following:

@contextmanager
def cd(to_dir):
    from_dir = os.getcwd()
    try:
        os.chdir(os.path.expanduser(to_dir))
        yield
    except Exception:
        log.error(traceback.format_exc())
        log.error(f"Failed to cd into {to_dir} from {from_dir}")
    finally:
        os.chdir(from_dir)

I would like to use it in something like this:

with cd('here'):
  ...
  ...
  ...

I specifically want to ensure that the body of the with statement is not executed if the cd into here fails which is why I am attempting to catch the Exception inside the context manager and prevent it from yielding. However it seems a python generator must yield or I get an error such as:

    raise RuntimeError("generator didn't yield") from None                                                                                         |
RuntimeError: generator didn't yield  

How do I suppress execution of the body of with if the chdir fails?

One solution I can think of is this:

@contextmanager
def cd(to_dir):
   from_dir = os.getcwd()
   try:
       os.chdir(os.path.expanduser(to_dir))
       yield True
   except Exception:
       log.error(traceback.format_exc())
       log.error(f"Failed to cd into {to_dir} from {from_dir}")
       yield False
   finally:
       os.chdir(from_dir)

Then use as follows:

with cd('here') as success:
  if success:
    ....
    ....

This however does require the additional conditionals whereever the with statement is used, so not so convenient to change everywhere in an existing program. Another issue with this is that if an exception occurs within the body of the with statement it also is captured by except clause inside the cd contextmanager function leading to a bogus error regarding failing to cd.


Solution

  • A context manager has no way of skipping the context body, other than raising an error. The context manager protocol only allows to return a value to be bound in the body (the target of as) but not to indicate skipping the body. Consequently, if entering a context might fail either a) do not suppress the failure exception or b) replace the failure exception with a new exception.

    @contextmanager
    def cd(to_dir):
        from_dir = os.getcwd()
        # if this fails, the exception bubbles out of the context manager
        os.chdir(os.path.expanduser(to_dir))
        try:
            yield
        finally:
            os.chdir(from_dir)
    

    If the initial os.chdir fails, the exception will "exit" the context at the with statement, without entering the body.