0

This question is similar to Python: standard function and context manager?, but slightly different.

I have a number of classes, each of which defines a few @contextmanagers:

class MyClass:
    @contextmanager
    def begin_foo(param):
        [some code]
        yield
        [some code]

    [...]

I also have an API module that acts as a facade for the classes. In the API module I have found that I need to "wrap" the inner context managers so that I can use them as such in client code:

@contextmanager
def begin_foo(param):
    with myobj.begin_foo(param):
        yield

My client code looks like this right now:

# Case 1: Some things need to be done in the context of foo
with begin_foo(param):
    [ stuff done in the context of foo ]

# Case 2: Nothing needs to be done in the context of foo --
# just run the entry and exit code
with begin_foo(param):
    pass

Question: Is there a way I can use begin_foo as a standard function in the second case, without the need for the with ... pass construct? i.e. just do this:

# Case 2: Nothing needs to be done in the context of foo --
# just run the entry and exit code
begin_foo(param)

I can change the implementation of begin_foo in the API module and/or in the classes, if needed.

Grodriguez
  • 21,501
  • 10
  • 63
  • 107
  • I'm not sure `begin_foo` should be the context manager in the first place; maybe a function that *returns* a context manager? It's hard to tell without more detail. – chepner Jun 03 '21 at 13:14
  • Your use case is really weird - there are very few situations where it makes sense to just enter and exit a context manager immediately. Maybe the context manager is taking on too many responsibilities, and some of them should be factored out. As chepner said, it's hard to tell without more information. – user2357112 Jun 03 '21 at 13:18
  • There may be more to it than I'm thinking right now but if "nothing needs to be done in the context of 'foo'" then why use the function as both **context manager** and an **init** function. Can't these be separated? – Maicon Mauricio Jun 03 '21 at 13:24
  • @chepner Perhaps. I can change begin_foo either in MyClass or in the api module if needed. The actual requirement is that client code should be able to use begin_foo as a CM or as a regular function, with the same function name. Can you elaborate on your idea ? – Grodriguez Jun 03 '21 at 13:27
  • (1/2) You've said _"I can change the implementation of begin_foo in the API module and/or in the classes, if needed."_ In that case, here's another option but it's a nasty dirty _diiirty_ solution and I **definitely don't recommend** even considering it: Use `async` or multithreading to start a "timer" when `begin_foo()` executes. Then with a [future](https://docs.python.org/3/library/asyncio-future.html#future-object) (or something), check if the `__exit__()` part is called within 0.001 seconds or anything short. – aneroid Jun 03 '21 at 14:13
  • (2/2) If it's not called within that time, then execute the part after `yield`. If it is called before that, then don't re-execute whatever is after `yield`. The point being, this assumes code executed inside the `with` context will take longer than if it was merely called as a function. So this is a flaky solution that may not work as expected when the system is under very heavy load. This also requires _drastically_ changing _all_ your `contextmanager`'ed methods. – aneroid Jun 03 '21 at 14:13

1 Answers1

1

The problem with the default contextmanager decorator for functions is that there's no way to invoke both __enter__ and __exit__ code unless in a with context, as you're aware:

from contextlib import contextmanager

@contextmanager
def foo_bar():
    print('Starting')
    yield
    print('Finishing')
    return False

>>> with foo_bar():
...     print('in between')
...
Starting
in between
Finishing

>>> foo_bar()  # only returns the cm if merely invoked
<contextlib._GeneratorContextManager at 0x517a4f0>

You could create another function to execute both the enter and exit, for any cm - I'm surprised this doesn't already exist:

def in_out(cm, *args, **kwargs):
    print('getting in')
    with cm(*args, **kwargs):
        print("we're in")

>>> in_out(foo_bar)
getting in
Starting
we're in
Finishing

Optionally, re-implement each of the many context managers in your class as their own context manager classes, and use them like in ContextDecorator docs' example, which includes invocation as a cm and as a single direct call:

from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

>>> @mycontext()
... def function():
...     print('The bit in the middle')
...
>>> function()
Starting
The bit in the middle
Finishing

>>> with mycontext():
...     print('The bit in the middle')
...
Starting
The bit in the middle
Finishing
aneroid
  • 12,983
  • 3
  • 36
  • 66
  • Yes but both approaches have the same problem -- different function names depending on whether the function is being used as a CM or not. I need that to be the same function name... – Grodriguez Jun 03 '21 at 13:13
  • @Grodriguez: You're trying to write a function that does different things depending on how the return value is used. That always causes way more problems than it's worth. – user2357112 Jun 03 '21 at 13:21
  • Frankly, I don't see an option better than `in_out`. Because `begin_foo` is already `contextmanager`ed, consider that `begin_foo()` and `with begin_foo()` can't both behave the same way - that requires `begin_foo()` to be aware of its caller, which nothing is, and then behave differently in each case. I suppose some frame-hacking may yield a better result but as it is, in Python `xyz()` will always do what it does regardless of `with xyz()`, `list(xyz())`, `raise xyz()` - the only difference is that it you get an error when the caller doesn't get what it expects (iterable, context mgr, etc.). – aneroid Jun 03 '21 at 13:21
  • @aneroid I guess I am looking for something such as open() which you can use as is (open("filename"), or with a CM: with open("filename") as f: ... – Grodriguez Jun 03 '21 at 13:25
  • 1
    @Grodriguez: This is sounding more and more like an XY problem. (Also, `open` behaves very differently from what you're asking for.) – user2357112 Jun 03 '21 at 13:29
  • @Grodriguez `open()` is a good example of why what you're trying is actually not possible: `with open(...) as ...:` will enter and exit ie close the file. But just `open(...)` only opens the file and doesn't close it unless you call `fp.close()`. And it won't handle errors automatically. Ie, you're only getting the `__enter__` part executed if you do just `open(...)` (And I'm sure `open` is implemented in C-code and isn't bound by Python-function-calling rules/behaviours.). If you really want only the enter part executed, then just put in `foo_bar().__enter__()`. – aneroid Jun 03 '21 at 13:29
  • @Grodriguez You've mentioned in [another comment](https://stackoverflow.com/questions/67821083/how-to-use-a-contextmanager-wrapper-as-a-standard-function/67821920#comment119878937_67822124) that you can't modify the client code `begin_foo` and all the other context managers. Well, `in_out()` would be in userland code and the user can determine if they want to use it as a context manager or as a direct call, using `with` or `in_out`, respectively. But your edit says _"I can change the implementation of begin_foo in the API module and/or in the classes, if needed."_ - so which is it? – aneroid Jun 03 '21 at 13:59
  • @aneroid I never said I cannot modify begin_foo or the CMs. I said I cannot modify "client code" which is what you call userland code in your comment, i.e. the consumer of the API. The client code must see the same function name for both usages. I _can_ modify begin_foo and the CMs. – Grodriguez Jun 03 '21 at 16:45
  • 1
    @Grodriguez Ah, so you're providing the classes which have context managers and the "client" is the user(s) of those classes. I'd still recommend that the easiest way is to provide them with a function that does the `with begin_foo(): pass`. No way around creating an alternative call-option or a func that encapsulates the cm. – aneroid Jun 03 '21 at 17:03
  • 2
    _“I'm surprised this [in_out] doesn't already exist”_ – Probably because `with open_context(): pass` is more explicit and doesn’t hide that there is a context manager involved. And explicit is better than implicit ;) – poke Jun 06 '21 at 10:05
  • @poke You're 100% right. Got carried away by the question. 'with ... pass' is simple enough. (Can't be used in a an expression but that's fine - would make things complicated for no real benefit.) – aneroid Jun 06 '21 at 10:26