Reputation: 83
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
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
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
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
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