Skipping execution of -with- block

Question:

I am defining a context manager class and I would like to be able to skip the block of code without raising an exception if certain conditions are met during instantiation. For example,

class My_Context(object):
    def __init__(self,mode=0):
        """
        if mode = 0, proceed as normal
        if mode = 1, do not execute block
        """
        self.mode=mode
    def __enter__(self):
        if self.mode==1:
            print 'Exiting...'
            CODE TO EXIT PREMATURELY
    def __exit__(self, type, value, traceback):
        print 'Exiting...'

with My_Context(mode=1):
    print 'Executing block of codes...'
Asked By: Macad_hack

||

Answers:

What you’re trying to do isn’t possible, unfortunately. If __enter__ raises an exception, that exception is raised at the with statement (__exit__ isn’t called). If it doesn’t raise an exception, then the return value is fed to the block and the block executes.

Closest thing I could think of is a flag checked explicitly by the block:

class Break(Exception):
    pass

class MyContext(object):
    def __init__(self,mode=0):
        """
        if mode = 0, proceed as normal
        if mode = 1, do not execute block
        """
        self.mode=mode
    def __enter__(self):
        if self.mode==1:
            print 'Exiting...'
        return self.mode
    def __exit__(self, type, value, traceback):
        if type is None:
            print 'Normal exit...'
            return # no exception
        if issubclass(type, Break):
            return True # suppress exception
        print 'Exception exit...'

with MyContext(mode=1) as skip:
    if skip: raise Break()
    print 'Executing block of codes...'

This also lets you raise Break() in the middle of a with block to simulate a normal break statement.

Answered By: nneonneo

According to PEP-343, a with statement translates from:

with EXPR as VAR:
    BLOCK

to:

mgr = (EXPR)
exit = type(mgr).__exit__  # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
    try:
        VAR = value  # Only if "as VAR" is present
        BLOCK
    except:
        # The exceptional case is handled here
        exc = False
        if not exit(mgr, *sys.exc_info()):
            raise
        # The exception is swallowed if exit() returns true
finally:
    # The normal and non-local-goto cases are handled here
    if exc:
        exit(mgr, None, None, None)

As you can see, there is nothing obvious you can do from the call to the __enter__() method of the context manager that can skip the body (“BLOCK“) of the with statement.

People have done Python-implementation-specific things, such as manipulating the call stack inside of the __enter__(), in projects such as withhacks. I recall Alex Martelli posting a very interesting with-hack on stackoverflow a year or two back (don’t recall enough of the post off-hand to search and find it).

But the simple answer to your question / problem is that you cannot do what you’re asking, skipping the body of the with statement, without resorting to so-called “deep magic” (which is not necessarily portable between python implementations). With deep magic, you might be able to do it, but I recommend only doing such things as an exercise in seeing how it might be done, never in “production code”.

Answered By: Matt Anderson

If you want an ad-hoc solution that uses the ideas from withhacks (specifically from AnonymousBlocksInPython), this will work:

import sys
import inspect

class My_Context(object):
    def __init__(self,mode=0):
        """
        if mode = 0, proceed as normal
        if mode = 1, do not execute block
        """
        self.mode=mode
    def __enter__(self):
        if self.mode==1:
            print 'Met block-skipping criterion ...'
            # Do some magic
            sys.settrace(lambda *args, **keys: None)
            frame = inspect.currentframe(1)
            frame.f_trace = self.trace
    def trace(self, frame, event, arg):
        raise
    def __exit__(self, type, value, traceback):
        print 'Exiting context ...'
        return True

Compare the following:

with My_Context(mode=1):
    print 'Executing block of code ...'

with

with My_Context(mode=0):
    print 'Executing block of code ... '
Answered By: joe

A python 3 update to the hack mentioned by other answers from
withhacks (specifically from AnonymousBlocksInPython):

class SkipWithBlock(Exception):
    pass


class SkipContextManager:
    def __init__(self, skip):
        self.skip = skip

    def __enter__(self):
        if self.skip:
            sys.settrace(lambda *args, **keys: None)
            frame = sys._getframe(1)
            frame.f_trace = self.trace

    def trace(self, frame, event, arg):
        raise SkipWithBlock()

    def __exit__(self, type, value, traceback):
        if type is None:
            return  # No exception
        if issubclass(type, SkipWithBlock):
            return True  # Suppress special SkipWithBlock exception


with SkipContextManager(skip=True):    
    print('In the with block')  # Won't be called
print('Out of the with block')

As mentioned before by joe, this is a hack that should be avoided:

The method trace() is called when a new local scope is entered, i.e. right when the code in your with block begins. When an exception is raised here it gets caught by exit(). That’s how this hack works. I should add that this is very much a hack and should not be relied upon. The magical sys.settrace() is not actually a part of the language definition, it just happens to be in CPython. Also, debuggers rely on sys.settrace() to do their job, so using it yourself interferes with that. There are many reasons why you shouldn’t use this code. Just FYI.

Answered By: Louis M

Another slightly hacky option makes use of exec. This is handy because it can be modified to do arbitrary things (e.g. memoization of context-blocks):

from contextlib import contextmanager


@contextmanager
def skippable_context_exec(skip):
    SKIP_STRING = 'Skipping Context Exception'
    old_value = skippable_context_exec.is_execed if hasattr(skippable_context_exec, 'is_execed') else False
    skippable_context_exec.is_execed=False
    command = "skippable_context_exec.is_execed=True; "+("raise ValueError('{}')".format(SKIP_STRING) if skip else '')
    try:
        yield command
    except ValueError as err:
        if SKIP_STRING not in str(err):
            raise
    finally:
        assert skippable_context_exec.is_execed, "You never called exec in your context block."
        skippable_context_exec.is_execed = old_value


print('=== Running with skip disabled ===')
with skippable_context_exec(skip=False) as command:
    exec(command)
    print('Entering this block')
print('... Done')

print('=== Running with skip enabled ===')
with skippable_context_exec(skip=True) as command:
    exec(command)
    print('... But this will never be printed')
print('... Done')

Would be nice to have something that gets rid of the exec without weird side effects, so if you can think of a way I’m all ears. The current lead answer to this question appears to do that but has some issues.

Answered By: Peter

Based on @Peter’s answer, here’s a version that uses no string manipulations but should work the same way otherwise:

from contextlib import contextmanager

@contextmanager
def skippable_context(skip):
    skip_error = ValueError("Skipping Context Exception")
    prev_entered = getattr(skippable_context, "entered", False)
    skippable_context.entered = False

    def command():
        skippable_context.entered = True
        if skip:
            raise skip_error

    try:
        yield command
    except ValueError as err:
        if err != skip_error:
            raise
    finally:
        assert skippable_context.entered, "Need to call returned command at least once."
        skippable_context.entered = prev_entered


print("=== Running with skip disabled ===")
with skippable_context(skip=False) as command:
    command()
    print("Entering this block")
print("... Done")

print("=== Running with skip enabled ===")
with skippable_context(skip=True) as command:
    command()
    raise NotImplementedError("... But this will never be printed")
print("... Done")

Answered By: bers

Context managers are not the right construct for this. You’re asking for the body to be executed n times, in this case zero or one. If you look at the general case, n where n >= 0, you end up with a for loop:

def do_squares(n):
  for i in range(n):
    yield i ** 2

for x in do_squares(3):
  print('square: ', x)

for x in do_squares(0):
  print('this does not print')

In your case, which is more special purpose, and doesn’t require binding to the loop variable:

def should_execute(mode=0):
  if mode == 0:
    yield

for _ in should_execute(0):
  print('this prints')

for _ in should_execute(1):
  print('this does not')
Answered By: jstaab
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.