Skip to content

Commit e41f5da

Browse files
committed
contextlib: re-instate ExitStack from CPython 3.4
1 parent 1260289 commit e41f5da

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

contextlib/contextlib.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
99
"""
1010

11+
import sys
12+
from collections import deque
1113
from ucontextlib import *
1214

1315

@@ -64,3 +66,134 @@ def __exit__(self, exctype, excinst, exctb):
6466
#
6567
# See http://bugs.python.org/issue12029 for more details
6668
return exctype is not None and issubclass(exctype, self._exceptions)
69+
70+
# Inspired by discussions on http://bugs.python.org/issue13585
71+
class ExitStack(object):
72+
"""Context manager for dynamic management of a stack of exit callbacks
73+
74+
For example:
75+
76+
with ExitStack() as stack:
77+
files = [stack.enter_context(open(fname)) for fname in filenames]
78+
# All opened files will automatically be closed at the end of
79+
# the with statement, even if attempts to open files later
80+
# in the list raise an exception
81+
82+
"""
83+
def __init__(self):
84+
self._exit_callbacks = deque()
85+
86+
def pop_all(self):
87+
"""Preserve the context stack by transferring it to a new instance"""
88+
new_stack = type(self)()
89+
new_stack._exit_callbacks = self._exit_callbacks
90+
self._exit_callbacks = deque()
91+
return new_stack
92+
93+
def _push_cm_exit(self, cm, cm_exit):
94+
"""Helper to correctly register callbacks to __exit__ methods"""
95+
def _exit_wrapper(*exc_details):
96+
return cm_exit(cm, *exc_details)
97+
_exit_wrapper.__self__ = cm
98+
self.push(_exit_wrapper)
99+
100+
def push(self, exit):
101+
"""Registers a callback with the standard __exit__ method signature
102+
103+
Can suppress exceptions the same way __exit__ methods can.
104+
105+
Also accepts any object with an __exit__ method (registering a call
106+
to the method instead of the object itself)
107+
"""
108+
# We use an unbound method rather than a bound method to follow
109+
# the standard lookup behaviour for special methods
110+
_cb_type = type(exit)
111+
try:
112+
exit_method = _cb_type.__exit__
113+
except AttributeError:
114+
# Not a context manager, so assume its a callable
115+
self._exit_callbacks.append(exit)
116+
else:
117+
self._push_cm_exit(exit, exit_method)
118+
return exit # Allow use as a decorator
119+
120+
def callback(self, callback, *args, **kwds):
121+
"""Registers an arbitrary callback and arguments.
122+
123+
Cannot suppress exceptions.
124+
"""
125+
def _exit_wrapper(exc_type, exc, tb):
126+
callback(*args, **kwds)
127+
# We changed the signature, so using @wraps is not appropriate, but
128+
# setting __wrapped__ may still help with introspection
129+
_exit_wrapper.__wrapped__ = callback
130+
self.push(_exit_wrapper)
131+
return callback # Allow use as a decorator
132+
133+
def enter_context(self, cm):
134+
"""Enters the supplied context manager
135+
136+
If successful, also pushes its __exit__ method as a callback and
137+
returns the result of the __enter__ method.
138+
"""
139+
# We look up the special methods on the type to match the with statement
140+
_cm_type = type(cm)
141+
_exit = _cm_type.__exit__
142+
result = _cm_type.__enter__(cm)
143+
self._push_cm_exit(cm, _exit)
144+
return result
145+
146+
def close(self):
147+
"""Immediately unwind the context stack"""
148+
self.__exit__(None, None, None)
149+
150+
def __enter__(self):
151+
return self
152+
153+
def __exit__(self, *exc_details):
154+
received_exc = exc_details[0] is not None
155+
156+
# We manipulate the exception state so it behaves as though
157+
# we were actually nesting multiple with statements
158+
frame_exc = sys.exc_info()[1]
159+
def _fix_exception_context(new_exc, old_exc):
160+
# Context may not be correct, so find the end of the chain
161+
while 1:
162+
exc_context = new_exc.__context__
163+
if exc_context is old_exc:
164+
# Context is already set correctly (see issue 20317)
165+
return
166+
if exc_context is None or exc_context is frame_exc:
167+
break
168+
new_exc = exc_context
169+
# Change the end of the chain to point to the exception
170+
# we expect it to reference
171+
new_exc.__context__ = old_exc
172+
173+
# Callbacks are invoked in LIFO order to match the behaviour of
174+
# nested context managers
175+
suppressed_exc = False
176+
pending_raise = False
177+
while self._exit_callbacks:
178+
cb = self._exit_callbacks.pop()
179+
try:
180+
if cb(*exc_details):
181+
suppressed_exc = True
182+
pending_raise = False
183+
exc_details = (None, None, None)
184+
except:
185+
new_exc_details = sys.exc_info()
186+
# simulate the stack of exceptions by setting the context
187+
_fix_exception_context(new_exc_details[1], exc_details[1])
188+
pending_raise = True
189+
exc_details = new_exc_details
190+
if pending_raise:
191+
try:
192+
# bare "raise exc_details[1]" replaces our carefully
193+
# set-up context
194+
fixed_ctx = exc_details[1].__context__
195+
raise exc_details[1]
196+
except BaseException:
197+
exc_details[1].__context__ = fixed_ctx
198+
raise
199+
return received_exc and suppressed_exc

0 commit comments

Comments
 (0)