From 30515dbe4f21fdeb4cee35e753de663680f4f29b Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Sat, 26 Oct 2013 21:42:03 +1000 Subject: [PATCH 1/8] Update to Python 3.4 alpha version --- NEWS.rst | 23 ++- VERSION.txt | 2 +- contextlib2.py | 195 +++++++++++++++----- docs/index.rst | 421 ++++++++++++++++++++++++++++++++------------ test_contextlib2.py | 309 ++++++++++++++++++++++++++++++-- 5 files changed, 782 insertions(+), 168 deletions(-) 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() From f1d447328a9bf68b27e05be373b82a80c8cdeb38 Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Sat, 26 Oct 2013 22:00:05 +1000 Subject: [PATCH 2/8] 2.6 compatibility --- contextlib2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contextlib2.py b/contextlib2.py index 1f6edee..acb093b 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -184,7 +184,7 @@ class redirect_stdout: def __enter__(self): if self._old_target is not self._sentinel: - raise RuntimeError("Cannot reenter {!r}".format(self)) + raise RuntimeError("Cannot reenter {0!r}".format(self)) self._old_target = sys.stdout sys.stdout = self._new_target return self._new_target From 4199bb9d9d8b8e4070f98801e27add0ccc89ae0f Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Tue, 12 Jan 2016 18:50:35 +1000 Subject: [PATCH 3/8] Ignore tox directory --- .hgignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.hgignore b/.hgignore index 37ca46e..0a06a5d 100644 --- a/.hgignore +++ b/.hgignore @@ -13,4 +13,5 @@ coverage/ htmlcov/ _build/ dist/ -MANIFEST \ No newline at end of file +MANIFEST +.tox From 939293da14f3b3f2f8a9097801a8813d8c08fb4e Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Tue, 12 Jan 2016 18:51:30 +1000 Subject: [PATCH 4/8] Tox support on dev branch --- tox.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..668a89d --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27, pypy, py34, py35, pypy3 + +[testenv] +commands = {envpython} test_contextlib2.py + +[testenv:py27] +deps = unittest2 + +[testenv:pypy] +deps = unittest2 From 6d77165d634288187d682039d222f2acf23f2d74 Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Tue, 12 Jan 2016 18:51:33 +1000 Subject: [PATCH 5/8] Rebase on Python 3.5 contextlib --- contextlib2.py | 112 ++++++++----- test_contextlib2.py | 393 ++++++++++++++++++++++---------------------- 2 files changed, 266 insertions(+), 239 deletions(-) diff --git a/contextlib2.py b/contextlib2.py index acb093b..a618f23 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -1,30 +1,19 @@ """contextlib2 - backports and enhancements to the contextlib module""" import sys +import warnings from collections import deque from functools import wraps -# Standard library exports __all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", - "redirect_stdout", "suppress"] + "redirect_stdout", "redirect_stderr", "suppress"] -# contextlib2 only exports (current and legacy experimental interfaces) +# Backwards compatibility __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. @@ -34,13 +23,30 @@ class ContextDecorator(object): Overriding this method allows otherwise one-shot context managers like _GeneratorContextManager to support use as decorators via implicit recreation. + + DEPRECATED: refresh_cm was never added to the standard library's + ContextDecorator API + """ + warnings.warn("refresh_cm was never added to the standard library", + DeprecationWarning) + return self._recreate_cm() + + def _recreate_cm(self): + """Return a recreated instance of self. + + Allows an otherwise one-shot context manager like + _GeneratorContextManager to support use as + a decorator via implicit recreation. + + This is a private interface just for _GeneratorContextManager. + See issue #11647 for details. """ return self def __call__(self, func): @wraps(func) def inner(*args, **kwds): - with self.refresh_cm(): + with self._recreate_cm(): return func(*args, **kwds) return inner @@ -48,7 +54,7 @@ class ContextDecorator(object): class _GeneratorContextManager(ContextDecorator): """Helper for @contextmanager decorator.""" - def __init__(self, func, *args, **kwds): + def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) self.func, self.args, self.kwds = func, args, kwds # Issue 19330: ensure context manager instances have good docstrings @@ -62,11 +68,11 @@ class _GeneratorContextManager(ContextDecorator): # for the class instead. # See http://bugs.python.org/issue19404 for more details. - def refresh_cm(self): + def _recreate_cm(self): # _GCM instances are one-shot context managers, so the # CM must be recreated each time a decorated function is # called - return self.__class__(self.func, *self.args, **self.kwds) + return self.__class__(self.func, self.args, self.kwds) def __enter__(self): try: @@ -91,10 +97,17 @@ class _GeneratorContextManager(ContextDecorator): self.gen.throw(type, value, traceback) raise RuntimeError("generator didn't stop after throw()") except StopIteration as exc: - # Suppress the exception *unless* it's the same exception that + # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration - # raised inside the "with" statement from being suppressed + # raised inside the "with" statement from being suppressed. return exc is not value + except RuntimeError as exc: + # Likewise, avoid suppressing if a StopIteration exception + # was passed to throw() and later wrapped into a RuntimeError + # (see PEP 479). + if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: + return False + raise except: # only re-raise if it's *not* the exception that was # passed to throw(), because __exit__() must not raise @@ -137,7 +150,7 @@ def contextmanager(func): """ @wraps(func) def helper(*args, **kwds): - return _GeneratorContextManager(func, *args, **kwds) + return _GeneratorContextManager(func, args, kwds) return helper @@ -165,8 +178,27 @@ class closing(object): def __exit__(self, *exc_info): self.thing.close() -class redirect_stdout: - """Context manager for temporarily redirecting stdout to another file + +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): @@ -178,22 +210,13 @@ class redirect_stdout: help(pow) """ - def __init__(self, new_target): - self._new_target = new_target - self._old_target = self._sentinel = object() + _stream = "stdout" - def __enter__(self): - if self._old_target is not self._sentinel: - raise RuntimeError("Cannot reenter {0!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 redirect_stderr(_RedirectStream): + """Context manager for temporarily redirecting stderr to another file.""" + _stream = "stderr" class suppress: @@ -227,14 +250,21 @@ class suppress: # Context manipulation is Python 3 only -if str is not bytes: +_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 in (None, frame_exc): + 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 @@ -258,7 +288,6 @@ else: 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 @@ -375,6 +404,11 @@ class ExitStack(object): class ContextStack(ExitStack): """Backwards compatibility alias for ExitStack""" + def __init__(self): + warnings.warn("ContextStack has been renamed to ExitStack", + DeprecationWarning) + super(ContextStack, self).__init__() + def register_exit(self, callback): return self.push(callback) diff --git a/test_contextlib2.py b/test_contextlib2.py index 2b9d315..3a12585 100755 --- a/test_contextlib2.py +++ b/test_contextlib2.py @@ -1,50 +1,21 @@ -#!/usr/bin/env python -"""Unit tests for contextlib2""" +"""Unit tests for contextlib2.py""" +from __future__ import print_function +from __future__ import unicode_literals import io import sys - -# 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) - - +import __future__ # For PEP 479 conditional test +import contextlib2 from contextlib2 import * # Tests __all__ -class ContextManagerTestCase(unittest.TestCase): +if not hasattr(unittest.TestCase, "assertRaisesRegex"): + import unittest2 as unittest - 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__) +requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, + "Test requires docstrings") + +class ContextManagerTestCase(unittest.TestCase): def test_contextmanager_plain(self): state = [] @@ -115,6 +86,44 @@ class ContextManagerTestCase(unittest.TestCase): raise ZeroDivisionError(999) self.assertEqual(state, [1, 42, 999]) + def test_contextmanager_except_stopiter(self): + stop_exc = StopIteration('spam') + @contextmanager + def woohoo(): + yield + try: + with self.assertWarnsRegex(PendingDeprecationWarning, + "StopIteration"): + with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail('StopIteration was suppressed') + + @unittest.skipUnless(hasattr(__future__, "generator_stop"), + "Test only valid for versions implementing PEP 479") + def test_contextmanager_except_pep479(self): + code = """\ +from __future__ import generator_stop +from contextlib import contextmanager +@contextmanager +def woohoo(): + yield +""" + locals = {} + exec(code, locals, locals) + woohoo = locals['woohoo'] + + stop_exc = StopIteration('spam') + try: + with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail('StopIteration was suppressed') + def _create_contextmanager_attribs(self): def attribs(**kw): def decorate(func): @@ -133,14 +142,28 @@ class ContextManagerTestCase(unittest.TestCase): self.assertEqual(baz.__name__,'baz') self.assertEqual(baz.foo, 'bar') - @unittest.skipIf(sys.flags.optimize >= 2, - "Docstrings are omitted with -O2 and above") + @requires_docstrings def test_contextmanager_doc_attrib(self): baz = self._create_contextmanager_attribs() self.assertEqual(baz.__doc__, "Whee!") + @requires_docstrings + def test_instance_docstring_given_cm_docstring(self): + baz = self._create_contextmanager_attribs()(None) + self.assertEqual(baz.__doc__, "Whee!") + + def test_keywords(self): + # Ensure no keyword arguments are inhibited + @contextmanager + def woohoo(self, func, args, kwds): + yield (self, func, args, kwds) + with woohoo(self=11, func=22, args=33, kwds=44) as target: + self.assertEqual(target, (11, 22, 33, 44)) + + class ClosingTestCase(unittest.TestCase): + @requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = closing.__doc__ @@ -189,6 +212,7 @@ class mycontext(ContextDecorator): class TestContextDecorator(unittest.TestCase): + @requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = mycontext.__doc__ @@ -207,7 +231,7 @@ class TestContextDecorator(unittest.TestCase): def test_contextdecorator_with_exception(self): context = mycontext() - with check_exception_details(self, NameError, 'foo'): + with self.assertRaisesRegex(NameError, 'foo'): with context: raise NameError('foo') self.assertIsNotNone(context.exc) @@ -241,7 +265,7 @@ class TestContextDecorator(unittest.TestCase): self.assertTrue(context.started) raise NameError('foo') - with check_exception_details(self, NameError, 'foo'): + with self.assertRaisesRegex(NameError, 'foo'): test() self.assertIsNotNone(context.exc) self.assertIs(context.exc[0], NameError) @@ -345,9 +369,12 @@ class TestContextDecorator(unittest.TestCase): test('something else') self.assertEqual(state, [1, 'something else', 999]) +# Detailed exception chaining checks only make sense on Python 3 +check_exception_chaining = contextlib2._HAVE_EXCEPTION_CHAINING class TestExitStack(unittest.TestCase): + @requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = ExitStack.__doc__ @@ -479,10 +506,7 @@ class TestExitStack(unittest.TestCase): 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): + class RaiseExc: def __init__(self, exc): self.exc = exc def __enter__(self): @@ -490,7 +514,7 @@ class TestExitStack(unittest.TestCase): def __exit__(self, *exc_details): raise self.exc - class RaiseExcWithContext(object): + class RaiseExcWithContext: def __init__(self, outer, inner): self.outer = outer self.inner = inner @@ -502,11 +526,11 @@ class TestExitStack(unittest.TestCase): except: raise self.outer - class SuppressExc(object): + class SuppressExc: def __enter__(self): return self def __exit__(self, *exc_details): - type(self).saved_details = exc_details + self.__class__.saved_details = exc_details return True try: @@ -531,15 +555,12 @@ class TestExitStack(unittest.TestCase): 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 = [] + saved_details = [None] def suppress_exc(*exc_details): - saved_details[:] = [exc_details] + saved_details[0] = exc_details return True try: @@ -591,6 +612,69 @@ class TestExitStack(unittest.TestCase): 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 @@ -622,51 +706,79 @@ class TestExitStack(unittest.TestCase): stack.push(cm) self.assertIs(stack._exit_callbacks[-1], cm) -class TestRedirectStdout(unittest.TestCase): +class TestRedirectStream: + + redirect_stream = None + orig_stream = None + + @requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings - cm_docstring = redirect_stdout.__doc__ - obj = redirect_stdout(None) + cm_docstring = self.redirect_stream.__doc__ + obj = self.redirect_stream(None) self.assertEqual(obj.__doc__, cm_docstring) + def test_no_redirect_in_init(self): + orig_stdout = getattr(sys, self.orig_stream) + self.redirect_stream(None) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) + def test_redirect_to_string_io(self): - f = StrIO() + f = io.StringIO() msg = "Consider an API like help(), which prints directly to stdout" - with redirect_stdout(f): - print(msg) + orig_stdout = getattr(sys, self.orig_stream) + with self.redirect_stream(f): + print(msg, file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) s = f.getvalue().strip() self.assertEqual(s, msg) def test_enter_result_is_target(self): - f = StrIO() - with redirect_stdout(f) as enter_result: + f = io.StringIO() + with self.redirect_stream(f) as enter_result: self.assertIs(enter_result, f) def test_cm_is_reusable(self): - f = StrIO() - write_to_f = redirect_stdout(f) + f = io.StringIO() + write_to_f = self.redirect_stream(f) + orig_stdout = getattr(sys, self.orig_stream) with write_to_f: - print("Hello") + print("Hello", end=" ", file=getattr(sys, self.orig_stream)) with write_to_f: - print("World!") + print("World!", file=getattr(sys, self.orig_stream)) + self.assertIs(getattr(sys, self.orig_stream), orig_stdout) s = f.getvalue() - self.assertEqual(s, "Hello\nWorld!\n") + self.assertEqual(s, "Hello World!\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"): + 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("Hello") - with write_to_f: - print("World!") + 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__ @@ -712,131 +824,12 @@ class TestSuppress(unittest.TestCase): 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 - - def test_register(self): - expected = [ - ((), {}), - ((1,), {}), - ((1,2), {}), - ((), dict(example=1)), - ((1,), dict(example=1)), - ((1,2), dict(example=1)), - ] - result = [] - def _exit(*args, **kwds): - """Test metadata propagation""" - result.append((args, kwds)) - with ContextStack() as stack: - for args, kwds in reversed(expected): - if args and kwds: - f = stack.register(_exit, *args, **kwds) - elif args: - f = stack.register(_exit, *args) - elif kwds: - f = stack.register(_exit, **kwds) - else: - f = stack.register(_exit) - self.assertIs(f, _exit) - for wrapper in stack._exit_callbacks: - self.assertIs(wrapper.__wrapped__, _exit) - self.assertNotEqual(wrapper.__name__, _exit.__name__) - self.assertIsNone(wrapper.__doc__, _exit.__doc__) - self.assertEqual(result, expected) - - def test_register_exit(self): - exc_raised = ZeroDivisionError - def _expect_exc(exc_type, exc, exc_tb): - self.assertIs(exc_type, exc_raised) - def _suppress_exc(*exc_details): - return True - def _expect_ok(exc_type, exc, exc_tb): - self.assertIsNone(exc_type) - self.assertIsNone(exc) - self.assertIsNone(exc_tb) - class ExitCM(object): - def __init__(self, check_exc): - self.check_exc = check_exc - def __enter__(self): - self.fail("Should not be called!") - def __exit__(self, *exc_details): - self.check_exc(*exc_details) - with ContextStack() as stack: - stack.register_exit(_expect_ok) - self.assertIs(stack._exit_callbacks[-1], _expect_ok) - cm = ExitCM(_expect_ok) - stack.register_exit(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) - stack.register_exit(_suppress_exc) - self.assertIs(stack._exit_callbacks[-1], _suppress_exc) - cm = ExitCM(_expect_exc) - stack.register_exit(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) - stack.register_exit(_expect_exc) - self.assertIs(stack._exit_callbacks[-1], _expect_exc) - stack.register_exit(_expect_exc) - self.assertIs(stack._exit_callbacks[-1], _expect_exc) + outer_continued = True 1/0 - - def test_enter_context(self): - class TestCM(object): - def __enter__(self): - result.append(1) - def __exit__(self, *exc_details): - result.append(3) - - result = [] - cm = TestCM() - with ContextStack() as stack: - @stack.register # Registered first => cleaned up last - def _exit(): - result.append(4) - self.assertIsNotNone(_exit) - stack.enter_context(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) - result.append(2) - self.assertEqual(result, [1, 2, 3, 4]) - - def test_close(self): - result = [] - with ContextStack() as stack: - @stack.register - def _exit(): - result.append(1) - self.assertIsNotNone(_exit) - stack.close() - result.append(2) - self.assertEqual(result, [1, 2]) - - def test_preserve(self): - result = [] - with ContextStack() as stack: - @stack.register - def _exit(): - result.append(3) - self.assertIsNotNone(_exit) - new_stack = stack.preserve() - result.append(1) - result.append(2) - new_stack.close() - self.assertEqual(result, [1, 2, 3]) - - def test_instance_bypass(self): - class Example(object): pass - cm = Example() - cm.__exit__ = object() - stack = ContextStack() - self.assertRaises(AttributeError, stack.enter_context, cm) - stack.register_exit(cm) - self.assertIs(stack._exit_callbacks[-1], cm) + self.assertTrue(outer_continued) if __name__ == "__main__": + import unittest unittest.main() From b5db8d23a44b9a7a275685dd40c038c0320ca98c Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Tue, 12 Jan 2016 20:54:00 +1000 Subject: [PATCH 6/8] Update NEWS and docs for 0.5.0 --- NEWS.rst | 4 ++-- docs/index.rst | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 3f7572e..fac21c5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,10 +1,10 @@ Release History --------------- -0.5.0 (2013-??-??) +0.5.0 (2016-01-12) ^^^^^^^^^^^^^^^^^^ -* Updated to include all features from the upcoming Python 3.4 version of +* 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) diff --git a/docs/index.rst b/docs/index.rst index 1fd4658..cd32393 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,14 +21,14 @@ involving the ``with`` statement. Additions Relative to the Standard Library ------------------------------------------ -This module is primarily a backport of the Python 3.4 version of +This module is primarily a backport of the Python 3.5 version of :mod:`contextlib` to earlier releases. However, it is also a proving ground for new features not yet part of the standard library. 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. +versions of Python 3 introduce the various APIs provided in this module. API Reference @@ -183,12 +183,23 @@ Functions and classes provided: 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 `. + This context manager is :ref:`reentrant `. .. 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 `. + + .. versionadded:: 0.5 + Part of the standard library in Python 3.5 and later + + .. class:: ContextDecorator() A base class that enables a context manager to also be used as a decorator. From 6820fad0491576636709ecc9f42b76a37e18b1b6 Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Tue, 12 Jan 2016 20:59:26 +1000 Subject: [PATCH 7/8] Note compatibility testing changes --- NEWS.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index fac21c5..c06803f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -11,6 +11,8 @@ Release History * 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) ^^^^^^^^^^^^^^^^^^ From fc66300a9039e401a716dee202e63ccc66180433 Mon Sep 17 00:00:00 2001 From: Nick Coghlan <@ncoghlan> Date: Tue, 12 Jan 2016 21:08:23 +1000 Subject: [PATCH 8/8] Sync setup and README with default --- MANIFEST.in | 2 +- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ README.txt | 3 --- setup.py | 23 +++++++++++++++++------ 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 README.md delete mode 100644 README.txt diff --git a/MANIFEST.in b/MANIFEST.in index 9e664ba..6722552 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include *.py *.txt *.rst +include *.py *.txt *.rst *.md recursive-include docs *.rst *.py make.bat Makefile diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b2ba0a --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +contextlib2 is a backport of the [standard library's contextlib +module](https://docs.python.org/3.5/library/contextlib.html) to +earlier Python versions. + +It also serves as a real world proving ground for possible future +enhancements to the standard library version. + +Development +----------- + +contextlib2 currently has no dependencies. + +Local testing is currently just a matter of running `python test_contextlib2.py`. + +You can test against multiple versions of Python with [tox](http://tox.testrun.org/)): + + pip install tox + tox + +Versions currently tested in tox are: + +* CPython 2.7 (also tested in Codeship) +* CPython 3.4 (also tested in Codeship) +* CPython 3.5 +* PyPy +* PyPy3 + +To install all the relevant runtimes on Fedora 23: + + sudo dnf install python python3 pypy pypy3 + sudo dnf copr enable -y mstuchli/Python3.5 + sudo dnf install python35-python3 + +Continuous integration +---------------------- + +CI is set up in Codeship to run against PRs and commits. + +[![Codeship Status for ncoghlan/contextlib2](https://codeship.com/projects/884e9500-3d1a-0133-3eb0-1abe7f570a4c/status?branch=default)](https://codeship.com/projects/102388) +[![codecov.io](https://codecov.io/bitbucket/ncoghlan/contextlib2/coverage.svg?branch=default)](https://codecov.io/bitbucket/ncoghlan/contextlib2?branch=default) diff --git a/README.txt b/README.txt deleted file mode 100644 index 3bf4588..0000000 --- a/README.txt +++ /dev/null @@ -1,3 +0,0 @@ -contextlib2 is a backport of the standard library's contextlib module to earlier Python versions. - -It also serves as a real world proving ground for possible future enhancements to the standard library version. diff --git a/setup.py b/setup.py index b172c59..a8560d4 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ #!/usr/bin/env python from distutils.core import setup -# Technically, unittest2 is a dependency to run the tests on 2.6 and 3.1 -# This file ignores that, since I don't want to depend on distribute -# or setuptools just to get "tests_require" support +# Technically, unittest2 is a dependency to run the tests on 2.7 +# This file ignores that, since I don't want to depend on +# setuptools just to get "tests_require" support setup( name='contextlib2', @@ -11,8 +11,19 @@ setup( py_modules=['contextlib2'], license='PSF License', description='Backports and enhancements for the contextlib module', - long_description=open('README.txt').read(), + long_description=open('README.md').read(), author='Nick Coghlan', author_email='ncoghlan@gmail.com', - url='http://contextlib2.readthedocs.org' -) + url='http://contextlib2.readthedocs.org', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'License :: OSI Approved :: Python Software Foundation License', + # These are the Python versions tested, it may work on others + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + +) \ No newline at end of file