20

Can I have a context manager which occasionally does not yield, and in which case the code within the with statement are simply not executed?

import contextlib

@contextlib.contextmanager
def MayNotYield(to_yield):
  if to_yield:
    yield

with MayNotYield(True):  
  print 'This works.'

with MayNotYield(False):  
  print 'This errors.'

I could ask the user to wrap the with statement with a try-catch, but that is not preferred. I could also do the following but it is ugly too.

import contextlib

@contextlib.contextmanager
def AlwaysYields(to_yield):
  if to_yield:
    yield 1
  else:
    yield 2

with AlwaysYields(True) as result:
  if result == 1:
    print 'This works.'
5
  • 3
    What are you actually trying to achieve? Commented Dec 29, 2015 at 22:05
  • 1
    @jonrsharpe: My use case is that I put initialization and clean up code in a context manager. The user of the context manager runs their code in the body. My context manager also forks a few processes and only one process is left to the user. All the other processes are to be handled by the library itself. Commented Dec 29, 2015 at 22:08
  • 4
    @shaoyl85: That still doesn't say why you wouldn't yield. It looks like you're trying to combine an if statement with a context manager to get around the inability to assign in an if statement, not trying to manage resources. Commented Dec 29, 2015 at 22:18
  • Could you explain this in the question, and give a less abstract example? It may help clarify whether this is an XY problem and get you better answers. Commented Dec 29, 2015 at 23:24
  • @jonrsharpe: Thank you! I believe my problem can have tons of solutions. My current question is just about whether it is possible to skip the yield. And I think the answer is indeed like what Blckknght said, the "with" statement itself does not have an option to skip the body. It is not the problem of the contextlib.contextmanager. I'm good with that. Commented Dec 30, 2015 at 6:06

4 Answers 4

9

Unfortunately, the context manager protocol does not give a context manager a way to say "Don't run the with block" (except raising an exception in __enter__). If you're using a context manager anyway, I think your second approach, which has __enter__ return a value to signal if the block should be run is the best approach. If you don't need a context manager for some other reason, you could just use a simple if statement:

if do_stuff:
    # do the stuff
Sign up to request clarification or add additional context in comments.

1 Comment

With exception catching, there is other clean solution I posted nearby. If to avoid exceptions, seems this is best answer. More recent related discussion (also touching interesting async generators): discuss.python.org/t/… Slightly more clean way to break out of with condition for this answer: stackoverflow.com/a/23665658/10141885
9

Another option would be to just use a regular generator rather than a contextmanager; a plain generator won't have this restriction. But you'll have to use it with a "for" construct rather than using "with":

def MayNotYield(to_yield):
   if to_yield:
      yield

for _ in MayNotYield(True):
   print('This prints.')

for _ in MayNotYield(False):
   print('This does not.')

1 Comment

This doesn't perform cleanup when an exception is raised inside the for-block, so it's not a real replacement for a context manager.
4

Given the goal of having the conditional within the context manager implementation, there is another possibility in cases where it is possible to extract the the contents of the with-block into its own function. You can pass this callable to the context manager and have the context manager return the passed callable or a dummy do-nothing callable from the context manager depending on the boolean. The with-block will always execute, but the action may or may not be invoked.

def do_something():
    print("This works!")

@contextlib.contextmanager
def conditional_on(condition, f):
    if condition:
        # Acquire resources here
        pass
    else:
        # Replace the callable with a do-nothing dummy
        f = lambda x: x
    try:
        yield f
    finally:
        if condition:
            # Release resources here
            pass    

with conditional_on(True, do_something) as f:
    f()  # Prints message

with conditional_on(False, do_something) as f:
    f()  # Does nothing

You will need to tailor this solution depending on which, if any, resources the context manager is managing, and on the required signature of of the callable.

Comments

1

May be so obvious that even possible to miss: also consider an approach only to yield correctly or raise (and never skip yield), with exception handling possibly above with scope.
Skipping yield raises anyway, so it's rather consistent. Also, desire to skip may hint that the whole routine is optional enough to just print any failure and continue.

And it's probably not a performance friendly way, but nested contexts can be readable solution to suppress yield skip:

@contextlib.contextmanager
def optional_context():
    try:
        yield
    except Exception as e:
        if type(e) is RuntimeError and str(e) == "generator didn't yield":
            print("yield skipped, not an error")
        else:
            raise

@contextlib.contextmanager
def context_skips_yield(skip_yeild):
    print("Begin")
    try:
        if not skip_yeild:
            yield
    finally:
        print("End")

with optional_context(), context_skips_yield(skip_yeild=True):
    raise Exception("Test raise")

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.