Skip to content

Commit

Permalink
Merged in dev (pull request #6)
Browse files Browse the repository at this point in the history
Rebase on Python 3.5 and prep 0.5.0 release
  • Loading branch information
Nick Coghlan committed Jan 12, 2016
2 parents 40f3598 + 3559e37 commit f931699
Show file tree
Hide file tree
Showing 8 changed files with 964 additions and 293 deletions.
1 change: 1 addition & 0 deletions .hgignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ htmlcov/
_build/
dist/
MANIFEST
.tox
13 changes: 13 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Release History
---------------

0.5.0 (2016-01-12)
~~~~~~~~~~~~~~~~~~

* Updated to include all features from the Python 3.4 and 3.5 releases of
contextlib (also includes some ``ExitStack`` enhancements made following
the integration into the standard library for Python 3.3)

* The legacy ``ContextStack`` and ``ContextDecorator.refresh_cm`` APIs are
no longer documented and emit ``DeprecationWarning`` when used

* Python 2.6, 3.2 and 3.3 have been dropped from compatibility testing


0.4.0 (2012-05-05)
~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.0
0.5.0
245 changes: 197 additions & 48 deletions contextlib2.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,78 @@
"""contextlib2 - backports and enhancements to the contextlib module"""

import sys
import warnings
from collections import deque
from functools import wraps

__all__ = ["contextmanager", "closing", "ContextDecorator",
"ContextStack", "ExitStack"]
__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
"redirect_stdout", "redirect_stderr", "suppress"]

# Backwards compatibility
__all__ += ["ContextStack"]

class ContextDecorator(object):
"A base class or mixin that enables context managers to work as decorators."

def refresh_cm(self):
"""Returns the context manager used to actually wrap the call to the
decorated function.
The default implementation just returns *self*.
Overriding this method allows otherwise one-shot context managers
like _GeneratorContextManager to support use as decorators via
implicit recreation.
DEPRECATED: refresh_cm was never added to the standard library's
ContextDecorator API
"""
warnings.warn("refresh_cm was never added to the standard library",
DeprecationWarning)
return self._recreate_cm()

def _recreate_cm(self):
"""Return a recreated instance of self.
Allows an otherwise one-shot context manager like
_GeneratorContextManager to support use as
a decorator via implicit recreation.
This is a private interface just for _GeneratorContextManager.
See issue #11647 for details.
"""
return self

def __call__(self, func):
@wraps(func)
def inner(*args, **kwds):
with self.refresh_cm():
with self._recreate_cm():
return func(*args, **kwds)
return inner


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

def refresh_cm(self):
# 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 _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:
Expand All @@ -67,10 +97,17 @@ def __exit__(self, type, value, traceback):
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
Expand Down Expand Up @@ -113,7 +150,7 @@ def some_generator(<arguments>):
"""
@wraps(func)
def helper(*args, **kwds):
return _GeneratorContextManager(func, *args, **kwds)
return _GeneratorContextManager(func, args, kwds)
return helper


Expand Down Expand Up @@ -142,22 +179,131 @@ def __exit__(self, *exc_info):
self.thing.close()


class _RedirectStream:

_stream = None

def __init__(self, new_target):
self._new_target = new_target
# We use a list of old targets to make this CM re-entrant
self._old_targets = []

def __enter__(self):
self._old_targets.append(getattr(sys, self._stream))
setattr(sys, self._stream, self._new_target)
return self._new_target

def __exit__(self, exctype, excinst, exctb):
setattr(sys, self._stream, self._old_targets.pop())


class redirect_stdout(_RedirectStream):
"""Context manager for temporarily redirecting stdout to another file.
# How to send help() to stderr
with redirect_stdout(sys.stderr):
help(dir)
# How to write help() to a file
with open('help.txt', 'w') as f:
with redirect_stdout(f):
help(pow)
"""

_stream = "stdout"


class redirect_stderr(_RedirectStream):
"""Context manager for temporarily redirecting stderr to another file."""

_stream = "stderr"


class suppress:
"""Context manager to suppress specified exceptions
After the exception is suppressed, execution proceeds with the next
statement following the with statement.
with suppress(FileNotFoundError):
os.remove(somefile)
# Execution still resumes here if the file was already removed
"""

def __init__(self, *exceptions):
self._exceptions = exceptions

def __enter__(self):
pass

def __exit__(self, exctype, excinst, exctb):
# Unlike isinstance and issubclass, CPython exception handling
# currently only looks at the concrete type hierarchy (ignoring
# the instance and subclass checking hooks). While Guido considers
# that a bug rather than a feature, it's a fairly hard one to fix
# due to various internal implementation details. suppress provides
# the simpler issubclass based semantics, rather than trying to
# exactly reproduce the limitations of the CPython interpreter.
#
# See http://bugs.python.org/issue12029 for more details
return exctype is not None and issubclass(exctype, self._exceptions)


# Context manipulation is Python 3 only
_HAVE_EXCEPTION_CHAINING = sys.version_info.major >= 3
if _HAVE_EXCEPTION_CHAINING:
def _make_context_fixer(frame_exc):
def _fix_exception_context(new_exc, old_exc):
# Context may not be correct, so find the end of the chain
while 1:
exc_context = new_exc.__context__
if exc_context is old_exc:
# Context is already set correctly (see issue 20317)
return
if exc_context is None or exc_context is frame_exc:
break
new_exc = exc_context
# Change the end of the chain to point to the exception
# we expect it to reference
new_exc.__context__ = old_exc
return _fix_exception_context

def _reraise_with_existing_context(exc_details):
try:
# bare "raise exc_details[1]" replaces our carefully
# set-up context
fixed_ctx = exc_details[1].__context__
raise exc_details[1]
except BaseException:
exc_details[1].__context__ = fixed_ctx
raise
else:
# No exception context in Python 2
def _make_context_fixer(frame_exc):
return lambda new_exc, old_exc: None

# Use 3 argument raise in Python 2,
# but use exec to avoid SyntaxError in Python 3
def _reraise_with_existing_context(exc_details):
exc_type, exc_value, exc_tb = exc_details
exec ("raise exc_type, exc_value, exc_tb")

# Inspired by discussions on http://bugs.python.org/issue13585
class ExitStack(object):
"""Context manager for dynamic management of a stack of exit callbacks
For example:
with ExitStack() as stack:
files = [stack.enter_context(open(fname)) for fname in filenames]
# All opened files will automatically be closed at the end of
# the with statement, even if attempts to open files later
# in the list throw an exception
# in the list raise an exception
"""
def __init__(self):
self._exit_callbacks = deque()

def pop_all(self):
"""Preserve the context stack by transferring it to a new instance"""
new_stack = type(self)()
Expand All @@ -171,14 +317,14 @@ def _exit_wrapper(*exc_details):
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
Expand All @@ -194,7 +340,7 @@ def push(self, exit):

def callback(self, callback, *args, **kwds):
"""Registers an arbitrary callback and arguments.
Cannot suppress exceptions.
"""
def _exit_wrapper(exc_type, exc, tb):
Expand All @@ -207,7 +353,7 @@ def _exit_wrapper(exc_type, exc, tb):

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.
"""
Expand All @@ -226,40 +372,43 @@ def __enter__(self):
return self

def __exit__(self, *exc_details):
if not self._exit_callbacks:
return
# This looks complicated, but it is really just
# setting up a chain of try-expect statements to ensure
# that outer callbacks still get invoked even if an
# inner one throws an exception
def _invoke_next_callback(exc_details):
# Callbacks are removed from the list in FIFO order
# but the recursion means they're invoked in LIFO order
cb = self._exit_callbacks.popleft()
if not self._exit_callbacks:
# Innermost callback is invoked directly
return cb(*exc_details)
# More callbacks left, so descend another level in the stack
received_exc = exc_details[0] is not None

# We manipulate the exception state so it behaves as though
# we were actually nesting multiple with statements
frame_exc = sys.exc_info()[1]
_fix_exception_context = _make_context_fixer(frame_exc)

# Callbacks are invoked in LIFO order to match the behaviour of
# nested context managers
suppressed_exc = False
pending_raise = False
while self._exit_callbacks:
cb = self._exit_callbacks.pop()
try:
suppress_exc = _invoke_next_callback(exc_details)
except:
suppress_exc = cb(*sys.exc_info())
# Check if this cb suppressed the inner exception
if not suppress_exc:
raise
else:
# Check if inner cb suppressed the original exception
if suppress_exc:
if cb(*exc_details):
suppressed_exc = True
pending_raise = False
exc_details = (None, None, None)
suppress_exc = cb(*exc_details) or suppress_exc
return suppress_exc
# Kick off the recursive chain
return _invoke_next_callback(exc_details)
except:
new_exc_details = sys.exc_info()
# simulate the stack of exceptions by setting the context
_fix_exception_context(new_exc_details[1], exc_details[1])
pending_raise = True
exc_details = new_exc_details
if pending_raise:
_reraise_with_existing_context(exc_details)
return received_exc and suppressed_exc

# Preserve backwards compatibility
class ContextStack(ExitStack):
"""Backwards compatibility alias for ExitStack"""

def __init__(self):
warnings.warn("ContextStack has been renamed to ExitStack",
DeprecationWarning)
super(ContextStack, self).__init__()

def register_exit(self, callback):
return self.push(callback)

Expand Down

0 comments on commit f931699

Please sign in to comment.