Skip to content

Commit f24be35

Browse files
committed
-
1 parent 05a055d commit f24be35

File tree

3 files changed

+111
-2
lines changed

3 files changed

+111
-2
lines changed

source_py3/python_toolbox/context_management/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,4 @@ def do_stuff():
133133
from .blank_context_manager import BlankContextManager
134134
from .reentrant_context_manager import ReentrantContextManager
135135
from .delegating_context_manager import DelegatingContextManager
136-
from .functions import nested
136+
from .functions import nested, idempotentify

source_py3/python_toolbox/context_management/functions.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import sys
1111

1212
from .context_manager_type import ContextManagerType
13+
from .context_manager import ContextManager
1314

1415

1516
@ContextManagerType
@@ -40,4 +41,50 @@ def nested(*managers):
4041
# the right information. Another exception may
4142
# have been raised and caught by an exit method
4243
raise exc[1].with_traceback(exc[2])
43-
44+
45+
46+
def idempotentify(context_manager):
47+
'''
48+
Wrap a context manager so repeated calls to enter and exit will be ignored.
49+
50+
This means that if you call `__enter__` a second time on the context
51+
manager, nothing will happen. The `__enter__` method won't be called and an
52+
exception would not be raised. Same goes for the `__exit__` method, after
53+
calling it once, if you try to call it again it will be a no-op. But now
54+
that you've called `__exit__` you can call `__enter__` and it will really
55+
do the enter action again, and then `__exit__` will be available again,
56+
etc.
57+
58+
This is useful when you have a context manager that you want to put in an
59+
`ExitStack`, but you also possibly want to exit it manually before the
60+
`ExitStack` closes. This way you don't risk an exception by having the
61+
context manager exit twice.
62+
'''
63+
class _IdempotentContextManager:
64+
# Not inheriting from `ContextManager` because this class is internal
65+
# and not used beyond `idempotentify`, so no need for the extra
66+
# baggage.
67+
_entered = False
68+
_enter_value = None
69+
70+
def __init__(self, wrapped_context_manager):
71+
self.__wrapped__ = wrapped_context_manager
72+
73+
74+
def __enter__(self):
75+
if not self._entered:
76+
self._enter_value = self.__wrapped__.__enter__()
77+
self._entered = True
78+
return self._enter_value
79+
80+
81+
def __exit__(self, exc_type, exc_value, exc_traceback):
82+
if self._entered:
83+
exit_value = self.__wrapped__.__exit__(exc_type, exc_value,
84+
exc_traceback)
85+
self._entered = False
86+
return exit_value
87+
88+
return _IdempotentContextManager(context_manager)
89+
90+
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2009-2015 Ram Rachum.
2+
# This program is distributed under the MIT license.
3+
4+
'''Testing module for `context_management.ReentrantContextManager`.'''
5+
6+
import queue as queue_module
7+
8+
from python_toolbox.context_management import idempotentify, ContextManager
9+
from python_toolbox import cute_testing
10+
11+
12+
class SomeContextManager(ContextManager):
13+
x = 0
14+
def manage_context(self):
15+
self.x += 1
16+
try:
17+
yield self
18+
finally:
19+
self.x -= 1
20+
21+
22+
23+
def test_idempotentify():
24+
some_context_manager = SomeContextManager()
25+
assert some_context_manager.x == 0
26+
with some_context_manager:
27+
assert some_context_manager.x == 1
28+
assert some_context_manager.x == 0
29+
30+
some_context_manager.__enter__()
31+
assert some_context_manager.x == 1
32+
some_context_manager.__enter__()
33+
assert some_context_manager.x == 2
34+
some_context_manager.__enter__()
35+
assert some_context_manager.x == 3
36+
some_context_manager.__exit__(None, None, None)
37+
assert some_context_manager.x == 2
38+
some_context_manager.__exit__(None, None, None)
39+
assert some_context_manager.x == 1
40+
some_context_manager.__exit__(None, None, None)
41+
assert some_context_manager.x == 0
42+
with cute_testing.RaiseAssertor():
43+
some_context_manager.__exit__(None, None, None)
44+
with cute_testing.RaiseAssertor():
45+
some_context_manager.__exit__(None, None, None)
46+
47+
idempotent_context_manager = idempotentify(SomeContextManager())
48+
49+
idempotent_context_manager.__enter__()
50+
assert idempotent_context_manager.__wrapped__.x == 1
51+
idempotent_context_manager.__enter__()
52+
assert idempotent_context_manager.__wrapped__.x == 1
53+
idempotent_context_manager.__enter__()
54+
assert idempotent_context_manager.__wrapped__.x == 1
55+
idempotent_context_manager.__exit__(None, None, None)
56+
assert idempotent_context_manager.__wrapped__.x == 0
57+
idempotent_context_manager.__exit__(None, None, None)
58+
assert idempotent_context_manager.__wrapped__.x == 0
59+
idempotent_context_manager.__exit__(None, None, None)
60+
assert idempotent_context_manager.__wrapped__.x == 0
61+
62+

0 commit comments

Comments
 (0)