diff --git a/NEWS.rst b/NEWS.rst index dbb3c98..7ff5ef6 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -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) diff --git a/contextlib2.py b/contextlib2.py index 65dd434..6dbd7ce 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -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 diff --git a/docs/index.rst b/docs/index.rst index dc89fe6..91daf82 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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. diff --git a/test_contextlib2.py b/test_contextlib2.py index 7a3150b..db1caa2 100755 --- a/test_contextlib2.py +++ b/test_contextlib2.py @@ -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__)