Merge from default

This commit is contained in:
Nick Coghlan 2016-01-12 21:13:45 +10:00
commit a254dc69e2
8 changed files with 966 additions and 295 deletions

View file

@ -15,3 +15,4 @@ htmlcov/
_build/ _build/
dist/ dist/
MANIFEST MANIFEST
.tox

View file

@ -1,6 +1,19 @@
Release History 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) 0.4.0 (2012-05-05)
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~

View file

@ -1 +1 @@
0.4.0 0.5.0

View file

@ -1,12 +1,15 @@
"""contextlib2 - backports and enhancements to the contextlib module""" """contextlib2 - backports and enhancements to the contextlib module"""
import sys import sys
import warnings
from collections import deque from collections import deque
from functools import wraps from functools import wraps
__all__ = ["contextmanager", "closing", "ContextDecorator", __all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
"ContextStack", "ExitStack"] "redirect_stdout", "redirect_stderr", "suppress"]
# Backwards compatibility
__all__ += ["ContextStack"]
class ContextDecorator(object): class ContextDecorator(object):
"A base class or mixin that enables context managers to work as decorators." "A base class or mixin that enables context managers to work as decorators."
@ -20,13 +23,30 @@ class ContextDecorator(object):
Overriding this method allows otherwise one-shot context managers Overriding this method allows otherwise one-shot context managers
like _GeneratorContextManager to support use as decorators via like _GeneratorContextManager to support use as decorators via
implicit recreation. 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 return self
def __call__(self, func): def __call__(self, func):
@wraps(func) @wraps(func)
def inner(*args, **kwds): def inner(*args, **kwds):
with self.refresh_cm(): with self._recreate_cm():
return func(*args, **kwds) return func(*args, **kwds)
return inner return inner
@ -34,15 +54,25 @@ class ContextDecorator(object):
class _GeneratorContextManager(ContextDecorator): class _GeneratorContextManager(ContextDecorator):
"""Helper for @contextmanager decorator.""" """Helper for @contextmanager decorator."""
def __init__(self, func, *args, **kwds): def __init__(self, func, args, kwds):
self.gen = func(*args, **kwds) self.gen = func(*args, **kwds)
self.func, self.args, self.kwds = 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 # _GCM instances are one-shot context managers, so the
# CM must be recreated each time a decorated function is # CM must be recreated each time a decorated function is
# called # called
return self.__class__(self.func, *self.args, **self.kwds) return self.__class__(self.func, self.args, self.kwds)
def __enter__(self): def __enter__(self):
try: try:
@ -67,10 +97,17 @@ class _GeneratorContextManager(ContextDecorator):
self.gen.throw(type, value, traceback) self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()") raise RuntimeError("generator didn't stop after throw()")
except StopIteration as exc: 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 # 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 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: except:
# only re-raise if it's *not* the exception that was # only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise # passed to throw(), because __exit__() must not raise
@ -113,7 +150,7 @@ def contextmanager(func):
""" """
@wraps(func) @wraps(func)
def helper(*args, **kwds): def helper(*args, **kwds):
return _GeneratorContextManager(func, *args, **kwds) return _GeneratorContextManager(func, args, kwds)
return helper return helper
@ -142,6 +179,115 @@ class closing(object):
self.thing.close() 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 # Inspired by discussions on http://bugs.python.org/issue13585
class ExitStack(object): class ExitStack(object):
"""Context manager for dynamic management of a stack of exit callbacks """Context manager for dynamic management of a stack of exit callbacks
@ -152,7 +298,7 @@ class ExitStack(object):
files = [stack.enter_context(open(fname)) for fname in filenames] files = [stack.enter_context(open(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 raise an exception
""" """
def __init__(self): def __init__(self):
@ -177,8 +323,8 @@ class ExitStack(object):
Can suppress exceptions the same way __exit__ methods can. Can suppress exceptions the same way __exit__ methods can.
Also accepts any object with an __exit__ method (registering the Also accepts any object with an __exit__ method (registering a call
method instead of the object itself) to the method instead of the object itself)
""" """
# We use an unbound method rather than a bound method to follow # We use an unbound method rather than a bound method to follow
# the standard lookup behaviour for special methods # the standard lookup behaviour for special methods
@ -226,40 +372,43 @@ class ExitStack(object):
return self return self
def __exit__(self, *exc_details): def __exit__(self, *exc_details):
if not self._exit_callbacks: received_exc = exc_details[0] is not None
return
# This looks complicated, but it is really just # We manipulate the exception state so it behaves as though
# setting up a chain of try-expect statements to ensure # we were actually nesting multiple with statements
# that outer callbacks still get invoked even if an frame_exc = sys.exc_info()[1]
# inner one throws an exception _fix_exception_context = _make_context_fixer(frame_exc)
def _invoke_next_callback(exc_details):
# Callbacks are removed from the list in FIFO order # Callbacks are invoked in LIFO order to match the behaviour of
# but the recursion means they're invoked in LIFO order # nested context managers
cb = self._exit_callbacks.popleft() suppressed_exc = False
if not self._exit_callbacks: pending_raise = False
# Innermost callback is invoked directly while self._exit_callbacks:
return cb(*exc_details) cb = self._exit_callbacks.pop()
# More callbacks left, so descend another level in the stack
try: try:
suppress_exc = _invoke_next_callback(exc_details) if cb(*exc_details):
except: suppressed_exc = True
suppress_exc = cb(*sys.exc_info()) pending_raise = False
# 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:
exc_details = (None, None, None) exc_details = (None, None, None)
suppress_exc = cb(*exc_details) or suppress_exc except:
return suppress_exc new_exc_details = sys.exc_info()
# Kick off the recursive chain # simulate the stack of exceptions by setting the context
return _invoke_next_callback(exc_details) _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 # Preserve backwards compatibility
class ContextStack(ExitStack): class ContextStack(ExitStack):
"""Backwards compatibility alias for 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): def register_exit(self, callback):
return self.push(callback) return self.push(callback)

View file

@ -21,22 +21,25 @@ involving the ``with`` statement.
Additions Relative to the Standard Library 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 :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 for new features not yet part of the standard library.
are currently:
* :class:`ExitStack` There are currently no such features in the module.
* :meth:`ContextDecorator.refresh_cm`
Refer to the :mod:`contextlib` documentation for details of which
versions of Python 3 introduce the various APIs provided in this module.
API Reference API Reference
============= =============
.. function:: contextmanager Functions and classes provided:
This function is a decorator that can be used to define a factory .. decorator:: contextmanager
function for ``with`` statement context managers, without needing to
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. 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!):: A simple example (this is not recommended as a real way of generating HTML!)::
@ -56,24 +59,24 @@ API Reference
foo foo
</h1> </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 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. 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 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 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 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 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 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 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 :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 When used as a decorator, a new generator instance is implicitly created on
each function call (this allows the otherwise "one-shot" context managers each function call (this allows the otherwise "one-shot" context managers
created by :func:`contextmanager` to meet the requirement that context created by :func:`contextmanager` to meet the requirement that context
@ -104,7 +107,97 @@ API Reference
print(line) print(line)
without needing to explicitly close ``page``. Even if an error occurs, 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() .. class:: ContextDecorator()
@ -112,7 +205,7 @@ API Reference
A base class that enables a context manager to also be used as a decorator. A base class that enables a context manager to also be used as a decorator.
Context managers inheriting from ``ContextDecorator`` have to implement 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. exception handling even when used as a decorator.
``ContextDecorator`` is used by :func:`contextmanager`, so you get this ``ContextDecorator`` is used by :func:`contextmanager`, so you get this
@ -174,22 +267,11 @@ API Reference
def __exit__(self, *exc): def __exit__(self, *exc):
return False return False
.. method:: refresh_cm() .. note::
This method is invoked each time a call is made to a decorated function.
The default implementation just returns *self*.
As the decorated function must be able to be called multiple times, the As the decorated function must be able to be called multiple times, the
underlying context manager must normally support use in multiple underlying context manager must support use in multiple :keyword:`with`
``with`` statements (preferably in a thread-safe manner). If statements. If this is not the case, then the original construct with the
this is not the case, then the context manager must define this method explicit :keyword:`with` statement inside the function should be used.
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
.. class:: ExitStack() .. class:: ExitStack()
@ -205,20 +287,32 @@ API Reference
files = [stack.enter_context(open(fname)) for fname in filenames] files = [stack.enter_context(open(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 raise an exception
Each instance maintains a stack of registered callbacks that are called in Each instance maintains a stack of registered callbacks that are called in
reverse order when the instance is closed (either explicitly or implicitly reverse order when the instance is closed (either explicitly or implicitly
at the end of a ``with`` statement). Note that callbacks are *not* invoked at the end of a :keyword:`with` statement). Note that callbacks are *not*
implicitly when the context stack instance is garbage collected. 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 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 statements had been used with the registered set of callbacks. This even
extends to exception handling - if an inner callback suppresses or replaces extends to exception handling - if an inner callback suppresses or replaces
an exception, then outer callbacks will be passed arguments based on that an exception, then outer callbacks will be passed arguments based on that
updated state. 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) .. method:: enter_context(cm)
Enters a new context manager and adds its :meth:`__exit__` method to Enters a new context manager and adds its :meth:`__exit__` method to
@ -226,21 +320,25 @@ API Reference
manager's own :meth:`__enter__` method. manager's own :meth:`__enter__` method.
These context managers may suppress exceptions just as they normally 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) .. method:: push(exit)
Directly accepts a callback with the same signature as a Adds a context manager's :meth:`__exit__` method to the callback stack.
context manager's :meth:`__exit__` method and adds it 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 By returning true values, these callbacks can suppress exceptions the
same way context manager :meth:`__exit__` methods can. same way context manager :meth:`__exit__` methods can.
This method also accepts any object with an ``__exit__`` method, and The passed in object is returned from the function, allowing this
will register that method as the callback. This is mainly useful to method to be used as a function decorator.
cover part of an :meth:`__enter__` implementation with a context
manager's own :meth:`__exit__` method.
.. method:: callback(callback, *args, **kwds) .. method:: callback(callback, *args, **kwds)
@ -250,22 +348,27 @@ API Reference
Unlike the other methods, callbacks added this way cannot suppress Unlike the other methods, callbacks added this way cannot suppress
exceptions (as they are never passed the exception details). 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() .. method:: pop_all()
Transfers the callback stack to a fresh instance and returns it. No Transfers the callback stack to a fresh :class:`ExitStack` instance
callbacks are invoked by this operation - instead, they will now be and returns it. No callbacks are invoked by this operation - instead,
invoked when the new stack is closed (either explicitly or implicitly). 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" For example, a group of files can be opened as an "all or nothing"
operation as follows:: operation as follows::
with ExitStack() as stack: with ExitStack() as stack:
files = [stack.enter_context(open(fname)) for fname in filenames] 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 close_files = stack.pop_all().close
# If opening any file fails, all previously opened files will be # If opening any file fails, all previously opened files will be
# closed automatically. If all files are opened successfully, # closed automatically. If all files are opened successfully,
# they will remain open even after the with statement ends. # 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() .. method:: close()
@ -274,21 +377,6 @@ API Reference
callbacks registered, the arguments passed in will indicate that no callbacks registered, the arguments passed in will indicate that no
exception occurred. 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 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. 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 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 # We don't need to duplicate any of our resource release logic
self.release_resource() 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 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 Due to the way the decorator protocol works, a callback function
declared this way cannot take any parameters. Instead, any resources to 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 Obtaining the Module

View file

@ -1,9 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
from distutils.core import setup from distutils.core import setup
# Technically, unittest2 is a dependency to run the tests on 2.6 and 3.1 # Technically, unittest2 is a dependency to run the tests on 2.7
# This file ignores that, since I don't want to depend on distribute # This file ignores that, since I don't want to depend on
# or setuptools just to get "tests_require" support # setuptools just to get "tests_require" support
setup( setup(
name='contextlib2', name='contextlib2',
@ -14,5 +14,16 @@ setup(
long_description=open('README.md').read(), long_description=open('README.md').read(),
author='Nick Coghlan', author='Nick Coghlan',
author_email='ncoghlan@gmail.com', 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',
],
) )

View file

@ -1,14 +1,19 @@
#!/usr/bin/env python """Unit tests for contextlib2.py"""
"""Unit tests for contextlib2""" from __future__ import print_function
from __future__ import unicode_literals
import io
import sys import sys
import unittest import unittest
if not hasattr(unittest, "skipIf"): import __future__ # For PEP 479 conditional test
import unittest2 as unittest import contextlib2
from contextlib2 import * # Tests __all__ 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): class ContextManagerTestCase(unittest.TestCase):
@ -81,6 +86,44 @@ class ContextManagerTestCase(unittest.TestCase):
raise ZeroDivisionError(999) raise ZeroDivisionError(999)
self.assertEqual(state, [1, 42, 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 _create_contextmanager_attribs(self):
def attribs(**kw): def attribs(**kw):
def decorate(func): def decorate(func):
@ -99,15 +142,33 @@ class ContextManagerTestCase(unittest.TestCase):
self.assertEqual(baz.__name__,'baz') self.assertEqual(baz.__name__,'baz')
self.assertEqual(baz.foo, 'bar') self.assertEqual(baz.foo, 'bar')
@unittest.skipIf(sys.flags.optimize >= 2, @requires_docstrings
"Docstrings are omitted with -O2 and above")
def test_contextmanager_doc_attrib(self): def test_contextmanager_doc_attrib(self):
baz = self._create_contextmanager_attribs() baz = self._create_contextmanager_attribs()
self.assertEqual(baz.__doc__, "Whee!") 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): 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): def test_closing(self):
state = [] state = []
@ -135,6 +196,7 @@ class ClosingTestCase(unittest.TestCase):
class mycontext(ContextDecorator): class mycontext(ContextDecorator):
"""Example decoration-compatible context manager for testing"""
started = False started = False
exc = None exc = None
catch = False catch = False
@ -150,6 +212,13 @@ class mycontext(ContextDecorator):
class TestContextDecorator(unittest.TestCase): 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): def test_contextdecorator(self):
context = mycontext() context = mycontext()
with context as result: with context as result:
@ -162,12 +231,11 @@ class TestContextDecorator(unittest.TestCase):
def test_contextdecorator_with_exception(self): def test_contextdecorator_with_exception(self):
context = mycontext() context = mycontext()
with self.assertRaises(NameError): with self.assertRaisesRegex(NameError, 'foo'):
with context: with context:
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', str(context.exc[1]))
context = mycontext() context = mycontext()
context.catch = True context.catch = True
@ -197,11 +265,10 @@ class TestContextDecorator(unittest.TestCase):
self.assertTrue(context.started) self.assertTrue(context.started)
raise NameError('foo') raise NameError('foo')
with self.assertRaises(NameError): with self.assertRaisesRegex(NameError, 'foo'):
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', str(context.exc[1]))
def test_decorating_method(self): def test_decorating_method(self):
@ -302,9 +369,18 @@ class TestContextDecorator(unittest.TestCase):
test('something else') test('something else')
self.assertEqual(state, [1, 'something else', 999]) 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): 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): def test_no_resources(self):
with ExitStack(): with ExitStack():
pass pass
@ -416,6 +492,211 @@ class TestExitStack(unittest.TestCase):
new_stack.close() new_stack.close()
self.assertEqual(result, [1, 2, 3]) 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): def test_instance_bypass(self):
class Example(object): pass class Example(object): pass
cm = Example() cm = Example()
@ -426,128 +707,129 @@ class TestExitStack(unittest.TestCase):
self.assertIs(stack._exit_callbacks[-1], cm) self.assertIs(stack._exit_callbacks[-1], cm)
class TestContextStack(unittest.TestCase): class TestRedirectStream:
def test_no_resources(self): redirect_stream = None
with ContextStack(): orig_stream = None
pass
def test_register(self): @requires_docstrings
expected = [ def test_instance_docs(self):
((), {}), # Issue 19330: ensure context manager instances have good docstrings
((1,), {}), cm_docstring = self.redirect_stream.__doc__
((1,2), {}), obj = self.redirect_stream(None)
((), dict(example=1)), self.assertEqual(obj.__doc__, cm_docstring)
((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)
def test_register_exit(self): def test_no_redirect_in_init(self):
exc_raised = ZeroDivisionError orig_stdout = getattr(sys, self.orig_stream)
def _expect_exc(exc_type, exc, exc_tb): self.redirect_stream(None)
self.assertIs(exc_type, exc_raised) self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
def _suppress_exc(*exc_details):
return True def test_redirect_to_string_io(self):
def _expect_ok(exc_type, exc, exc_tb): f = io.StringIO()
self.assertIsNone(exc_type) msg = "Consider an API like help(), which prints directly to stdout"
self.assertIsNone(exc) orig_stdout = getattr(sys, self.orig_stream)
self.assertIsNone(exc_tb) with self.redirect_stream(f):
class ExitCM(object): print(msg, file=getattr(sys, self.orig_stream))
def __init__(self, check_exc): self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
self.check_exc = check_exc s = f.getvalue().strip()
def __enter__(self): self.assertEqual(s, msg)
self.fail("Should not be called!")
def __exit__(self, *exc_details): def test_enter_result_is_target(self):
self.check_exc(*exc_details) f = io.StringIO()
with ContextStack() as stack: with self.redirect_stream(f) as enter_result:
stack.register_exit(_expect_ok) self.assertIs(enter_result, f)
self.assertIs(stack._exit_callbacks[-1], _expect_ok)
cm = ExitCM(_expect_ok) def test_cm_is_reusable(self):
stack.register_exit(cm) f = io.StringIO()
self.assertIs(stack._exit_callbacks[-1].__self__, cm) write_to_f = self.redirect_stream(f)
stack.register_exit(_suppress_exc) orig_stdout = getattr(sys, self.orig_stream)
self.assertIs(stack._exit_callbacks[-1], _suppress_exc) with write_to_f:
cm = ExitCM(_expect_exc) print("Hello", end=" ", file=getattr(sys, self.orig_stream))
stack.register_exit(cm) with write_to_f:
self.assertIs(stack._exit_callbacks[-1].__self__, cm) print("World!", file=getattr(sys, self.orig_stream))
stack.register_exit(_expect_exc) self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
self.assertIs(stack._exit_callbacks[-1], _expect_exc) s = f.getvalue()
stack.register_exit(_expect_exc) self.assertEqual(s, "Hello World!\n")
self.assertIs(stack._exit_callbacks[-1], _expect_exc)
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 1/0
with suppress(ZeroDivisionError, TypeError):
len(5)
def test_enter_context(self): def test_cm_is_reentrant(self):
class TestCM(object): ignore_exceptions = suppress(Exception)
def __enter__(self): with ignore_exceptions:
result.append(1) pass
def __exit__(self, *exc_details): with ignore_exceptions:
result.append(3) len(5)
with ignore_exceptions:
result = [] with ignore_exceptions: # Check nested usage
cm = TestCM() len(5)
with ContextStack() as stack: outer_continued = True
@stack.register # Registered first => cleaned up last 1/0
def _exit(): self.assertTrue(outer_continued)
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__": if __name__ == "__main__":
import unittest import unittest
unittest.main(__name__) unittest.main()

View file

@ -4,3 +4,8 @@ envlist = py27, pypy, py34, py35, pypy3
[testenv] [testenv]
commands = {envpython} test_contextlib2.py commands = {envpython} test_contextlib2.py
[testenv:py27]
deps = unittest2
[testenv:pypy]
deps = unittest2