RichL
RichL

Reputation: 83

Context Manager Reverse

I'm at an intermediate level with Python, and I've recently been playing around with Python context managers. I wanted to invert the order in which the enter and exit are run. So I wrote this context manager:

class ReversibleContextManager(object):

    def __enter__(self, *args):
        print('d')
        return self

    def __exit__(self, *args):
        print('g')
        return self

    def __invert__(self):
        self.__enter__, self.__exit__ = self.__exit__, self.__enter__
        return self

It works fine forwards:

with ContextManager():
    print('o')

d
o
g

But in reverse, we still get:

with ~ContextManager():
    print('a')

d
o
g

If we call the enter and exit functions explicitly, as expected, we get:

with ReversibleContextManager() as c:
    c.__enter__()
    print('o')
    c.__exit__()

d
d
o
g
g

BUT the order IS reversed for the instance's method!

with ~ReversibleContextManager() as c:
    c.__enter__()
    print('o')
    c.__exit__()

d
g
o
d
g

So it looks like the with statement calls using the method bound to the Class rather than the instance (is this the right terminology?). Is this expected?

i.e.

what is called is:

c = ReversibleContextManager()
c.__invert__()
ReversibleContextManager.__enter__(c)
...in context...
ReversibleContextManager.__exit__(c)

Rather than what I expect:

c = ReversibleContextManager()
c.__invert__()
c.__enter__()
...in context...
c.__exit__()

Upvotes: 2

Views: 376

Answers (4)

Mike Müller
Mike Müller

Reputation: 85442

Another way:

class ReversibleContextManager(object):

    inverted = False

    def __init__(self):
        if self.__class__.inverted:
            self.__invert__()
            self.__class__.inverted = False

    def __enter__(self, *args):
        print('d')
        return self

    def __exit__(self, *args):
        print('g')
        return self

    def __invert__(self):
        self.__class__.inverted = True
        self.__class__.__enter__, self.__class__.__exit__ = self.__exit__, self.__enter__
        return self

Upvotes: 0

Mike Müller
Mike Müller

Reputation: 85442

A more verbose but pretty explicit way is to use two helper functions and just switch which one comes at enter and exit by a flag reverse:

class ReversibleContextManager(object):

    def __init__(self, reverse=False):
        self.reverse = reverse

    def _enter(self, *args):
        print('d')
        return self

    def _exit(self, *args):
        print('g')
        return self

    def __enter__(self, *args):
        if self.reverse:
            return self._exit(*args)
        return self._enter(*args)

    def __exit__(self, *args):
        if self.reverse:
            return self._enter(*args)
        return self._exit(*args)

    def __invert__(self):
        self.reverse = True
        return self


>>> with ReversibleContextManager() as r:
    print('o')
d
o
g

>>> with ~ReversibleContextManager() as r:
    print('o')
g
o
d

>>> with ReversibleContextManager(reverse=True) as r:
    print('o')
g
o
d

Upvotes: 1

Adam Smith
Adam Smith

Reputation: 54173

As a workaround, you could create your reversed class inside the __invert__ function and return an instance of the new class.

class ReversibleContextManager(object):
    def __enter__(self, *args):
        print('enter')
        return self
    def __exit__(self, *args):
        print('exit')
        return self
    def __invert__(self):
        new = type("ReversibleContextManager",
                   (object,),
                   {'__enter__': self.__exit__,
                    '__exit__': self.__enter__})
        return new()

>>> with ReversibleContextManager() as f:
...     print("normal")
enter
normal
exit

>>> with ~ReversibleContextManager() as f:
...     print("reversed")
exit
reversed
enter

Upvotes: 2

user2357112
user2357112

Reputation: 280426

So it looks like the with statement calls using the method bound to the Class rather than the instance (is this the right terminology?). Is this expected?

Absolutely. That's how Python generally looks up special methods. This is mostly so in cases like if you have a class Foo that implements __str__, print Foo calls type(Foo).__str__ instead of Foo.__str__.

Upvotes: 2

Related Questions