4

I have a pair of python functions that currently flip a global variable between two values. I would like to turn them into context managers so I can use them as with blocks, setting the variable inside the block, but restoring it after. Here's the desired behaviour:

>>> MODE
'user'
>>> mode_sudo()  # Sets MODE to 'sudo'...
>>> MODE
'sudo'
>>> mode_user()  # Sets MODE to 'user'...
>>> MODE
'user'
>>> with mode_sudo():
...    print MODE
'sudo'
>>> MODE
'user'

Is such a chimera possible?

UPDATE: Just for clarity, here's the context-manager-only implementation:

from contextlib import contextmanager

@contextmanager
def mode_sudo():
    global MODE
    old_mode = MODE
    MODE = 'sudo'
    yield
    MODE = old_mode

@contextmanager
def mode_user():
    global MODE
    old_mode = MODE
    MODE = 'user'
    yield
    MODE = old_mode

Calling these w/o a with keyword returns a generator. Is there a way to get the mode-flipping behavior with both the plain-vanilla function call and the chocolate context manager?

smci
  • 32,567
  • 20
  • 113
  • 146
David Eyk
  • 12,171
  • 11
  • 63
  • 103

2 Answers2

8

Do it like this:

class mod_user:

    def __init__(self):
        global MODE
        self._old_mode = MODE
        MODE = "user"

    def __enter__(self):
        pass

    def __exit__(self, *args, **kws):
        global MODE
        MODE = self._old_mode

MODE = "sudo"

with mod_user():
    print MODE  # print : user.

print MODE  # print: sudo.

mod_user()
print MODE   # print: user.
mouad
  • 67,571
  • 18
  • 114
  • 106
  • Precisely what I was looking for. And no dependency on contextlib. That's nice. :) – David Eyk Aug 08 '11 at 21:27
  • 1
    Devious but nice, I've never thought to use a constructor to fake a callable. – Evpok Aug 08 '11 at 21:36
  • 2
    Probably not a good thing to do all the time, but in this case, the effect is definitely worth it. – David Eyk Aug 08 '11 at 21:43
  • 1
    As to naming the context-manager class `mod_user` instead of `ModUser`, there doesn't seem to be a strongly-enforced [naming convention for context-manager classes](https://stackoverflow.com/questions/19948457/naming-convention-for-context-classes-with-blocks). But it seems more Pythonic to write: `with ModUser() as mod_user: ...` – smci Apr 11 '18 at 22:06
2

Easy way:

from contextlib import contextmanager
@contextmanager
def mode_user():
    global MODE
    old_mode = MODE
    MODE = "user"
    yield
    MODE = old_mode

idem for mode_sudo(). See the doc for more details. It is actually a shortcut for the whole "definine a class that implements __enter__ and __exit__"-thing.

Evpok
  • 4,273
  • 3
  • 34
  • 46
  • Whoops, I just posted an edit that looks almost exactly like your code. :) But this answers only half the question. If you just call this implementation w/o a with statement, you'll get a generator. – David Eyk Aug 08 '11 at 21:13
  • The point is when you do `with mode_user()` it calls the `__call__()` method of `mode_user`, exactly like if you simply call `mode_user()` outside the `with` clause. I can't think of a way to discriminate whether you're in a `with` clause. – Evpok Aug 08 '11 at 21:28
  • We were both boxed in by `contextlib` and its generator shortcut. @mouad's pure-protocol implementation gets around that: `__exit__` never gets called except in a with statement. – David Eyk Aug 08 '11 at 21:31
  • @David I don't see so - if I try the said code above and call mode_user(), I get a which is not a generator, it is even not iterable. If you don't like this behaviour, you can, of course, craft your own class like above. – glglgl Aug 09 '11 at 07:37
  • 1
    @glglgl Well, looks like you're right--it's not iterable. However, the main point of my objection still stands: nothing happens until `__enter__` is invoked, making it unhelpful for this particular use case. – David Eyk Aug 09 '11 at 20:39
  • Aahh, ok. Now at a second glance, I see the difference: the mode switch happens in `__init__()`, not in `__enter__()` here. Then it is clear. Thanks for pointing out! – glglgl Aug 10 '11 at 04:43