Rename CleanupManager to ContextStack and fix usage of the test module as __main__

This commit is contained in:
Nick Coghlan 2011-12-15 21:07:03 +10:00
parent 0077233514
commit 4e624974a6
4 changed files with 90 additions and 59 deletions

View file

@ -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

View file

@ -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

View file

@ -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',

43
test_contextlib2.py Normal file → Executable file
View file

@ -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__)