Issue #1: Add ContextStack.preserve()

This commit is contained in:
Nick Coghlan 2012-01-03 15:05:55 +10:00
parent 7f99236d3b
commit 8e373d228e
4 changed files with 48 additions and 8 deletions

View file

@ -5,6 +5,8 @@ Release History
0.3 (2012-01-XX)
~~~~~~~~~~~~~~~~
* Issue #1: Add ContextStack.preserve() to move all registered callbacks to
a new ContextStack object
* Wrapped callbacks now use functools.wraps to aid in introspection
* Moved version number to a VERSION.txt file (read by both docs and setup.py)
* Added NEWS.rst (and incorporated into documentation)

View file

@ -143,7 +143,7 @@ class closing(object):
# Inspired by discussions on http://bugs.python.org/issue13585
class ContextStack(object):
"""Context manager for programmatic management of resource cleanup
"""Context manager for dynamic management of a stack of exit callbacks
For example:
@ -156,6 +156,13 @@ class ContextStack(object):
"""
def __init__(self):
self._callbacks = deque()
def preserve(self):
"""Preserve the context stack by transferring it to a new instance"""
new_stack = type(self)()
new_stack._callbacks = self._callbacks
self._callbacks = deque()
return new_stack
def register_exit(self, callback):
"""Registers a callback with the standard __exit__ method signature

View file

@ -207,14 +207,14 @@ API Reference
# the with statement, even if attempts to open files later
# in the list throw an exception
Each instance maintains a stack of registered callbacks (usually context
manager exit methods) that are called in reverse order when the instance
is closed (either explicitly or implicitly at the end of a ``with``
statement).
Each instance maintains a stack of registered callbacks that are called in
reverse order when the instance is closed (either explicitly or implicitly
at the end of a ``with`` statement). Note that callbacks are *not* invoked
implicitly when the context stack instance is garbage collected.
Since registered callbacks are invoked in the reverse order of
registration, this ends up behaving as if multiple nested ``with``
statements had been used with the registered set of resources. This even
statements had been used with the registered set of callbacks. This even
extends to exception handling - if an inner callback suppresses or replaces
an exception, then outer callbacks will be passed arguments based on that
updated state.
@ -245,9 +245,28 @@ API Reference
Unlike the other methods, callbacks added this way cannot suppress
exceptions (as they are never passed the exception details).
.. method:: preserve()
Transfers the callback stack to a fresh instance and returns it. No
callbacks are invoked by this operation - instead, they will now be
invoked when the new stack is closed (either explicitly or implicitly).
For example, a group of files can be opened as an "all or nothing"
operation as follows::
with ContextStack() as stack:
files = [stack.enter_context(open(fname)) for fname in filenames]
close_files = stack.preserve().close
# If opening any file fails, all previously opened files will be
# closed automatically. If all files are opened successfully,
# they will remain open even after the with statement ends.
# close_files() can then be invoked explicitly to close them all
.. versionadded:: 0.3
.. method:: close()
Immediately unwinds the context stack, invoking callbacks in the
Immediately unwinds the callback stack, invoking callbacks in the
reverse order of registration. For any context managers and exit
callbacks registered, the arguments passed in will indicate that no
exception occurred.

View file

@ -370,14 +370,26 @@ class TestContextStack(unittest.TestCase):
def test_close(self):
result = []
with ContextStack() as stack:
@stack.register # Registered first => cleaned up last
@stack.register
def _exit():
result.append(1)
stack.close()
result.append(2)
self.assertEqual(result, [1, 2])
def test_preserve(self):
result = []
with ContextStack() as stack:
@stack.register
def _exit():
result.append(3)
new_stack = stack.preserve()
result.append(1)
result.append(2)
new_stack.close()
self.assertEqual(result, [1, 2, 3])
if __name__ == "__main__":
import unittest
unittest.main(__name__)