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 collections import deque
from functools import wraps from functools import wraps
__all__ = ["contextmanager", "closing", "ContextDecorator", "CleanupManager"] __all__ = ["contextmanager", "closing", "ContextDecorator", "ContextStack"]
class ContextDecorator(object): class ContextDecorator(object):
@ -141,13 +141,13 @@ class closing(object):
self.thing.close() self.thing.close()
class CleanupManager(object): class ContextStack(object):
"""Context for programmatic management of resource cleanup """Context for programmatic management of resource cleanup
For example: For example:
with CleanupManager() as cmgr: with ContextStack() as stack:
files = [cmgr.enter_context(fname) for fname in filenames] files = [stack.enter_context(fname) for fname in filenames]
# All opened files will automatically be closed at the end of # All opened files will automatically be closed at the end of
# the with statement, even if attempts to open files later # the with statement, even if attempts to open files later
# in the list throw an exception # in the list throw an exception
@ -156,22 +156,29 @@ class CleanupManager(object):
def __init__(self): def __init__(self):
self._callbacks = deque() self._callbacks = deque()
def register_exit(self, exit): def register_exit(self, callback):
"""Accepts callbacks with the same signature as context manager __exit__ methods """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) self._callbacks.append(callback)
return exit # Allow use as a decorator return callback # Allow use as a decorator
def register(self, _cb, *args, **kwds): def register(self, callback, *args, **kwds):
"""Accepts arbitrary callbacks and arguments. Cannot suppress exceptions.""" """Registers an arbitrary callback and arguments.
Cannot suppress exceptions.
"""
def _wrapper(exc_type, exc, tb): def _wrapper(exc_type, exc, tb):
_cb(*args, **kwds) callback(*args, **kwds)
return self.register_exit(_wrapper) return self.register_exit(_wrapper)
def enter_context(self, cm): 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 # We look up the special methods on the type to match the with statement
_cm_type = type(cm) _cm_type = type(cm)
_exit = _cm_type.__exit__ _exit = _cm_type.__exit__
@ -182,7 +189,7 @@ class CleanupManager(object):
return result return result
def close(self): def close(self):
"""Immediately cleanup all registered resources""" """Immediately unwind the context stack"""
self.__exit__(None, None, None) self.__exit__(None, None, None)
def __enter__(self): def __enter__(self):
@ -197,7 +204,7 @@ class CleanupManager(object):
# inner one throws an exception # inner one throws an exception
def _invoke_next_callback(exc_details): def _invoke_next_callback(exc_details):
# Callbacks are removed from the list in FIFO order # 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() cb = self._callbacks.popleft()
if not self._callbacks: if not self._callbacks:
# Innermost callback is invoked directly # 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 This may involve keeping a copy of the original arguments used to
first initialise the context manager. first initialise the context manager.
.. class:: CleanupManager()
.. class:: ContextStack()
A context manager that is designed to make it easy to programmatically A context manager that is designed to make it easy to programmatically
combine other context managers and cleanup functions, that are either combine other context managers and cleanup functions, especially those
optional or driven by input data. that are optional or otherwise driven by input data.
For example, a set of files may easily be handled in a single with For example, a set of files may easily be handled in a single with
statement as follows:: statement as follows::
with CleanupManager() as cmgr: with ContextStack() as stack:
files = [cmgr.enter_context(fname) for fname in filenames] files = [stack.enter_context(fname) for fname in filenames]
# All opened files will automatically be closed at the end of # All opened files will automatically be closed at the end of
# the with statement, even if attempts to open files later # the with statement, even if attempts to open files later
# in the list throw an exception # 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 Since registered callbacks are invoked in the reverse order of
:meth:`__exit__` methods registration, this ends up behaving as if multiple nested ``with``
statements had been used with the registered set of resources. This even
By returing true values, these callbacks can suppress exceptions the extends to exception handling - if an inner callback suppresses or replaces
same way context manager :meth:`__exit__` methods can. an exception, then outer callbacks will be passed arguments based on that
that updated state.
.. method:: register(_cb, *args, **kwds):
Accepts arbitrary callbacks and arguments. These callbacks cannot
suppress exceptions.
.. method:: enter_context(cm): .. method:: enter_context(cm):
Accepts and automatically enters other context managers. These Enters a new context manager and adds its :meth:`__exit__` method to
context managers may suppress exceptions just as they normally would. 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() .. method:: close()
Immediately cleans up all registered resources, resetting the manager Immediately unwinds the context stack, invoking callbacks in the
to its initial state in the process. 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 Obtaining the Module
@ -241,7 +264,7 @@ PyPI page`_.
There are no operating system or distribution specific versions of this There are no operating system or distribution specific versions of this
module - it is a pure Python module that should work on all platforms. 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 .. _Python Package Index: http://pypi.python.org
.. _pip: http://www.pip-installer.org .. _pip: http://www.pip-installer.org
@ -251,7 +274,7 @@ Supported Python versions are 2.7 and 3.2+.
Development and Support 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`_. improvements can be posted to the `issue tracker`_.
.. _BitBucket: https://bitbucket.org/ncoghlan/contextlib2/overview .. _BitBucket: https://bitbucket.org/ncoghlan/contextlib2/overview

View file

@ -2,7 +2,7 @@ from distutils.core import setup
setup( setup(
name='contextlib2', name='contextlib2',
version='0.1', version='0.2',
py_modules=['contextlib2'], py_modules=['contextlib2'],
license='PSF License', license='PSF License',
description='Backports and enhancements for the contextlib module', 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') raise NameError('foo')
self.assertIsNotNone(context.exc) self.assertIsNotNone(context.exc)
self.assertIs(context.exc[0], NameError) self.assertIs(context.exc[0], NameError)
self.assertIn('foo', context.exc[1]) self.assertIn('foo', str(context.exc[1]))
context = mycontext() context = mycontext()
context.catch = True context.catch = True
@ -197,7 +197,7 @@ class TestContextDecorator(unittest.TestCase):
test() test()
self.assertIsNotNone(context.exc) self.assertIsNotNone(context.exc)
self.assertIs(context.exc[0], NameError) self.assertIs(context.exc[0], NameError)
self.assertIn('foo', context.exc[1]) self.assertIn('foo', str(context.exc[1]))
def test_decorating_method(self): def test_decorating_method(self):
@ -299,10 +299,10 @@ class TestContextDecorator(unittest.TestCase):
self.assertEqual(state, [1, 'something else', 999]) self.assertEqual(state, [1, 'something else', 999])
class TestCleanupManager(unittest.TestCase): class TestContextStack(unittest.TestCase):
def test_no_resources(self): def test_no_resources(self):
with CleanupManager(): with ContextStack():
pass pass
def test_register(self): def test_register(self):
@ -317,16 +317,16 @@ class TestCleanupManager(unittest.TestCase):
result = [] result = []
def _exit(*args, **kwds): def _exit(*args, **kwds):
result.append((args, kwds)) result.append((args, kwds))
with CleanupManager() as cmgr: with ContextStack() as stack:
for args, kwds in reversed(expected): for args, kwds in reversed(expected):
if args and kwds: if args and kwds:
cmgr.register(_exit, *args, **kwds) stack.register(_exit, *args, **kwds)
elif args: elif args:
cmgr.register(_exit, *args) stack.register(_exit, *args)
elif kwds: elif kwds:
cmgr.register(_exit, **kwds) stack.register(_exit, **kwds)
else: else:
cmgr.register(_exit) stack.register(_exit)
def test_register_exit(self): def test_register_exit(self):
exc_raised = ZeroDivisionError exc_raised = ZeroDivisionError
@ -338,11 +338,11 @@ class TestCleanupManager(unittest.TestCase):
self.assertIsNone(exc_type) self.assertIsNone(exc_type)
self.assertIsNone(exc) self.assertIsNone(exc)
self.assertIsNone(exc_tb) self.assertIsNone(exc_tb)
with CleanupManager() as cmgr: with ContextStack() as stack:
cmgr.register_exit(_expect_ok) stack.register_exit(_expect_ok)
cmgr.register_exit(_suppress_exc) stack.register_exit(_suppress_exc)
cmgr.register_exit(_expect_exc) stack.register_exit(_expect_exc)
cmgr.register_exit(_expect_exc) stack.register_exit(_expect_exc)
1/0 1/0
def test_enter_context(self): def test_enter_context(self):
@ -353,24 +353,25 @@ class TestCleanupManager(unittest.TestCase):
result.append(3) result.append(3)
result = [] result = []
with CleanupManager() as cmgr: with ContextStack() as stack:
@cmgr.register # Registered first => cleaned up last @stack.register # Registered first => cleaned up last
def _exit(): def _exit():
result.append(4) result.append(4)
cmgr.enter_context(TestCM()) stack.enter_context(TestCM())
result.append(2) result.append(2)
self.assertEqual(result, [1, 2, 3, 4]) self.assertEqual(result, [1, 2, 3, 4])
def test_close(self): def test_close(self):
result = [] result = []
with CleanupManager() as cmgr: with ContextStack() as stack:
@cmgr.register # Registered first => cleaned up last @stack.register # Registered first => cleaned up last
def _exit(): def _exit():
result.append(1) result.append(1)
cmgr.close() stack.close()
result.append(2) result.append(2)
self.assertEqual(result, [1, 2]) self.assertEqual(result, [1, 2])
if __name__ == "__main__": if __name__ == "__main__":
test_main() import unittest
unittest.main(__name__)