diff --git a/NEWS.rst b/NEWS.rst index 0d4d8ee..3f7572e 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,8 +1,19 @@ Release History --------------- -0.4.0 (2012-05-??) -~~~~~~~~~~~~~~~~~~ +0.5.0 (2013-??-??) +^^^^^^^^^^^^^^^^^^ + +* Updated to include all features from the upcoming Python 3.4 version 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 + + +0.4.0 (2012-05-05) +^^^^^^^^^^^^^^^^^^ * Issue #8: Replace ContextStack with ExitStack (old ContextStack API retained for backwards compatibility) @@ -10,13 +21,13 @@ Release History 0.3.1 (2012-01-17) -~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^ * Issue #7: Add MANIFEST.in so PyPI package contains all relevant files 0.3 (2012-01-04) -~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^ * Issue #5: ContextStack.register no longer pointlessly returns the wrapped function @@ -32,14 +43,14 @@ Release History 0.2 (2011-12-15) -~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^ * Renamed CleanupManager to ContextStack (hopefully before anyone started using the module for anything, since I didn't alias the old name at all) 0.1 (2011-12-13) -~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^ * Initial release as a backport module * Added CleanupManager (based on a `Python feature request`_) diff --git a/VERSION.txt b/VERSION.txt index 60a2d3e..8f0916f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.5.0 diff --git a/contextlib2.py b/contextlib2.py index 1fe5bc2..1f6edee 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -4,17 +4,31 @@ import sys from collections import deque from functools import wraps -__all__ = ["contextmanager", "closing", "ContextDecorator", - "ContextStack", "ExitStack"] +# Standard library exports +__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", + "redirect_stdout", "suppress"] +# contextlib2 only exports (current and legacy experimental interfaces) +__all__ += ["ContextStack"] class ContextDecorator(object): "A base class or mixin that enables context managers to work as decorators." + 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 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 @@ -37,6 +51,16 @@ class _GeneratorContextManager(ContextDecorator): 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): # _GCM instances are one-shot context managers, so the @@ -141,23 +165,116 @@ class closing(object): def __exit__(self, *exc_info): self.thing.close() +class redirect_stdout: + """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) + """ + + def __init__(self, new_target): + self._new_target = new_target + self._old_target = self._sentinel = object() + + def __enter__(self): + if self._old_target is not self._sentinel: + raise RuntimeError("Cannot reenter {!r}".format(self)) + self._old_target = sys.stdout + sys.stdout = self._new_target + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + restore_stdout = self._old_target + self._old_target = self._sentinel + sys.stdout = restore_stdout + + + +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 +if str is not bytes: + def _make_context_fixer(frame_exc): + def _fix_exception_context(new_exc, old_exc): + while 1: + exc_context = new_exc.__context__ + if exc_context in (None, frame_exc): + break + new_exc = exc_context + 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 +288,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 +311,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 +324,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,35 +343,33 @@ 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): diff --git a/docs/index.rst b/docs/index.rst index f4ea3ce..1fd4658 100644 --- a/docs/index.rst +++ b/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.4 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 include 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 - 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,86 @@ 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 `. + + .. 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:`reusable but not reentrant `. + + .. versionadded:: 0.5 + Part of the standard library in Python 3.4 and later .. class:: ContextDecorator() @@ -112,7 +194,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 +256,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 +276,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 +309,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 +337,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 +366,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 +376,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 +414,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 +488,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 diff --git a/test_contextlib2.py b/test_contextlib2.py index 86cf15e..2b9d315 100755 --- a/test_contextlib2.py +++ b/test_contextlib2.py @@ -1,17 +1,51 @@ #!/usr/bin/env python """Unit tests for contextlib2""" +import io import sys +# Check for a sufficiently recent unittest module import unittest if not hasattr(unittest, "skipIf"): import unittest2 as unittest +# Handle 2/3 compatibility for redirect_stdout testing, +# checking 3.x only implicit exception chaining behaviour +# and checking for exception details in test cases +if str is bytes: + # Python 2 + from io import BytesIO as StrIO + check_exception_chaining = False + + def check_exception_details(case, exc_type, regex): + return case.assertRaisesRegexp(exc_type, regex) +else: + # Python 3 + from io import StringIO as StrIO + check_exception_chaining = True + + def check_exception_details(case, exc_type, regex): + return case.assertRaisesRegex(exc_type, regex) + + from contextlib2 import * # Tests __all__ - class ContextManagerTestCase(unittest.TestCase): + def test_instance_docstring_given_function_docstring(self): + # Issue 19330: ensure context manager instances have good docstrings + # See http://bugs.python.org/issue19404 for why this doesn't currently + # affect help() output :( + def gen_with_docstring(): + """This has a docstring""" + yield + gen_docstring = gen_with_docstring.__doc__ + cm_with_docstring = contextmanager(gen_with_docstring) + self.assertEqual(cm_with_docstring.__doc__, gen_docstring) + obj = cm_with_docstring() + self.assertEqual(obj.__doc__, gen_docstring) + self.assertNotEqual(obj.__doc__, type(obj).__doc__) + def test_contextmanager_plain(self): state = [] @contextmanager @@ -107,7 +141,11 @@ class ContextManagerTestCase(unittest.TestCase): class ClosingTestCase(unittest.TestCase): - # XXX This needs more work + 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 +173,7 @@ class ClosingTestCase(unittest.TestCase): class mycontext(ContextDecorator): + """Example decoration-compatible context manager for testing""" started = False exc = None catch = False @@ -150,6 +189,12 @@ class mycontext(ContextDecorator): class TestContextDecorator(unittest.TestCase): + 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 +207,11 @@ class TestContextDecorator(unittest.TestCase): def test_contextdecorator_with_exception(self): context = mycontext() - with self.assertRaises(NameError): + with check_exception_details(self, 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 +241,10 @@ class TestContextDecorator(unittest.TestCase): self.assertTrue(context.started) raise NameError('foo') - with self.assertRaises(NameError): + with check_exception_details(self, 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): @@ -305,6 +348,12 @@ class TestContextDecorator(unittest.TestCase): class TestExitStack(unittest.TestCase): + 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 +465,154 @@ 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 + # We still run this under Py2, but the only thing it actually + # checks in that case is that the outermost exception is IndexError + # and that the inner ValueError was suppressed + class RaiseExc(object): + def __init__(self, exc): + self.exc = exc + def __enter__(self): + return self + def __exit__(self, *exc_details): + raise self.exc + + class RaiseExcWithContext(object): + 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(object): + def __enter__(self): + return self + def __exit__(self, *exc_details): + type(self).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 + # We still run this under Py2, but the only thing it actually + # checks in that case is that the outermost exception is IndexError + # and that the inner ValueError was suppressed + def raise_exc(exc): + raise exc + + saved_details = [] + def suppress_exc(*exc_details): + saved_details[:] = [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_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() @@ -425,9 +622,102 @@ class TestExitStack(unittest.TestCase): stack.push(cm) self.assertIs(stack._exit_callbacks[-1], cm) +class TestRedirectStdout(unittest.TestCase): + + def test_instance_docs(self): + # Issue 19330: ensure context manager instances have good docstrings + cm_docstring = redirect_stdout.__doc__ + obj = redirect_stdout(None) + self.assertEqual(obj.__doc__, cm_docstring) + + def test_redirect_to_string_io(self): + f = StrIO() + msg = "Consider an API like help(), which prints directly to stdout" + with redirect_stdout(f): + print(msg) + s = f.getvalue().strip() + self.assertEqual(s, msg) + + def test_enter_result_is_target(self): + f = StrIO() + with redirect_stdout(f) as enter_result: + self.assertIs(enter_result, f) + + def test_cm_is_reusable(self): + f = StrIO() + write_to_f = redirect_stdout(f) + with write_to_f: + print("Hello") + with write_to_f: + print("World!") + s = f.getvalue() + self.assertEqual(s, "Hello\nWorld!\n") + + # If this is ever made reentrant, update the reusable-but-not-reentrant + # example at the end of the contextlib docs accordingly. + def test_nested_reentry_fails(self): + f = StrIO() + write_to_f = redirect_stdout(f) + with check_exception_details(self, RuntimeError, "Cannot reenter"): + with write_to_f: + print("Hello") + with write_to_f: + print("World!") + + +class TestSuppress(unittest.TestCase): + + 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_cm_is_reentrant(self): + ignore_exceptions = suppress(Exception) + with ignore_exceptions: + pass + with ignore_exceptions: + len(5) + with ignore_exceptions: + 1/0 + with ignore_exceptions: # Check nested usage + len(5) class TestContextStack(unittest.TestCase): - + def test_no_resources(self): with ContextStack(): pass @@ -547,7 +837,6 @@ class TestContextStack(unittest.TestCase): 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()