From 4e624974a659ef2ab3f22a7bc96d0a87e2b02616 Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Thu, 15 Dec 2011 21:07:03 +1000 Subject: [PATCH] Rename CleanupManager to ContextStack and fix usage of the test module as __main__ --- contextlib2.py | 37 +++++++++++++++---------- docs/index.rst | 67 ++++++++++++++++++++++++++++++--------------- setup.py | 2 +- test_contextlib2.py | 43 +++++++++++++++-------------- 4 files changed, 90 insertions(+), 59 deletions(-) mode change 100644 => 100755 test_contextlib2.py diff --git a/contextlib2.py b/contextlib2.py index f9c9d66..f99bbee 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -4,7 +4,7 @@ import sys from collections import deque from functools import wraps -__all__ = ["contextmanager", "closing", "ContextDecorator", "CleanupManager"] +__all__ = ["contextmanager", "closing", "ContextDecorator", "ContextStack"] class ContextDecorator(object): @@ -141,13 +141,13 @@ class closing(object): self.thing.close() -class CleanupManager(object): +class ContextStack(object): """Context for programmatic management of resource cleanup For example: - with CleanupManager() as cmgr: - files = [cmgr.enter_context(fname) for fname in filenames] + with ContextStack() as stack: + files = [stack.enter_context(fname) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list throw an exception @@ -156,22 +156,29 @@ class CleanupManager(object): def __init__(self): self._callbacks = deque() - def register_exit(self, exit): - """Accepts callbacks with the same signature as context manager __exit__ methods + def register_exit(self, callback): + """Registers a callback with the standard __exit__ method signature - Can also suppress exceptions the same way __exit__ methods can. + Can suppress exceptions the same way __exit__ methods can. """ - self._callbacks.append(exit) - return exit # Allow use as a decorator + self._callbacks.append(callback) + return callback # Allow use as a decorator - def register(self, _cb, *args, **kwds): - """Accepts arbitrary callbacks and arguments. Cannot suppress exceptions.""" + def register(self, callback, *args, **kwds): + """Registers an arbitrary callback and arguments. + + Cannot suppress exceptions. + """ def _wrapper(exc_type, exc, tb): - _cb(*args, **kwds) + callback(*args, **kwds) return self.register_exit(_wrapper) def enter_context(self, cm): - """Accepts and automatically enters other context managers""" + """Enters the supplied context manager + + If successful, also registers its __exit__ method as a callback and + returns the result of the __enter__ method. + """ # We look up the special methods on the type to match the with statement _cm_type = type(cm) _exit = _cm_type.__exit__ @@ -182,7 +189,7 @@ class CleanupManager(object): return result def close(self): - """Immediately cleanup all registered resources""" + """Immediately unwind the context stack""" self.__exit__(None, None, None) def __enter__(self): @@ -197,7 +204,7 @@ class CleanupManager(object): # inner one throws an exception def _invoke_next_callback(exc_details): # Callbacks are removed from the list in FIFO order - # but the recursion means they're *invoked* in LIFO order + # but the recursion means they're invoked in LIFO order cb = self._callbacks.popleft() if not self._callbacks: # Innermost callback is invoked directly diff --git a/docs/index.rst b/docs/index.rst index 6fd7427..5928a30 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -188,43 +188,66 @@ API Reference This may involve keeping a copy of the original arguments used to first initialise the context manager. -.. class:: CleanupManager() + +.. class:: ContextStack() A context manager that is designed to make it easy to programmatically - combine other context managers and cleanup functions, that are either - optional or driven by input data. + combine other context managers and cleanup functions, especially those + that are optional or otherwise driven by input data. For example, a set of files may easily be handled in a single with statement as follows:: - with CleanupManager() as cmgr: - files = [cmgr.enter_context(fname) for fname in filenames] + with ContextStack() as stack: + files = [stack.enter_context(fname) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list throw an exception - .. method:: register_exit(exit): + 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). - Accepts callbacks with the same signature as context manager - :meth:`__exit__` methods - - By returing true values, these callbacks can suppress exceptions the - same way context manager :meth:`__exit__` methods can. - - .. method:: register(_cb, *args, **kwds): - - Accepts arbitrary callbacks and arguments. These callbacks cannot - suppress exceptions. + 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 + extends to exception handling - if an inner callback suppresses or replaces + an exception, then outer callbacks will be passed arguments based on that + that updated state. .. method:: enter_context(cm): - Accepts and automatically enters other context managers. These - context managers may suppress exceptions just as they normally would. + Enters a new context manager and adds its :meth:`__exit__` method to + the callback stack. The return value is the result of the context + manager's own :meth:`__enter__` method. + + These context managers may suppress exceptions just as they normally + would if used directly as part of a ``with`` statement. + + .. method:: register_exit(callback): + + Directly accepts a callback with the same signature as a + context manager's :meth:`__exit__` method and adds it to the callback + stack. + + By returning true values, these callbacks can suppress exceptions the + same way context manager :meth:`__exit__` methods can. + + .. method:: push_callback(callback, *args, **kwds): + + Accepts an arbitrary callback function and arguments and adds it to + the callback stack. + + Unlike the other methods, callbacks added this way cannot suppress + exceptions (as they are never passed the exception details). .. method:: close() - Immediately cleans up all registered resources, resetting the manager - to its initial state in the process. + Immediately unwinds the context 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. Obtaining the Module @@ -241,7 +264,7 @@ PyPI page`_. There are no operating system or distribution specific versions of this module - it is a pure Python module that should work on all platforms. -Supported Python versions are 2.7 and 3.2+. +Supported Python versions are currently 2.7 and 3.2+. .. _Python Package Index: http://pypi.python.org .. _pip: http://www.pip-installer.org @@ -251,7 +274,7 @@ Supported Python versions are 2.7 and 3.2+. Development and Support ----------------------- -WalkDir is developed and maintained on BitBucket_. Problems and suggested +contextlib2 is developed and maintained on BitBucket_. Problems and suggested improvements can be posted to the `issue tracker`_. .. _BitBucket: https://bitbucket.org/ncoghlan/contextlib2/overview diff --git a/setup.py b/setup.py index 24962f9..5c9d52f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from distutils.core import setup setup( name='contextlib2', - version='0.1', + version='0.2', py_modules=['contextlib2'], license='PSF License', description='Backports and enhancements for the contextlib module', diff --git a/test_contextlib2.py b/test_contextlib2.py old mode 100644 new mode 100755 index 64f8086..10f146c --- a/test_contextlib2.py +++ b/test_contextlib2.py @@ -163,7 +163,7 @@ class TestContextDecorator(unittest.TestCase): raise NameError('foo') self.assertIsNotNone(context.exc) self.assertIs(context.exc[0], NameError) - self.assertIn('foo', context.exc[1]) + self.assertIn('foo', str(context.exc[1])) context = mycontext() context.catch = True @@ -197,7 +197,7 @@ class TestContextDecorator(unittest.TestCase): test() self.assertIsNotNone(context.exc) self.assertIs(context.exc[0], NameError) - self.assertIn('foo', context.exc[1]) + self.assertIn('foo', str(context.exc[1])) def test_decorating_method(self): @@ -299,10 +299,10 @@ class TestContextDecorator(unittest.TestCase): self.assertEqual(state, [1, 'something else', 999]) -class TestCleanupManager(unittest.TestCase): +class TestContextStack(unittest.TestCase): def test_no_resources(self): - with CleanupManager(): + with ContextStack(): pass def test_register(self): @@ -317,16 +317,16 @@ class TestCleanupManager(unittest.TestCase): result = [] def _exit(*args, **kwds): result.append((args, kwds)) - with CleanupManager() as cmgr: + with ContextStack() as stack: for args, kwds in reversed(expected): if args and kwds: - cmgr.register(_exit, *args, **kwds) + stack.register(_exit, *args, **kwds) elif args: - cmgr.register(_exit, *args) + stack.register(_exit, *args) elif kwds: - cmgr.register(_exit, **kwds) + stack.register(_exit, **kwds) else: - cmgr.register(_exit) + stack.register(_exit) def test_register_exit(self): exc_raised = ZeroDivisionError @@ -338,11 +338,11 @@ class TestCleanupManager(unittest.TestCase): self.assertIsNone(exc_type) self.assertIsNone(exc) self.assertIsNone(exc_tb) - with CleanupManager() as cmgr: - cmgr.register_exit(_expect_ok) - cmgr.register_exit(_suppress_exc) - cmgr.register_exit(_expect_exc) - cmgr.register_exit(_expect_exc) + with ContextStack() as stack: + stack.register_exit(_expect_ok) + stack.register_exit(_suppress_exc) + stack.register_exit(_expect_exc) + stack.register_exit(_expect_exc) 1/0 def test_enter_context(self): @@ -353,24 +353,25 @@ class TestCleanupManager(unittest.TestCase): result.append(3) result = [] - with CleanupManager() as cmgr: - @cmgr.register # Registered first => cleaned up last + with ContextStack() as stack: + @stack.register # Registered first => cleaned up last def _exit(): result.append(4) - cmgr.enter_context(TestCM()) + stack.enter_context(TestCM()) result.append(2) self.assertEqual(result, [1, 2, 3, 4]) def test_close(self): result = [] - with CleanupManager() as cmgr: - @cmgr.register # Registered first => cleaned up last + with ContextStack() as stack: + @stack.register # Registered first => cleaned up last def _exit(): result.append(1) - cmgr.close() + stack.close() result.append(2) self.assertEqual(result, [1, 2]) if __name__ == "__main__": - test_main() + import unittest + unittest.main(__name__)