mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-05-14 10:03:12 +00:00
Rename CleanupManager to ContextStack and fix usage of the test module as __main__
This commit is contained in:
parent
0077233514
commit
4e624974a6
4 changed files with 90 additions and 59 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -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
43
test_contextlib2.py
Normal file → Executable 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__)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue