Skipping execution of -with- block
Asked Answered
H

9

47

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...'
Hydrogeology answered 26/9, 2012 at 3:28 Comment(5)
I found this, but I don't quite know how to make sense of it, nor how to implement it. python.org/dev/peps/pep-0377 Are there other more elegant ways?Hydrogeology
The fact that it's a PEP (and the discussion of the semantic changes) suggests that it cannot be implemented without resorting to changing the interpreter's behaviour.Klina
obsessed with neatness ? :) with A(), B(): where B's enter can raise something seems fine to me.Scurlock
I also needed that feature. But the only proposal to do that in a non-hacky way required adding a new system exception to python. That proposal was rejected because it was deemed that the cost of additional complexity in the language isn't worth the benefit.Amora
Side warning: users of a context generally don't expect their block to be skipped (even conditionally). Skipping the with block runs the risk of violating developers' expectations, leading to hard-to-debug scenarios downstream.Aged
U
26

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 ... '
Unfolded answered 26/9, 2012 at 4:56 Comment(7)
This is what I was looking for. tytytytytyty.Hydrogeology
I see, so it triggers a TypeError somehow which is caught and suppressed by __exit__() method. Interesting work around!Hydrogeology
I added an if loop in method __exit__() to check for type and value so that only the exception raised by the hack is suppressed.Hydrogeology
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.Unfolded
Thanks Joe for this trick. I'm trying to use this to make an extremely hacky quick-and-dirty caching system. I'm getting a strange result. I add a return 5 to the end of your __enter__ block, and have: for i in range(2): c=0 print('=== Iter {} ==='.format(i)) with My_Context(mode=1) as c: print('Executing block of code ...') print('c={}'.format(c)) Strangely, I get: ` === Iter 0 === Met block-skipping criterion ... Exiting context ... c=5 === Iter 1 === Met block-skipping criterion ... Exiting context ... c=0 ` Why 0 iter 1?Mahala
Sorry if that's unreadable. I created a new question for it: #57618244Mahala
The call to inspect.currentframe()` will raise a TypeError: currentframe() takes 0 positional arguments but 1 was given — so the code in this answer doesn't work (in Python 3.x anyway). See Context Manager Hackery.Antiphonal
W
34

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".

Weatherboard answered 26/9, 2012 at 3:54 Comment(1)
OK, this explains a lot. I checked out withhacks. Think it is beyond me at this point.... Not sure how I can use the code to perform the skip yet but there are definitely interesting snippets of codes I could use. (Update: RUBY STYLE BLOCKS? I see, hahahah. THat's insane indeed) Otherwise, I will really need to think of another approach then. Thank you!Hydrogeology
U
26

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 ... '
Unfolded answered 26/9, 2012 at 4:56 Comment(7)
This is what I was looking for. tytytytytyty.Hydrogeology
I see, so it triggers a TypeError somehow which is caught and suppressed by __exit__() method. Interesting work around!Hydrogeology
I added an if loop in method __exit__() to check for type and value so that only the exception raised by the hack is suppressed.Hydrogeology
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.Unfolded
Thanks Joe for this trick. I'm trying to use this to make an extremely hacky quick-and-dirty caching system. I'm getting a strange result. I add a return 5 to the end of your __enter__ block, and have: for i in range(2): c=0 print('=== Iter {} ==='.format(i)) with My_Context(mode=1) as c: print('Executing block of code ...') print('c={}'.format(c)) Strangely, I get: ` === Iter 0 === Met block-skipping criterion ... Exiting context ... c=5 === Iter 1 === Met block-skipping criterion ... Exiting context ... c=0 ` Why 0 iter 1?Mahala
Sorry if that's unreadable. I created a new question for it: #57618244Mahala
The call to inspect.currentframe()` will raise a TypeError: currentframe() takes 0 positional arguments but 1 was given — so the code in this answer doesn't work (in Python 3.x anyway). See Context Manager Hackery.Antiphonal
S
15

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.

Supersonic answered 19/2, 2019 at 11:40 Comment(2)
This is terrible! For some reason, with SkipContextManager(skip=True) as x: works (that is, you can do print(x) after the context). But with SkipContextManager(skip=True) \ as y: (with an explicit line break) does not: NameError: name 'y' is not defined! And this is due to frame.f_trace = self.trace...Lysimachus
See pyfiddle.io/fiddle/f4419a57-c0b0-40f1-97c2-0d83750d97e3 for code.Lysimachus
L
8

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")

Lysimachus answered 14/1, 2020 at 15:19 Comment(2)
(I am not sure skippable_context.entered is required and, if so, actually working, but I kept these variables around nonetheless.)Lysimachus
This is a clever answer that doesn't resort to frame rewriting .. very nice!Annoyance
C
8

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')
Codel answered 27/4, 2020 at 17:52 Comment(2)
This is a completely unintuitive (to me) approach to this problem that has yielded (hah!) a beautifully elegant solution to a problem I had in my program. Thanks for this.Flan
at this point you might as well just use if: if should_execute(1): ...Germano
K
3

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.

Klina answered 26/9, 2012 at 3:50 Comment(1)
The flag works, but I would like to keep all the checks within the context manager and keep the block of codes clean. If its impossible, I may have to find another way besides with. Thank you so much anw!Hydrogeology
M
1

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.

Mahala answered 31/10, 2019 at 16:7 Comment(1)
For one, you could yield lambda: exec(command) and run command() in the body.Lysimachus
S
0

Don't resort to stack manipulation hacks. Instead I think the best solution is just to use a callback instead, and then not call it:

def my_thing(mode, cb):
    if self.mode==1:
        print 'Exiting...'
        return
    cb()
    print 'Exiting...'
...

def with_block():
   ...

my_thing(mode=1, cb=with_block)
Shelia answered 11/12, 2023 at 11:39 Comment(0)
B
-1

It is possible to skip the context manager body with help of double context manager. As you know, contexts managers can be nested, and one thing you could do is to use one outer context for termination (whose __exit__ will be called) and one for actual work you want the context to do (whose __enter__ will be invoked).

The solution works by entering the outer context, in whose __enter__ method we invoke the inner context that does whatever you need, and if you need to terminate the context manager, you can raise an exception at the end of the inner context __enter__. Exceptions in this method are not caught (otherwise you could easily raise one and skip the body), but they propagate to the outer context. In there, it is caught and redirected to the outer context __exit__ method that acts as a goto label. Finally, to achieve this behaviour, you could wrap it around a single manager.

from contextlib import ExitStack

class WorkContext:
    def __enter__(self):
        # Do whatever work or validation you need
        raise Exception() # This line terminates the context and skips its body.

class SkipableContex:
    def __exit__(self):
        #Do whatever cleanup if needed.
        pass

class SkipBody:
    def __enter__(self):
        with ExitStack() as stack:
            stack.enter_context(SkippableContext)
            stack.enter_context(WorkContext)

Batrachian answered 23/7, 2023 at 12:48 Comment(1)
As is, this code doesn't run. After tinkering with it for a while, I don't think this approach provides what the question is asking for, which is a context manager which aborts, while neither running its block nor propagating an exception.Leyes

© 2022 - 2024 — McMap. All rights reserved.