mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-03-16 21:50:24 +00:00
Merge from default
This commit is contained in:
commit
a254dc69e2
8 changed files with 966 additions and 295 deletions
|
|
@ -15,3 +15,4 @@ htmlcov/
|
|||
_build/
|
||||
dist/
|
||||
MANIFEST
|
||||
.tox
|
||||
|
|
|
|||
13
NEWS.rst
13
NEWS.rst
|
|
@ -1,6 +1,19 @@
|
|||
Release History
|
||||
---------------
|
||||
|
||||
0.5.0 (2016-01-12)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Updated to include all features from the Python 3.4 and 3.5 releases of
|
||||
contextlib (also includes some ``ExitStack`` enhancements made following
|
||||
the integration into the standard library for Python 3.3)
|
||||
|
||||
* The legacy ``ContextStack`` and ``ContextDecorator.refresh_cm`` APIs are
|
||||
no longer documented
|
||||
|
||||
* Python 2.6, 3.2 and 3.3 have been dropped from compatibility testing
|
||||
|
||||
|
||||
0.4.0 (2012-05-05)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.4.0
|
||||
0.5.0
|
||||
|
|
|
|||
243
contextlib2.py
243
contextlib2.py
|
|
@ -1,12 +1,15 @@
|
|||
"""contextlib2 - backports and enhancements to the contextlib module"""
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from collections import deque
|
||||
from functools import wraps
|
||||
|
||||
__all__ = ["contextmanager", "closing", "ContextDecorator",
|
||||
"ContextStack", "ExitStack"]
|
||||
__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
|
||||
"redirect_stdout", "redirect_stderr", "suppress"]
|
||||
|
||||
# Backwards compatibility
|
||||
__all__ += ["ContextStack"]
|
||||
|
||||
class ContextDecorator(object):
|
||||
"A base class or mixin that enables context managers to work as decorators."
|
||||
|
|
@ -14,19 +17,36 @@ class ContextDecorator(object):
|
|||
def refresh_cm(self):
|
||||
"""Returns the context manager used to actually wrap the call to the
|
||||
decorated function.
|
||||
|
||||
|
||||
The default implementation just returns *self*.
|
||||
|
||||
Overriding this method allows otherwise one-shot context managers
|
||||
like _GeneratorContextManager to support use as decorators via
|
||||
implicit recreation.
|
||||
|
||||
DEPRECATED: refresh_cm was never added to the standard library's
|
||||
ContextDecorator API
|
||||
"""
|
||||
warnings.warn("refresh_cm was never added to the standard library",
|
||||
DeprecationWarning)
|
||||
return self._recreate_cm()
|
||||
|
||||
def _recreate_cm(self):
|
||||
"""Return a recreated instance of self.
|
||||
|
||||
Allows an otherwise one-shot context manager like
|
||||
_GeneratorContextManager to support use as
|
||||
a decorator via implicit recreation.
|
||||
|
||||
This is a private interface just for _GeneratorContextManager.
|
||||
See issue #11647 for details.
|
||||
"""
|
||||
return self
|
||||
|
||||
def __call__(self, func):
|
||||
@wraps(func)
|
||||
def inner(*args, **kwds):
|
||||
with self.refresh_cm():
|
||||
with self._recreate_cm():
|
||||
return func(*args, **kwds)
|
||||
return inner
|
||||
|
||||
|
|
@ -34,15 +54,25 @@ class ContextDecorator(object):
|
|||
class _GeneratorContextManager(ContextDecorator):
|
||||
"""Helper for @contextmanager decorator."""
|
||||
|
||||
def __init__(self, func, *args, **kwds):
|
||||
def __init__(self, func, args, kwds):
|
||||
self.gen = func(*args, **kwds)
|
||||
self.func, self.args, self.kwds = func, args, kwds
|
||||
# Issue 19330: ensure context manager instances have good docstrings
|
||||
doc = getattr(func, "__doc__", None)
|
||||
if doc is None:
|
||||
doc = type(self).__doc__
|
||||
self.__doc__ = doc
|
||||
# Unfortunately, this still doesn't provide good help output when
|
||||
# inspecting the created context manager instances, since pydoc
|
||||
# currently bypasses the instance docstring and shows the docstring
|
||||
# for the class instead.
|
||||
# See http://bugs.python.org/issue19404 for more details.
|
||||
|
||||
def refresh_cm(self):
|
||||
def _recreate_cm(self):
|
||||
# _GCM instances are one-shot context managers, so the
|
||||
# CM must be recreated each time a decorated function is
|
||||
# called
|
||||
return self.__class__(self.func, *self.args, **self.kwds)
|
||||
return self.__class__(self.func, self.args, self.kwds)
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
|
|
@ -67,10 +97,17 @@ class _GeneratorContextManager(ContextDecorator):
|
|||
self.gen.throw(type, value, traceback)
|
||||
raise RuntimeError("generator didn't stop after throw()")
|
||||
except StopIteration as exc:
|
||||
# Suppress the exception *unless* it's the same exception that
|
||||
# Suppress StopIteration *unless* it's the same exception that
|
||||
# was passed to throw(). This prevents a StopIteration
|
||||
# raised inside the "with" statement from being suppressed
|
||||
# raised inside the "with" statement from being suppressed.
|
||||
return exc is not value
|
||||
except RuntimeError as exc:
|
||||
# Likewise, avoid suppressing if a StopIteration exception
|
||||
# was passed to throw() and later wrapped into a RuntimeError
|
||||
# (see PEP 479).
|
||||
if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value:
|
||||
return False
|
||||
raise
|
||||
except:
|
||||
# only re-raise if it's *not* the exception that was
|
||||
# passed to throw(), because __exit__() must not raise
|
||||
|
|
@ -113,7 +150,7 @@ def contextmanager(func):
|
|||
"""
|
||||
@wraps(func)
|
||||
def helper(*args, **kwds):
|
||||
return _GeneratorContextManager(func, *args, **kwds)
|
||||
return _GeneratorContextManager(func, args, kwds)
|
||||
return helper
|
||||
|
||||
|
||||
|
|
@ -142,22 +179,131 @@ class closing(object):
|
|||
self.thing.close()
|
||||
|
||||
|
||||
class _RedirectStream:
|
||||
|
||||
_stream = None
|
||||
|
||||
def __init__(self, new_target):
|
||||
self._new_target = new_target
|
||||
# We use a list of old targets to make this CM re-entrant
|
||||
self._old_targets = []
|
||||
|
||||
def __enter__(self):
|
||||
self._old_targets.append(getattr(sys, self._stream))
|
||||
setattr(sys, self._stream, self._new_target)
|
||||
return self._new_target
|
||||
|
||||
def __exit__(self, exctype, excinst, exctb):
|
||||
setattr(sys, self._stream, self._old_targets.pop())
|
||||
|
||||
|
||||
class redirect_stdout(_RedirectStream):
|
||||
"""Context manager for temporarily redirecting stdout to another file.
|
||||
|
||||
# How to send help() to stderr
|
||||
with redirect_stdout(sys.stderr):
|
||||
help(dir)
|
||||
|
||||
# How to write help() to a file
|
||||
with open('help.txt', 'w') as f:
|
||||
with redirect_stdout(f):
|
||||
help(pow)
|
||||
"""
|
||||
|
||||
_stream = "stdout"
|
||||
|
||||
|
||||
class redirect_stderr(_RedirectStream):
|
||||
"""Context manager for temporarily redirecting stderr to another file."""
|
||||
|
||||
_stream = "stderr"
|
||||
|
||||
|
||||
class suppress:
|
||||
"""Context manager to suppress specified exceptions
|
||||
|
||||
After the exception is suppressed, execution proceeds with the next
|
||||
statement following the with statement.
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove(somefile)
|
||||
# Execution still resumes here if the file was already removed
|
||||
"""
|
||||
|
||||
def __init__(self, *exceptions):
|
||||
self._exceptions = exceptions
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, exctype, excinst, exctb):
|
||||
# Unlike isinstance and issubclass, CPython exception handling
|
||||
# currently only looks at the concrete type hierarchy (ignoring
|
||||
# the instance and subclass checking hooks). While Guido considers
|
||||
# that a bug rather than a feature, it's a fairly hard one to fix
|
||||
# due to various internal implementation details. suppress provides
|
||||
# the simpler issubclass based semantics, rather than trying to
|
||||
# exactly reproduce the limitations of the CPython interpreter.
|
||||
#
|
||||
# See http://bugs.python.org/issue12029 for more details
|
||||
return exctype is not None and issubclass(exctype, self._exceptions)
|
||||
|
||||
|
||||
# Context manipulation is Python 3 only
|
||||
_HAVE_EXCEPTION_CHAINING = sys.version_info.major >= 3
|
||||
if _HAVE_EXCEPTION_CHAINING:
|
||||
def _make_context_fixer(frame_exc):
|
||||
def _fix_exception_context(new_exc, old_exc):
|
||||
# Context may not be correct, so find the end of the chain
|
||||
while 1:
|
||||
exc_context = new_exc.__context__
|
||||
if exc_context is old_exc:
|
||||
# Context is already set correctly (see issue 20317)
|
||||
return
|
||||
if exc_context is None or exc_context is frame_exc:
|
||||
break
|
||||
new_exc = exc_context
|
||||
# Change the end of the chain to point to the exception
|
||||
# we expect it to reference
|
||||
new_exc.__context__ = old_exc
|
||||
return _fix_exception_context
|
||||
|
||||
def _reraise_with_existing_context(exc_details):
|
||||
try:
|
||||
# bare "raise exc_details[1]" replaces our carefully
|
||||
# set-up context
|
||||
fixed_ctx = exc_details[1].__context__
|
||||
raise exc_details[1]
|
||||
except BaseException:
|
||||
exc_details[1].__context__ = fixed_ctx
|
||||
raise
|
||||
else:
|
||||
# No exception context in Python 2
|
||||
def _make_context_fixer(frame_exc):
|
||||
return lambda new_exc, old_exc: None
|
||||
|
||||
# Use 3 argument raise in Python 2,
|
||||
# but use exec to avoid SyntaxError in Python 3
|
||||
def _reraise_with_existing_context(exc_details):
|
||||
exc_type, exc_value, exc_tb = exc_details
|
||||
exec ("raise exc_type, exc_value, exc_tb")
|
||||
|
||||
# Inspired by discussions on http://bugs.python.org/issue13585
|
||||
class ExitStack(object):
|
||||
"""Context manager for dynamic management of a stack of exit callbacks
|
||||
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
with ExitStack() as stack:
|
||||
files = [stack.enter_context(open(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
|
||||
|
||||
# in the list raise an exception
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self._exit_callbacks = deque()
|
||||
|
||||
|
||||
def pop_all(self):
|
||||
"""Preserve the context stack by transferring it to a new instance"""
|
||||
new_stack = type(self)()
|
||||
|
|
@ -171,14 +317,14 @@ class ExitStack(object):
|
|||
return cm_exit(cm, *exc_details)
|
||||
_exit_wrapper.__self__ = cm
|
||||
self.push(_exit_wrapper)
|
||||
|
||||
|
||||
def push(self, exit):
|
||||
"""Registers a callback with the standard __exit__ method signature
|
||||
|
||||
Can suppress exceptions the same way __exit__ methods can.
|
||||
|
||||
Also accepts any object with an __exit__ method (registering the
|
||||
method instead of the object itself)
|
||||
Also accepts any object with an __exit__ method (registering a call
|
||||
to the method instead of the object itself)
|
||||
"""
|
||||
# We use an unbound method rather than a bound method to follow
|
||||
# the standard lookup behaviour for special methods
|
||||
|
|
@ -194,7 +340,7 @@ class ExitStack(object):
|
|||
|
||||
def callback(self, callback, *args, **kwds):
|
||||
"""Registers an arbitrary callback and arguments.
|
||||
|
||||
|
||||
Cannot suppress exceptions.
|
||||
"""
|
||||
def _exit_wrapper(exc_type, exc, tb):
|
||||
|
|
@ -207,7 +353,7 @@ class ExitStack(object):
|
|||
|
||||
def enter_context(self, cm):
|
||||
"""Enters the supplied context manager
|
||||
|
||||
|
||||
If successful, also pushes its __exit__ method as a callback and
|
||||
returns the result of the __enter__ method.
|
||||
"""
|
||||
|
|
@ -226,40 +372,43 @@ class ExitStack(object):
|
|||
return self
|
||||
|
||||
def __exit__(self, *exc_details):
|
||||
if not self._exit_callbacks:
|
||||
return
|
||||
# This looks complicated, but it is really just
|
||||
# setting up a chain of try-expect statements to ensure
|
||||
# that outer callbacks still get invoked even if an
|
||||
# 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
|
||||
cb = self._exit_callbacks.popleft()
|
||||
if not self._exit_callbacks:
|
||||
# Innermost callback is invoked directly
|
||||
return cb(*exc_details)
|
||||
# More callbacks left, so descend another level in the stack
|
||||
received_exc = exc_details[0] is not None
|
||||
|
||||
# We manipulate the exception state so it behaves as though
|
||||
# we were actually nesting multiple with statements
|
||||
frame_exc = sys.exc_info()[1]
|
||||
_fix_exception_context = _make_context_fixer(frame_exc)
|
||||
|
||||
# Callbacks are invoked in LIFO order to match the behaviour of
|
||||
# nested context managers
|
||||
suppressed_exc = False
|
||||
pending_raise = False
|
||||
while self._exit_callbacks:
|
||||
cb = self._exit_callbacks.pop()
|
||||
try:
|
||||
suppress_exc = _invoke_next_callback(exc_details)
|
||||
except:
|
||||
suppress_exc = cb(*sys.exc_info())
|
||||
# Check if this cb suppressed the inner exception
|
||||
if not suppress_exc:
|
||||
raise
|
||||
else:
|
||||
# Check if inner cb suppressed the original exception
|
||||
if suppress_exc:
|
||||
if cb(*exc_details):
|
||||
suppressed_exc = True
|
||||
pending_raise = False
|
||||
exc_details = (None, None, None)
|
||||
suppress_exc = cb(*exc_details) or suppress_exc
|
||||
return suppress_exc
|
||||
# Kick off the recursive chain
|
||||
return _invoke_next_callback(exc_details)
|
||||
except:
|
||||
new_exc_details = sys.exc_info()
|
||||
# simulate the stack of exceptions by setting the context
|
||||
_fix_exception_context(new_exc_details[1], exc_details[1])
|
||||
pending_raise = True
|
||||
exc_details = new_exc_details
|
||||
if pending_raise:
|
||||
_reraise_with_existing_context(exc_details)
|
||||
return received_exc and suppressed_exc
|
||||
|
||||
# Preserve backwards compatibility
|
||||
class ContextStack(ExitStack):
|
||||
"""Backwards compatibility alias for ExitStack"""
|
||||
|
||||
def __init__(self):
|
||||
warnings.warn("ContextStack has been renamed to ExitStack",
|
||||
DeprecationWarning)
|
||||
super(ContextStack, self).__init__()
|
||||
|
||||
def register_exit(self, callback):
|
||||
return self.push(callback)
|
||||
|
||||
|
|
|
|||
432
docs/index.rst
432
docs/index.rst
|
|
@ -21,22 +21,25 @@ involving the ``with`` statement.
|
|||
Additions Relative to the Standard Library
|
||||
------------------------------------------
|
||||
|
||||
This module is primarily a backport of the Python 3.2 version of
|
||||
This module is primarily a backport of the Python 3.5 version of
|
||||
:mod:`contextlib` to earlier releases. However, it is also a proving ground
|
||||
for new features not yet part of the standard library. Those new features
|
||||
are currently:
|
||||
for new features not yet part of the standard library.
|
||||
|
||||
* :class:`ExitStack`
|
||||
* :meth:`ContextDecorator.refresh_cm`
|
||||
There are currently no such features in the module.
|
||||
|
||||
Refer to the :mod:`contextlib` documentation for details of which
|
||||
versions of Python 3 introduce the various APIs provided in this module.
|
||||
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
.. function:: contextmanager
|
||||
Functions and classes provided:
|
||||
|
||||
This function is a decorator that can be used to define a factory
|
||||
function for ``with`` statement context managers, without needing to
|
||||
.. decorator:: contextmanager
|
||||
|
||||
This function is a :term:`decorator` that can be used to define a factory
|
||||
function for :keyword:`with` statement context managers, without needing to
|
||||
create a class or separate :meth:`__enter__` and :meth:`__exit__` methods.
|
||||
|
||||
A simple example (this is not recommended as a real way of generating HTML!)::
|
||||
|
|
@ -56,24 +59,24 @@ API Reference
|
|||
foo
|
||||
</h1>
|
||||
|
||||
The function being decorated must return a generator-iterator when
|
||||
The function being decorated must return a :term:`generator`-iterator when
|
||||
called. This iterator must yield exactly one value, which will be bound to
|
||||
the targets in the ``with`` statement's ``as`` clause, if any.
|
||||
the targets in the :keyword:`with` statement's :keyword:`as` clause, if any.
|
||||
|
||||
At the point where the generator yields, the block nested in the ``with``
|
||||
At the point where the generator yields, the block nested in the :keyword:`with`
|
||||
statement is executed. The generator is then resumed after the block is exited.
|
||||
If an unhandled exception occurs in the block, it is reraised inside the
|
||||
generator at the point where the yield occurred. Thus, you can use a
|
||||
``try``...\ ``except``...\ ``finally`` statement to trap
|
||||
:keyword:`try`...\ :keyword:`except`...\ :keyword:`finally` statement to trap
|
||||
the error (if any), or ensure that some cleanup takes place. If an exception is
|
||||
trapped merely in order to log it or to perform some action (rather than to
|
||||
suppress it entirely), the generator must reraise that exception. Otherwise the
|
||||
generator context manager will indicate to the ``with`` statement that
|
||||
generator context manager will indicate to the :keyword:`with` statement that
|
||||
the exception has been handled, and execution will resume with the statement
|
||||
immediately following the ``with`` statement.
|
||||
immediately following the :keyword:`with` statement.
|
||||
|
||||
:func:`contextmanager` uses :class:`ContextDecorator` so the context managers
|
||||
it creates can be used as decorators as well as in ``with`` statements.
|
||||
it creates can be used as decorators as well as in :keyword:`with` statements.
|
||||
When used as a decorator, a new generator instance is implicitly created on
|
||||
each function call (this allows the otherwise "one-shot" context managers
|
||||
created by :func:`contextmanager` to meet the requirement that context
|
||||
|
|
@ -104,7 +107,97 @@ API Reference
|
|||
print(line)
|
||||
|
||||
without needing to explicitly close ``page``. Even if an error occurs,
|
||||
``page.close()`` will be called when the ``with`` block is exited.
|
||||
``page.close()`` will be called when the :keyword:`with` block is exited.
|
||||
|
||||
|
||||
.. function:: suppress(*exceptions)
|
||||
|
||||
Return a context manager that suppresses any of the specified exceptions
|
||||
if they occur in the body of a with statement and then resumes execution
|
||||
with the first statement following the end of the with statement.
|
||||
|
||||
As with any other mechanism that completely suppresses exceptions, this
|
||||
context manager should be used only to cover very specific errors where
|
||||
silently continuing with program execution is known to be the right
|
||||
thing to do.
|
||||
|
||||
For example::
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove('somefile.tmp')
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove('someotherfile.tmp')
|
||||
|
||||
This code is equivalent to::
|
||||
|
||||
try:
|
||||
os.remove('somefile.tmp')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.remove('someotherfile.tmp')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
Part of the standard library in Python 3.4 and later
|
||||
|
||||
|
||||
.. function:: redirect_stdout(new_target)
|
||||
|
||||
Context manager for temporarily redirecting :data:`sys.stdout` to
|
||||
another file or file-like object.
|
||||
|
||||
This tool adds flexibility to existing functions or classes whose output
|
||||
is hardwired to stdout.
|
||||
|
||||
For example, the output of :func:`help` normally is sent to *sys.stdout*.
|
||||
You can capture that output in a string by redirecting the output to a
|
||||
:class:`io.StringIO` object::
|
||||
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
help(pow)
|
||||
s = f.getvalue()
|
||||
|
||||
To send the output of :func:`help` to a file on disk, redirect the output
|
||||
to a regular file::
|
||||
|
||||
with open('help.txt', 'w') as f:
|
||||
with redirect_stdout(f):
|
||||
help(pow)
|
||||
|
||||
To send the output of :func:`help` to *sys.stderr*::
|
||||
|
||||
with redirect_stdout(sys.stderr):
|
||||
help(pow)
|
||||
|
||||
Note that the global side effect on :data:`sys.stdout` means that this
|
||||
context manager is not suitable for use in library code and most threaded
|
||||
applications. It also has no effect on the output of subprocesses.
|
||||
However, it is still a useful approach for many utility scripts.
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
Part of the standard library in Python 3.4 and later
|
||||
|
||||
|
||||
.. function:: redirect_stderr(new_target)
|
||||
|
||||
Similar to :func:`redirect_stdout`, but redirecting :data:`sys.stderr` to
|
||||
another file or file-like object.
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
Part of the standard library in Python 3.5 and later
|
||||
|
||||
|
||||
.. class:: ContextDecorator()
|
||||
|
|
@ -112,7 +205,7 @@ API Reference
|
|||
A base class that enables a context manager to also be used as a decorator.
|
||||
|
||||
Context managers inheriting from ``ContextDecorator`` have to implement
|
||||
:meth:`__enter__` and :meth:`__exit__` as normal. :meth:`__exit__` retains its optional
|
||||
``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional
|
||||
exception handling even when used as a decorator.
|
||||
|
||||
``ContextDecorator`` is used by :func:`contextmanager`, so you get this
|
||||
|
|
@ -174,22 +267,11 @@ API Reference
|
|||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
.. method:: refresh_cm()
|
||||
|
||||
This method is invoked each time a call is made to a decorated function.
|
||||
The default implementation just returns *self*.
|
||||
|
||||
.. note::
|
||||
As the decorated function must be able to be called multiple times, the
|
||||
underlying context manager must normally support use in multiple
|
||||
``with`` statements (preferably in a thread-safe manner). If
|
||||
this is not the case, then the context manager must define this method
|
||||
and return a *new* copy of the context manager on each invocation.
|
||||
|
||||
This may involve keeping a copy of the original arguments used to
|
||||
first initialise the context manager.
|
||||
|
||||
.. versionchanged:: 0.1
|
||||
Made the standard library's private :meth:`refresh_cm` API public
|
||||
underlying context manager must support use in multiple :keyword:`with`
|
||||
statements. If this is not the case, then the original construct with the
|
||||
explicit :keyword:`with` statement inside the function should be used.
|
||||
|
||||
|
||||
.. class:: ExitStack()
|
||||
|
|
@ -205,20 +287,32 @@ API Reference
|
|||
files = [stack.enter_context(open(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
|
||||
# in the list raise an exception
|
||||
|
||||
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.
|
||||
at the end of a :keyword:`with` statement). Note that callbacks are *not*
|
||||
invoked implicitly when the context stack instance is garbage collected.
|
||||
|
||||
This stack model is used so that context managers that acquire their
|
||||
resources in their ``__init__`` method (such as file objects) can be
|
||||
handled correctly.
|
||||
|
||||
Since registered callbacks are invoked in the reverse order of
|
||||
registration, this ends up behaving as if multiple nested ``with``
|
||||
registration, this ends up behaving as if multiple nested :keyword:`with`
|
||||
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.
|
||||
|
||||
This is a relatively low level API that takes care of the details of
|
||||
correctly unwinding the stack of exit callbacks. It provides a suitable
|
||||
foundation for higher level context managers that manipulate the exit
|
||||
stack in application specific ways.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
Part of the standard library in Python 3.3 and later
|
||||
|
||||
.. method:: enter_context(cm)
|
||||
|
||||
Enters a new context manager and adds its :meth:`__exit__` method to
|
||||
|
|
@ -226,21 +320,25 @@ API Reference
|
|||
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.
|
||||
would if used directly as part of a :keyword:`with` statement.
|
||||
|
||||
.. method:: push(exit)
|
||||
|
||||
Directly accepts a callback with the same signature as a
|
||||
context manager's :meth:`__exit__` method and adds it to the callback
|
||||
stack.
|
||||
Adds a context manager's :meth:`__exit__` method to the callback stack.
|
||||
|
||||
As ``__enter__`` is *not* invoked, this method can be used to cover
|
||||
part of an :meth:`__enter__` implementation with a context manager's own
|
||||
:meth:`__exit__` method.
|
||||
|
||||
If passed an object that is not a context manager, this method assumes
|
||||
it is a callback with the same signature as a context manager's
|
||||
:meth:`__exit__` method and adds it directly to the callback stack.
|
||||
|
||||
By returning true values, these callbacks can suppress exceptions the
|
||||
same way context manager :meth:`__exit__` methods can.
|
||||
|
||||
This method also accepts any object with an ``__exit__`` method, and
|
||||
will register that method as the callback. This is mainly useful to
|
||||
cover part of an :meth:`__enter__` implementation with a context
|
||||
manager's own :meth:`__exit__` method.
|
||||
The passed in object is returned from the function, allowing this
|
||||
method to be used as a function decorator.
|
||||
|
||||
.. method:: callback(callback, *args, **kwds)
|
||||
|
||||
|
|
@ -250,22 +348,27 @@ API Reference
|
|||
Unlike the other methods, callbacks added this way cannot suppress
|
||||
exceptions (as they are never passed the exception details).
|
||||
|
||||
The passed in callback is returned from the function, allowing this
|
||||
method to be used as a function decorator.
|
||||
|
||||
.. method:: pop_all()
|
||||
|
||||
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).
|
||||
Transfers the callback stack to a fresh :class:`ExitStack` 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 at the end of a :keyword:`with` statement).
|
||||
|
||||
For example, a group of files can be opened as an "all or nothing"
|
||||
operation as follows::
|
||||
|
||||
with ExitStack() as stack:
|
||||
files = [stack.enter_context(open(fname)) for fname in filenames]
|
||||
# Hold onto the close method, but don't call it yet.
|
||||
close_files = stack.pop_all().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
|
||||
# close_files() can then be invoked explicitly to close them all.
|
||||
|
||||
.. method:: close()
|
||||
|
||||
|
|
@ -274,21 +377,6 @@ API Reference
|
|||
callbacks registered, the arguments passed in will indicate that no
|
||||
exception occurred.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
New API for :mod:`contextlib2`, not available in standard library
|
||||
|
||||
|
||||
.. class:: ContextStack()
|
||||
|
||||
An earlier incarnation of the :class:`ExitStack` interface. This class
|
||||
is deprecated and should no longer be used.
|
||||
|
||||
.. versionchanged:: 0.4
|
||||
Deprecated in favour of :class:`ExitStack`
|
||||
|
||||
.. versionadded:: 0.2
|
||||
New API for :mod:`contextlib2`, not available in standard library
|
||||
|
||||
|
||||
Examples and Recipes
|
||||
====================
|
||||
|
|
@ -299,53 +387,6 @@ the tools provided by :mod:`contextlib2`. Some of them may also work with
|
|||
case, it is noted at the end of the example.
|
||||
|
||||
|
||||
Using a context manager as a function decorator
|
||||
-----------------------------------------------
|
||||
|
||||
:class:`ContextDecorator` makes it possible to use a context manager in
|
||||
both an ordinary ``with`` statement and also as a function decorator. The
|
||||
:meth:`ContextDecorator.refresh_cm` method even makes it possible to use
|
||||
otherwise single use context managers (such as those created by
|
||||
:func:`contextmanager`) that way.
|
||||
|
||||
For example, it is sometimes useful to wrap functions or groups of statements
|
||||
with a logger that can track the time of entry and time of exit. Rather than
|
||||
writing both a function decorator and a context manager for the task,
|
||||
:func:`contextmanager` provides both capabilities in a single
|
||||
definition::
|
||||
|
||||
from contextlib2 import contextmanager
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@contextmanager
|
||||
def track_entry_and_exit(name):
|
||||
logging.info('Entering: {}'.format(name))
|
||||
yield
|
||||
logging.info('Exiting: {}'.format(name))
|
||||
|
||||
This can be used as both a context manager::
|
||||
|
||||
with track_entry_and_exit('widget loader'):
|
||||
print('Some time consuming activity goes here')
|
||||
load_widget()
|
||||
|
||||
And also as a function decorator::
|
||||
|
||||
@track_entry_and_exit('widget loader')
|
||||
def activity():
|
||||
print('Some time consuming activity goes here')
|
||||
load_widget()
|
||||
|
||||
Note that there is one additional limitation when using context managers
|
||||
as function decorators: there's no way to access the return value of
|
||||
:meth:`__enter__`. If that value is needed, then it is still necessary to use
|
||||
an explicit ``with`` statement.
|
||||
|
||||
This example should also work with :mod:`contextlib` in Python 3.2.1 or later.
|
||||
|
||||
|
||||
Cleaning up in an ``__enter__`` implementation
|
||||
----------------------------------------------
|
||||
|
||||
|
|
@ -384,6 +425,8 @@ and maps them to the context management protocol::
|
|||
# We don't need to duplicate any of our resource release logic
|
||||
self.release_resource()
|
||||
|
||||
This example will also work with :mod:`contextlib` in Python 3.3 or later.
|
||||
|
||||
|
||||
Replacing any use of ``try-finally`` and flag variables
|
||||
-------------------------------------------------------
|
||||
|
|
@ -456,7 +499,174 @@ advance::
|
|||
|
||||
Due to the way the decorator protocol works, a callback function
|
||||
declared this way cannot take any parameters. Instead, any resources to
|
||||
be released must be accessed as closure variables
|
||||
be released must be accessed as closure variables.
|
||||
|
||||
This example will also work with :mod:`contextlib` in Python 3.3 or later.
|
||||
|
||||
|
||||
Using a context manager as a function decorator
|
||||
-----------------------------------------------
|
||||
|
||||
:class:`ContextDecorator` makes it possible to use a context manager in
|
||||
both an ordinary ``with`` statement and also as a function decorator. The
|
||||
:meth:`ContextDecorator.refresh_cm` method even makes it possible to use
|
||||
otherwise single use context managers (such as those created by
|
||||
:func:`contextmanager`) that way.
|
||||
|
||||
For example, it is sometimes useful to wrap functions or groups of statements
|
||||
with a logger that can track the time of entry and time of exit. Rather than
|
||||
writing both a function decorator and a context manager for the task,
|
||||
:func:`contextmanager` provides both capabilities in a single
|
||||
definition::
|
||||
|
||||
from contextlib2 import contextmanager
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@contextmanager
|
||||
def track_entry_and_exit(name):
|
||||
logging.info('Entering: {}'.format(name))
|
||||
yield
|
||||
logging.info('Exiting: {}'.format(name))
|
||||
|
||||
This can be used as both a context manager::
|
||||
|
||||
with track_entry_and_exit('widget loader'):
|
||||
print('Some time consuming activity goes here')
|
||||
load_widget()
|
||||
|
||||
And also as a function decorator::
|
||||
|
||||
@track_entry_and_exit('widget loader')
|
||||
def activity():
|
||||
print('Some time consuming activity goes here')
|
||||
load_widget()
|
||||
|
||||
Note that there is one additional limitation when using context managers
|
||||
as function decorators: there's no way to access the return value of
|
||||
:meth:`__enter__`. If that value is needed, then it is still necessary to use
|
||||
an explicit ``with`` statement.
|
||||
|
||||
This example will also work with :mod:`contextlib` in Python 3.2.1 or later.
|
||||
|
||||
|
||||
Context Management Concepts
|
||||
===========================
|
||||
|
||||
.. _single-use-reusable-and-reentrant-cms:
|
||||
|
||||
Single use, reusable and reentrant context managers
|
||||
---------------------------------------------------
|
||||
|
||||
Most context managers are written in a way that means they can only be
|
||||
used effectively in a :keyword:`with` statement once. These single use
|
||||
context managers must be created afresh each time they're used -
|
||||
attempting to use them a second time will trigger an exception or
|
||||
otherwise not work correctly.
|
||||
|
||||
This common limitation means that it is generally advisable to create
|
||||
context managers directly in the header of the :keyword:`with` statement
|
||||
where they are used (as shown in all of the usage examples above).
|
||||
|
||||
Files are an example of effectively single use context managers, since
|
||||
the first :keyword:`with` statement will close the file, preventing any
|
||||
further IO operations using that file object.
|
||||
|
||||
Context managers created using :func:`contextmanager` are also single use
|
||||
context managers, and will complain about the underlying generator failing
|
||||
to yield if an attempt is made to use them a second time::
|
||||
|
||||
>>> from contextlib import contextmanager
|
||||
>>> @contextmanager
|
||||
... def singleuse():
|
||||
... print("Before")
|
||||
... yield
|
||||
... print("After")
|
||||
...
|
||||
>>> cm = singleuse()
|
||||
>>> with cm:
|
||||
... pass
|
||||
...
|
||||
Before
|
||||
After
|
||||
>>> with cm:
|
||||
... pass
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
RuntimeError: generator didn't yield
|
||||
|
||||
|
||||
.. _reentrant-cms:
|
||||
|
||||
Reentrant context managers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
More sophisticated context managers may be "reentrant". These context
|
||||
managers can not only be used in multiple :keyword:`with` statements,
|
||||
but may also be used *inside* a :keyword:`with` statement that is already
|
||||
using the same context manager.
|
||||
|
||||
:class:`threading.RLock` is an example of a reentrant context manager, as is
|
||||
:func:`suppress`. Here's a toy example of reentrant use (real world
|
||||
examples of reentrancy are more likely to occur with objects like recursive
|
||||
locks and are likely to be far more complicated than this example)::
|
||||
|
||||
>>> from contextlib import suppress
|
||||
>>> ignore_raised_exception = suppress(ZeroDivisionError)
|
||||
>>> with ignore_raised_exception:
|
||||
... with ignore_raised_exception:
|
||||
... 1/0
|
||||
... print("This line runs")
|
||||
... 1/0
|
||||
... print("This is skipped")
|
||||
...
|
||||
This line runs
|
||||
>>> # The second exception is also suppressed
|
||||
|
||||
|
||||
.. _reusable-cms:
|
||||
|
||||
Reusable context managers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Distinct from both single use and reentrant context managers are "reusable"
|
||||
context managers (or, to be completely explicit, "reusable, but not
|
||||
reentrant" context managers, since reentrant context managers are also
|
||||
reusable). These context managers support being used multiple times, but
|
||||
will fail (or otherwise not work correctly) if the specific context manager
|
||||
instance has already been used in a containing with statement.
|
||||
|
||||
An example of a reusable context manager is :func:`redirect_stdout`::
|
||||
|
||||
>>> from contextlib import redirect_stdout
|
||||
>>> from io import StringIO
|
||||
>>> f = StringIO()
|
||||
>>> collect_output = redirect_stdout(f)
|
||||
>>> with collect_output:
|
||||
... print("Collected")
|
||||
...
|
||||
>>> print("Not collected")
|
||||
Not collected
|
||||
>>> with collect_output:
|
||||
... print("Also collected")
|
||||
...
|
||||
>>> print(f.getvalue())
|
||||
Collected
|
||||
Also collected
|
||||
|
||||
However, this context manager is not reentrant, so attempting to reuse it
|
||||
within a containing with statement fails:
|
||||
|
||||
>>> with collect_output:
|
||||
... # Nested reuse is not permitted
|
||||
... with collect_output:
|
||||
... pass
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
RuntimeError: Cannot reenter <...>
|
||||
|
||||
|
||||
Obtaining the Module
|
||||
|
|
|
|||
21
setup.py
21
setup.py
|
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
from distutils.core import setup
|
||||
|
||||
# Technically, unittest2 is a dependency to run the tests on 2.6 and 3.1
|
||||
# This file ignores that, since I don't want to depend on distribute
|
||||
# or setuptools just to get "tests_require" support
|
||||
# Technically, unittest2 is a dependency to run the tests on 2.7
|
||||
# This file ignores that, since I don't want to depend on
|
||||
# setuptools just to get "tests_require" support
|
||||
|
||||
setup(
|
||||
name='contextlib2',
|
||||
|
|
@ -14,5 +14,16 @@ setup(
|
|||
long_description=open('README.md').read(),
|
||||
author='Nick Coghlan',
|
||||
author_email='ncoghlan@gmail.com',
|
||||
url='http://contextlib2.readthedocs.org'
|
||||
)
|
||||
url='http://contextlib2.readthedocs.org',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: Python Software Foundation License',
|
||||
# These are the Python versions tested, it may work on others
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
],
|
||||
|
||||
)
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
"""Unit tests for contextlib2"""
|
||||
"""Unit tests for contextlib2.py"""
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
import unittest
|
||||
if not hasattr(unittest, "skipIf"):
|
||||
import unittest2 as unittest
|
||||
|
||||
import __future__ # For PEP 479 conditional test
|
||||
import contextlib2
|
||||
from contextlib2 import * # Tests __all__
|
||||
|
||||
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
|
||||
import unittest2 as unittest
|
||||
|
||||
requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2,
|
||||
"Test requires docstrings")
|
||||
|
||||
class ContextManagerTestCase(unittest.TestCase):
|
||||
|
||||
|
|
@ -81,6 +86,44 @@ class ContextManagerTestCase(unittest.TestCase):
|
|||
raise ZeroDivisionError(999)
|
||||
self.assertEqual(state, [1, 42, 999])
|
||||
|
||||
def test_contextmanager_except_stopiter(self):
|
||||
stop_exc = StopIteration('spam')
|
||||
@contextmanager
|
||||
def woohoo():
|
||||
yield
|
||||
try:
|
||||
with self.assertWarnsRegex(PendingDeprecationWarning,
|
||||
"StopIteration"):
|
||||
with woohoo():
|
||||
raise stop_exc
|
||||
except Exception as ex:
|
||||
self.assertIs(ex, stop_exc)
|
||||
else:
|
||||
self.fail('StopIteration was suppressed')
|
||||
|
||||
@unittest.skipUnless(hasattr(__future__, "generator_stop"),
|
||||
"Test only valid for versions implementing PEP 479")
|
||||
def test_contextmanager_except_pep479(self):
|
||||
code = """\
|
||||
from __future__ import generator_stop
|
||||
from contextlib import contextmanager
|
||||
@contextmanager
|
||||
def woohoo():
|
||||
yield
|
||||
"""
|
||||
locals = {}
|
||||
exec(code, locals, locals)
|
||||
woohoo = locals['woohoo']
|
||||
|
||||
stop_exc = StopIteration('spam')
|
||||
try:
|
||||
with woohoo():
|
||||
raise stop_exc
|
||||
except Exception as ex:
|
||||
self.assertIs(ex, stop_exc)
|
||||
else:
|
||||
self.fail('StopIteration was suppressed')
|
||||
|
||||
def _create_contextmanager_attribs(self):
|
||||
def attribs(**kw):
|
||||
def decorate(func):
|
||||
|
|
@ -99,15 +142,33 @@ class ContextManagerTestCase(unittest.TestCase):
|
|||
self.assertEqual(baz.__name__,'baz')
|
||||
self.assertEqual(baz.foo, 'bar')
|
||||
|
||||
@unittest.skipIf(sys.flags.optimize >= 2,
|
||||
"Docstrings are omitted with -O2 and above")
|
||||
@requires_docstrings
|
||||
def test_contextmanager_doc_attrib(self):
|
||||
baz = self._create_contextmanager_attribs()
|
||||
self.assertEqual(baz.__doc__, "Whee!")
|
||||
|
||||
@requires_docstrings
|
||||
def test_instance_docstring_given_cm_docstring(self):
|
||||
baz = self._create_contextmanager_attribs()(None)
|
||||
self.assertEqual(baz.__doc__, "Whee!")
|
||||
|
||||
def test_keywords(self):
|
||||
# Ensure no keyword arguments are inhibited
|
||||
@contextmanager
|
||||
def woohoo(self, func, args, kwds):
|
||||
yield (self, func, args, kwds)
|
||||
with woohoo(self=11, func=22, args=33, kwds=44) as target:
|
||||
self.assertEqual(target, (11, 22, 33, 44))
|
||||
|
||||
|
||||
class ClosingTestCase(unittest.TestCase):
|
||||
|
||||
# XXX This needs more work
|
||||
@requires_docstrings
|
||||
def test_instance_docs(self):
|
||||
# Issue 19330: ensure context manager instances have good docstrings
|
||||
cm_docstring = closing.__doc__
|
||||
obj = closing(None)
|
||||
self.assertEqual(obj.__doc__, cm_docstring)
|
||||
|
||||
def test_closing(self):
|
||||
state = []
|
||||
|
|
@ -135,6 +196,7 @@ class ClosingTestCase(unittest.TestCase):
|
|||
|
||||
|
||||
class mycontext(ContextDecorator):
|
||||
"""Example decoration-compatible context manager for testing"""
|
||||
started = False
|
||||
exc = None
|
||||
catch = False
|
||||
|
|
@ -150,6 +212,13 @@ class mycontext(ContextDecorator):
|
|||
|
||||
class TestContextDecorator(unittest.TestCase):
|
||||
|
||||
@requires_docstrings
|
||||
def test_instance_docs(self):
|
||||
# Issue 19330: ensure context manager instances have good docstrings
|
||||
cm_docstring = mycontext.__doc__
|
||||
obj = mycontext()
|
||||
self.assertEqual(obj.__doc__, cm_docstring)
|
||||
|
||||
def test_contextdecorator(self):
|
||||
context = mycontext()
|
||||
with context as result:
|
||||
|
|
@ -162,12 +231,11 @@ class TestContextDecorator(unittest.TestCase):
|
|||
def test_contextdecorator_with_exception(self):
|
||||
context = mycontext()
|
||||
|
||||
with self.assertRaises(NameError):
|
||||
with self.assertRaisesRegex(NameError, 'foo'):
|
||||
with context:
|
||||
raise NameError('foo')
|
||||
self.assertIsNotNone(context.exc)
|
||||
self.assertIs(context.exc[0], NameError)
|
||||
self.assertIn('foo', str(context.exc[1]))
|
||||
|
||||
context = mycontext()
|
||||
context.catch = True
|
||||
|
|
@ -197,11 +265,10 @@ class TestContextDecorator(unittest.TestCase):
|
|||
self.assertTrue(context.started)
|
||||
raise NameError('foo')
|
||||
|
||||
with self.assertRaises(NameError):
|
||||
with self.assertRaisesRegex(NameError, 'foo'):
|
||||
test()
|
||||
self.assertIsNotNone(context.exc)
|
||||
self.assertIs(context.exc[0], NameError)
|
||||
self.assertIn('foo', str(context.exc[1]))
|
||||
|
||||
|
||||
def test_decorating_method(self):
|
||||
|
|
@ -302,9 +369,18 @@ class TestContextDecorator(unittest.TestCase):
|
|||
test('something else')
|
||||
self.assertEqual(state, [1, 'something else', 999])
|
||||
|
||||
# Detailed exception chaining checks only make sense on Python 3
|
||||
check_exception_chaining = contextlib2._HAVE_EXCEPTION_CHAINING
|
||||
|
||||
class TestExitStack(unittest.TestCase):
|
||||
|
||||
@requires_docstrings
|
||||
def test_instance_docs(self):
|
||||
# Issue 19330: ensure context manager instances have good docstrings
|
||||
cm_docstring = ExitStack.__doc__
|
||||
obj = ExitStack()
|
||||
self.assertEqual(obj.__doc__, cm_docstring)
|
||||
|
||||
def test_no_resources(self):
|
||||
with ExitStack():
|
||||
pass
|
||||
|
|
@ -416,6 +492,211 @@ class TestExitStack(unittest.TestCase):
|
|||
new_stack.close()
|
||||
self.assertEqual(result, [1, 2, 3])
|
||||
|
||||
def test_exit_raise(self):
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
with ExitStack() as stack:
|
||||
stack.push(lambda *exc: False)
|
||||
1/0
|
||||
|
||||
def test_exit_suppress(self):
|
||||
with ExitStack() as stack:
|
||||
stack.push(lambda *exc: True)
|
||||
1/0
|
||||
|
||||
def test_exit_exception_chaining_reference(self):
|
||||
# Sanity check to make sure that ExitStack chaining matches
|
||||
# actual nested with statements
|
||||
class RaiseExc:
|
||||
def __init__(self, exc):
|
||||
self.exc = exc
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *exc_details):
|
||||
raise self.exc
|
||||
|
||||
class RaiseExcWithContext:
|
||||
def __init__(self, outer, inner):
|
||||
self.outer = outer
|
||||
self.inner = inner
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *exc_details):
|
||||
try:
|
||||
raise self.inner
|
||||
except:
|
||||
raise self.outer
|
||||
|
||||
class SuppressExc:
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *exc_details):
|
||||
self.__class__.saved_details = exc_details
|
||||
return True
|
||||
|
||||
try:
|
||||
with RaiseExc(IndexError):
|
||||
with RaiseExcWithContext(KeyError, AttributeError):
|
||||
with SuppressExc():
|
||||
with RaiseExc(ValueError):
|
||||
1 / 0
|
||||
except IndexError as exc:
|
||||
if check_exception_chaining:
|
||||
self.assertIsInstance(exc.__context__, KeyError)
|
||||
self.assertIsInstance(exc.__context__.__context__, AttributeError)
|
||||
# Inner exceptions were suppressed
|
||||
self.assertIsNone(exc.__context__.__context__.__context__)
|
||||
else:
|
||||
self.fail("Expected IndexError, but no exception was raised")
|
||||
# Check the inner exceptions
|
||||
inner_exc = SuppressExc.saved_details[1]
|
||||
self.assertIsInstance(inner_exc, ValueError)
|
||||
if check_exception_chaining:
|
||||
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
||||
|
||||
def test_exit_exception_chaining(self):
|
||||
# Ensure exception chaining matches the reference behaviour
|
||||
def raise_exc(exc):
|
||||
raise exc
|
||||
|
||||
saved_details = [None]
|
||||
def suppress_exc(*exc_details):
|
||||
saved_details[0] = exc_details
|
||||
return True
|
||||
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
stack.callback(raise_exc, IndexError)
|
||||
stack.callback(raise_exc, KeyError)
|
||||
stack.callback(raise_exc, AttributeError)
|
||||
stack.push(suppress_exc)
|
||||
stack.callback(raise_exc, ValueError)
|
||||
1 / 0
|
||||
except IndexError as exc:
|
||||
if check_exception_chaining:
|
||||
self.assertIsInstance(exc.__context__, KeyError)
|
||||
self.assertIsInstance(exc.__context__.__context__, AttributeError)
|
||||
# Inner exceptions were suppressed
|
||||
self.assertIsNone(exc.__context__.__context__.__context__)
|
||||
else:
|
||||
self.fail("Expected IndexError, but no exception was raised")
|
||||
# Check the inner exceptions
|
||||
inner_exc = saved_details[0][1]
|
||||
self.assertIsInstance(inner_exc, ValueError)
|
||||
if check_exception_chaining:
|
||||
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
||||
|
||||
def test_exit_exception_non_suppressing(self):
|
||||
# http://bugs.python.org/issue19092
|
||||
def raise_exc(exc):
|
||||
raise exc
|
||||
|
||||
def suppress_exc(*exc_details):
|
||||
return True
|
||||
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
stack.callback(lambda: None)
|
||||
stack.callback(raise_exc, IndexError)
|
||||
except Exception as exc:
|
||||
self.assertIsInstance(exc, IndexError)
|
||||
else:
|
||||
self.fail("Expected IndexError, but no exception was raised")
|
||||
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
stack.callback(raise_exc, KeyError)
|
||||
stack.push(suppress_exc)
|
||||
stack.callback(raise_exc, IndexError)
|
||||
except Exception as exc:
|
||||
self.assertIsInstance(exc, KeyError)
|
||||
else:
|
||||
self.fail("Expected KeyError, but no exception was raised")
|
||||
|
||||
def test_exit_exception_with_correct_context(self):
|
||||
# http://bugs.python.org/issue20317
|
||||
@contextmanager
|
||||
def gets_the_context_right(exc):
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
raise exc
|
||||
|
||||
exc1 = Exception(1)
|
||||
exc2 = Exception(2)
|
||||
exc3 = Exception(3)
|
||||
exc4 = Exception(4)
|
||||
|
||||
# The contextmanager already fixes the context, so prior to the
|
||||
# fix, ExitStack would try to fix it *again* and get into an
|
||||
# infinite self-referential loop
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(gets_the_context_right(exc4))
|
||||
stack.enter_context(gets_the_context_right(exc3))
|
||||
stack.enter_context(gets_the_context_right(exc2))
|
||||
raise exc1
|
||||
except Exception as exc:
|
||||
self.assertIs(exc, exc4)
|
||||
if check_exception_chaining:
|
||||
self.assertIs(exc.__context__, exc3)
|
||||
self.assertIs(exc.__context__.__context__, exc2)
|
||||
self.assertIs(exc.__context__.__context__.__context__, exc1)
|
||||
self.assertIsNone(
|
||||
exc.__context__.__context__.__context__.__context__)
|
||||
|
||||
def test_exit_exception_with_existing_context(self):
|
||||
# Addresses a lack of test coverage discovered after checking in a
|
||||
# fix for issue 20317 that still contained debugging code.
|
||||
def raise_nested(inner_exc, outer_exc):
|
||||
try:
|
||||
raise inner_exc
|
||||
finally:
|
||||
raise outer_exc
|
||||
exc1 = Exception(1)
|
||||
exc2 = Exception(2)
|
||||
exc3 = Exception(3)
|
||||
exc4 = Exception(4)
|
||||
exc5 = Exception(5)
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
stack.callback(raise_nested, exc4, exc5)
|
||||
stack.callback(raise_nested, exc2, exc3)
|
||||
raise exc1
|
||||
except Exception as exc:
|
||||
self.assertIs(exc, exc5)
|
||||
if check_exception_chaining:
|
||||
self.assertIs(exc.__context__, exc4)
|
||||
self.assertIs(exc.__context__.__context__, exc3)
|
||||
self.assertIs(exc.__context__.__context__.__context__, exc2)
|
||||
self.assertIs(
|
||||
exc.__context__.__context__.__context__.__context__, exc1)
|
||||
self.assertIsNone(
|
||||
exc.__context__.__context__.__context__.__context__.__context__)
|
||||
|
||||
|
||||
|
||||
def test_body_exception_suppress(self):
|
||||
def suppress_exc(*exc_details):
|
||||
return True
|
||||
try:
|
||||
with ExitStack() as stack:
|
||||
stack.push(suppress_exc)
|
||||
1/0
|
||||
except IndexError as exc:
|
||||
self.fail("Expected no exception, got IndexError")
|
||||
|
||||
def test_exit_exception_chaining_suppress(self):
|
||||
with ExitStack() as stack:
|
||||
stack.push(lambda *exc: True)
|
||||
stack.push(lambda *exc: 1/0)
|
||||
stack.push(lambda *exc: {}[1])
|
||||
|
||||
def test_excessive_nesting(self):
|
||||
# The original implementation would die with RecursionError here
|
||||
with ExitStack() as stack:
|
||||
for i in range(10000):
|
||||
stack.callback(int)
|
||||
|
||||
def test_instance_bypass(self):
|
||||
class Example(object): pass
|
||||
cm = Example()
|
||||
|
|
@ -426,128 +707,129 @@ class TestExitStack(unittest.TestCase):
|
|||
self.assertIs(stack._exit_callbacks[-1], cm)
|
||||
|
||||
|
||||
class TestContextStack(unittest.TestCase):
|
||||
|
||||
def test_no_resources(self):
|
||||
with ContextStack():
|
||||
pass
|
||||
class TestRedirectStream:
|
||||
|
||||
def test_register(self):
|
||||
expected = [
|
||||
((), {}),
|
||||
((1,), {}),
|
||||
((1,2), {}),
|
||||
((), dict(example=1)),
|
||||
((1,), dict(example=1)),
|
||||
((1,2), dict(example=1)),
|
||||
]
|
||||
result = []
|
||||
def _exit(*args, **kwds):
|
||||
"""Test metadata propagation"""
|
||||
result.append((args, kwds))
|
||||
with ContextStack() as stack:
|
||||
for args, kwds in reversed(expected):
|
||||
if args and kwds:
|
||||
f = stack.register(_exit, *args, **kwds)
|
||||
elif args:
|
||||
f = stack.register(_exit, *args)
|
||||
elif kwds:
|
||||
f = stack.register(_exit, **kwds)
|
||||
else:
|
||||
f = stack.register(_exit)
|
||||
self.assertIs(f, _exit)
|
||||
for wrapper in stack._exit_callbacks:
|
||||
self.assertIs(wrapper.__wrapped__, _exit)
|
||||
self.assertNotEqual(wrapper.__name__, _exit.__name__)
|
||||
self.assertIsNone(wrapper.__doc__, _exit.__doc__)
|
||||
self.assertEqual(result, expected)
|
||||
redirect_stream = None
|
||||
orig_stream = None
|
||||
|
||||
def test_register_exit(self):
|
||||
exc_raised = ZeroDivisionError
|
||||
def _expect_exc(exc_type, exc, exc_tb):
|
||||
self.assertIs(exc_type, exc_raised)
|
||||
def _suppress_exc(*exc_details):
|
||||
return True
|
||||
def _expect_ok(exc_type, exc, exc_tb):
|
||||
self.assertIsNone(exc_type)
|
||||
self.assertIsNone(exc)
|
||||
self.assertIsNone(exc_tb)
|
||||
class ExitCM(object):
|
||||
def __init__(self, check_exc):
|
||||
self.check_exc = check_exc
|
||||
def __enter__(self):
|
||||
self.fail("Should not be called!")
|
||||
def __exit__(self, *exc_details):
|
||||
self.check_exc(*exc_details)
|
||||
with ContextStack() as stack:
|
||||
stack.register_exit(_expect_ok)
|
||||
self.assertIs(stack._exit_callbacks[-1], _expect_ok)
|
||||
cm = ExitCM(_expect_ok)
|
||||
stack.register_exit(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||
stack.register_exit(_suppress_exc)
|
||||
self.assertIs(stack._exit_callbacks[-1], _suppress_exc)
|
||||
cm = ExitCM(_expect_exc)
|
||||
stack.register_exit(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||
stack.register_exit(_expect_exc)
|
||||
self.assertIs(stack._exit_callbacks[-1], _expect_exc)
|
||||
stack.register_exit(_expect_exc)
|
||||
self.assertIs(stack._exit_callbacks[-1], _expect_exc)
|
||||
@requires_docstrings
|
||||
def test_instance_docs(self):
|
||||
# Issue 19330: ensure context manager instances have good docstrings
|
||||
cm_docstring = self.redirect_stream.__doc__
|
||||
obj = self.redirect_stream(None)
|
||||
self.assertEqual(obj.__doc__, cm_docstring)
|
||||
|
||||
def test_no_redirect_in_init(self):
|
||||
orig_stdout = getattr(sys, self.orig_stream)
|
||||
self.redirect_stream(None)
|
||||
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||
|
||||
def test_redirect_to_string_io(self):
|
||||
f = io.StringIO()
|
||||
msg = "Consider an API like help(), which prints directly to stdout"
|
||||
orig_stdout = getattr(sys, self.orig_stream)
|
||||
with self.redirect_stream(f):
|
||||
print(msg, file=getattr(sys, self.orig_stream))
|
||||
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||
s = f.getvalue().strip()
|
||||
self.assertEqual(s, msg)
|
||||
|
||||
def test_enter_result_is_target(self):
|
||||
f = io.StringIO()
|
||||
with self.redirect_stream(f) as enter_result:
|
||||
self.assertIs(enter_result, f)
|
||||
|
||||
def test_cm_is_reusable(self):
|
||||
f = io.StringIO()
|
||||
write_to_f = self.redirect_stream(f)
|
||||
orig_stdout = getattr(sys, self.orig_stream)
|
||||
with write_to_f:
|
||||
print("Hello", end=" ", file=getattr(sys, self.orig_stream))
|
||||
with write_to_f:
|
||||
print("World!", file=getattr(sys, self.orig_stream))
|
||||
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||
s = f.getvalue()
|
||||
self.assertEqual(s, "Hello World!\n")
|
||||
|
||||
def test_cm_is_reentrant(self):
|
||||
f = io.StringIO()
|
||||
write_to_f = self.redirect_stream(f)
|
||||
orig_stdout = getattr(sys, self.orig_stream)
|
||||
with write_to_f:
|
||||
print("Hello", end=" ", file=getattr(sys, self.orig_stream))
|
||||
with write_to_f:
|
||||
print("World!", file=getattr(sys, self.orig_stream))
|
||||
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||
s = f.getvalue()
|
||||
self.assertEqual(s, "Hello World!\n")
|
||||
|
||||
|
||||
class TestRedirectStdout(TestRedirectStream, unittest.TestCase):
|
||||
|
||||
redirect_stream = redirect_stdout
|
||||
orig_stream = "stdout"
|
||||
|
||||
|
||||
class TestRedirectStderr(TestRedirectStream, unittest.TestCase):
|
||||
|
||||
redirect_stream = redirect_stderr
|
||||
orig_stream = "stderr"
|
||||
|
||||
|
||||
class TestSuppress(unittest.TestCase):
|
||||
|
||||
@requires_docstrings
|
||||
def test_instance_docs(self):
|
||||
# Issue 19330: ensure context manager instances have good docstrings
|
||||
cm_docstring = suppress.__doc__
|
||||
obj = suppress()
|
||||
self.assertEqual(obj.__doc__, cm_docstring)
|
||||
|
||||
def test_no_result_from_enter(self):
|
||||
with suppress(ValueError) as enter_result:
|
||||
self.assertIsNone(enter_result)
|
||||
|
||||
def test_no_exception(self):
|
||||
with suppress(ValueError):
|
||||
self.assertEqual(pow(2, 5), 32)
|
||||
|
||||
def test_exact_exception(self):
|
||||
with suppress(TypeError):
|
||||
len(5)
|
||||
|
||||
def test_exception_hierarchy(self):
|
||||
with suppress(LookupError):
|
||||
'Hello'[50]
|
||||
|
||||
def test_other_exception(self):
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
with suppress(TypeError):
|
||||
1/0
|
||||
|
||||
def test_no_args(self):
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
with suppress():
|
||||
1/0
|
||||
|
||||
def test_multiple_exception_args(self):
|
||||
with suppress(ZeroDivisionError, TypeError):
|
||||
1/0
|
||||
with suppress(ZeroDivisionError, TypeError):
|
||||
len(5)
|
||||
|
||||
def test_enter_context(self):
|
||||
class TestCM(object):
|
||||
def __enter__(self):
|
||||
result.append(1)
|
||||
def __exit__(self, *exc_details):
|
||||
result.append(3)
|
||||
def test_cm_is_reentrant(self):
|
||||
ignore_exceptions = suppress(Exception)
|
||||
with ignore_exceptions:
|
||||
pass
|
||||
with ignore_exceptions:
|
||||
len(5)
|
||||
with ignore_exceptions:
|
||||
with ignore_exceptions: # Check nested usage
|
||||
len(5)
|
||||
outer_continued = True
|
||||
1/0
|
||||
self.assertTrue(outer_continued)
|
||||
|
||||
result = []
|
||||
cm = TestCM()
|
||||
with ContextStack() as stack:
|
||||
@stack.register # Registered first => cleaned up last
|
||||
def _exit():
|
||||
result.append(4)
|
||||
self.assertIsNotNone(_exit)
|
||||
stack.enter_context(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||
result.append(2)
|
||||
self.assertEqual(result, [1, 2, 3, 4])
|
||||
|
||||
def test_close(self):
|
||||
result = []
|
||||
with ContextStack() as stack:
|
||||
@stack.register
|
||||
def _exit():
|
||||
result.append(1)
|
||||
self.assertIsNotNone(_exit)
|
||||
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)
|
||||
self.assertIsNotNone(_exit)
|
||||
new_stack = stack.preserve()
|
||||
result.append(1)
|
||||
result.append(2)
|
||||
new_stack.close()
|
||||
self.assertEqual(result, [1, 2, 3])
|
||||
|
||||
def test_instance_bypass(self):
|
||||
class Example(object): pass
|
||||
cm = Example()
|
||||
cm.__exit__ = object()
|
||||
stack = ContextStack()
|
||||
self.assertRaises(AttributeError, stack.enter_context, cm)
|
||||
stack.register_exit(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1], cm)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
unittest.main(__name__)
|
||||
unittest.main()
|
||||
|
|
|
|||
5
tox.ini
5
tox.ini
|
|
@ -4,3 +4,8 @@ envlist = py27, pypy, py34, py35, pypy3
|
|||
[testenv]
|
||||
commands = {envpython} test_contextlib2.py
|
||||
|
||||
[testenv:py27]
|
||||
deps = unittest2
|
||||
|
||||
[testenv:pypy]
|
||||
deps = unittest2
|
||||
|
|
|
|||
Loading…
Reference in a new issue