1

I have the following code:

import unittest
from contextlib import contextmanager


def outer(with_context=False):
    def inner():
        # Some code here

        if with_context:
            with unittest.TestCase().assertRaises(KeyError):
                yield
        else:
            print("No context")

        # Some more code here

    if with_context:
        return contextmanager(inner)
    else:
        return inner


no_context_func = outer()
no_context_func()  # "No context" does not get printed

my_dict = {"valid_key": 123}

context_func = outer(with_context=True)
with context_func():
    my_dict["valid_key"]  # Works fine

Why is "No context" not printed to the console? In fact, it seems that the function call to no_context_func is skipped completely, as I can not set a break point at the print("No context") line. Removing the content of the if with_context: branch prints "No context" but why does the yield influence the program execution although it is not in a branch that gets executed?

Edit: I have added comments to the code example showing that there is more code before and after the if/else in the inner function. I eventually want to use outer as a decorator that I can parameterize and that I can use to decorate regular functions and context managers, like assertRaises. My question is whether there is a way of using the same decorator for both cases or if I will have to use separate decorators, one for regular functions and one for context managers. That unfortunately means that I have to duplicate the content of the inner function.

Mr-Pepe
  • 77
  • 6
  • `no_context_func()` is *defined* in the `outer()` but only executed if a truthy argument is passed to `outer()` when it's executed (regardless of the assigning the function to a variable with a different name. – martineau Jun 21 '22 at 10:35
  • @martineau That doesn't sound right. What do you mean with "only executed if"? I mean, it does get executed (and returns an iterator). – Kelly Bundy Jun 21 '22 at 11:11
  • @KellyBundy: I meant just defining a function doesn't execute the code in it — it must be *called* from somewhere afterwards for that to happen. – martineau Jun 21 '22 at 11:24
  • @martineau Their `no_context_func()` does call it. Unless you meant the resulting iterator isn't called? But that's not even callable... – Kelly Bundy Jun 21 '22 at 12:00
  • @KellyBundy: The `no_context_func()` aka the `outer()` function doesn't necessarily call it. – martineau Jun 21 '22 at 15:29
  • @martineau `no_context_func` isn't `outer` but the result of calling `outer`, i.e., it's `inner`. And the `()` at the end of `no_context_func()` means calling it. So they **do** call it. – Kelly Bundy Jun 21 '22 at 15:50

2 Answers2

1

Your inner is a generator function and no_context_func() thus returns a generator iterator. Whose code is only executed when you iterate it. So you get your desired effect (printing "No context") if you for example replace return inner with return lambda: next(inner(), None).

Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65
  • That's the correct answer, thank you very much! The only drawback is that the function signature of `inner` gets lost. – Mr-Pepe Jun 22 '22 at 08:05
  • What do you mean with signature? – Kelly Bundy Jun 22 '22 at 08:09
  • My `inner` function is actually a wrapt decorator that I use to wrap other functions. Autocompletion for the `with_context` case works fine, conserving the signature of the wrapped function. However, it's not preserved for the cases where the lambda is returned. Nonetheless, the docstring is still preserved which lists the arguments and their types so I'm happy with that. Thanks a lot! – Mr-Pepe Jun 23 '22 at 07:50
  • Ah, ok. Maybe the functools wrap stuff could help, I'm not familiar with that. – Kelly Bundy Jun 23 '22 at 08:02
0

Adding yield to a function turns it into generator, which work quite differently than standard functions. Consider the following basic generator example:

def mygenerator():
    print('First item')
    yield 10

    print('Second item')
    yield 20

    print('Last item')
    yield 30

a = mygenerator()
print("calling next")
print(next(a))

As you can see, print('First item') only happened after next was called for the first time, even though with standard function it would happen at a = mygenerator() call already.

My question is, what is the point of your yield statement here?

matszwecja
  • 6,357
  • 2
  • 10
  • 17
  • *"As you can see"* - I can't. Did you intend to show the output but then forgot? – Kelly Bundy Jun 21 '22 at 10:03
  • The point of the `yield` is to satisfy `contextmanager`. – Kelly Bundy Jun 21 '22 at 10:06
  • Looks like a I can not achieve my goal. According to the accepted answer [here](https://stackoverflow.com/questions/47683745/python-functions-as-optional-generators): "The reason is that occurrence of yield anywhere in the function makes it a generator function. There's no way to have a function that decides at call time whether it's a generator function or not. Instead, you have to have a non-generator function that decides at runtime whether to return a generator or something else." – Mr-Pepe Jun 21 '22 at 10:29
  • The problem is that I have code before and after the if/else block and now I have to duplicate stuff to a degree. – Mr-Pepe Jun 21 '22 at 10:30
  • @Mr-Pepe In the no-context case you could just consume the generator iterator, that'd execute its code. – Kelly Bundy Jun 21 '22 at 10:59
  • @KellyBundy I tried and did not succeed. I have updated my question to explain a bit more what I am trying to achieve. Let me know if you have any ideas. – Mr-Pepe Jun 21 '22 at 15:17