From 17b0a93e5b28f1be71a9c3456e3d9c6b1fdcb644 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 16:36:23 +1000 Subject: [PATCH 1/7] Issue #12: sync contextlib from Python 3.10 --- contextlib2.py | 546 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 411 insertions(+), 135 deletions(-) diff --git a/contextlib2.py b/contextlib2.py index a70e8cd..3599644 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -3,15 +3,15 @@ import abc import sys import warnings +import _collections_abc from collections import deque from functools import wraps +from types import MethodType, GenericAlias -from _collections_abc import _check_methods - -__all__ = ["contextmanager", "closing", "nullcontext", - "AbstractContextManager", - "ContextDecorator", "ExitStack", - "redirect_stdout", "redirect_stderr", "suppress"] +__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", + "AbstractContextManager", "AbstractAsyncContextManager", + "AsyncExitStack", "ContextDecorator", "ExitStack", + "redirect_stdout", "redirect_stderr", "suppress", "aclosing"] # Backwards compatibility __all__ += ["ContextStack"] @@ -19,6 +19,8 @@ __all__ += ["ContextStack"] class AbstractContextManager(abc.ABC): """An abstract base class for context managers.""" + __class_getitem__ = classmethod(GenericAlias) + def __enter__(self): """Return `self` upon entering the runtime context.""" return self @@ -30,31 +32,36 @@ class AbstractContextManager(abc.ABC): @classmethod def __subclasshook__(cls, C): - """Check whether subclass is considered a subclass of this ABC.""" if cls is AbstractContextManager: - return _check_methods(C, "__enter__", "__exit__") + return _collections_abc._check_methods(C, "__enter__", "__exit__") + return NotImplemented + + +class AbstractAsyncContextManager(abc.ABC): + + """An abstract base class for asynchronous context managers.""" + + __class_getitem__ = classmethod(GenericAlias) + + async def __aenter__(self): + """Return `self` upon entering the runtime context.""" + return self + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_value, traceback): + """Raise any exception triggered within the runtime context.""" + return None + + @classmethod + def __subclasshook__(cls, C): + if cls is AbstractAsyncContextManager: + return _collections_abc._check_methods(C, "__aenter__", + "__aexit__") return NotImplemented 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() + "A base class or mixin that enables context managers to work as decorators." def _recreate_cm(self): """Return a recreated instance of self. @@ -76,8 +83,24 @@ class ContextDecorator(object): return inner -class _GeneratorContextManager(ContextDecorator): - """Helper for @contextmanager decorator.""" +class AsyncContextDecorator(object): + "A base class or mixin that enables async context managers to work as decorators." + + def _recreate_cm(self): + """Return a recreated instance of self. + """ + return self + + def __call__(self, func): + @wraps(func) + async def inner(*args, **kwds): + async with self._recreate_cm(): + return await func(*args, **kwds) + return inner + + +class _GeneratorContextManagerBase: + """Shared functionality for @contextmanager and @asynccontextmanager.""" def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) @@ -93,6 +116,12 @@ class _GeneratorContextManager(ContextDecorator): # for the class instead. # See http://bugs.python.org/issue19404 for more details. + +class _GeneratorContextManager(_GeneratorContextManagerBase, + AbstractContextManager, + ContextDecorator): + """Helper for @contextmanager decorator.""" + def _recreate_cm(self): # _GCM instances are one-shot context managers, so the # CM must be recreated each time a decorated function is @@ -100,17 +129,20 @@ class _GeneratorContextManager(ContextDecorator): return self.__class__(self.func, self.args, self.kwds) def __enter__(self): + # do not keep args and kwds alive unnecessarily + # they are only needed for recreation, which is not possible anymore + del self.args, self.kwds, self.func try: return next(self.gen) except StopIteration: - raise RuntimeError("generator didn't yield") + raise RuntimeError("generator didn't yield") from None def __exit__(self, type, value, traceback): if type is None: try: next(self.gen) except StopIteration: - return + return False else: raise RuntimeError("generator didn't stop") else: @@ -120,20 +152,19 @@ class _GeneratorContextManager(ContextDecorator): value = type() try: self.gen.throw(type, value, traceback) - raise RuntimeError("generator didn't stop after throw()") except StopIteration as exc: # 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. return exc is not value except RuntimeError as exc: - # Don't re-raise the passed in exception + # Don't re-raise the passed in exception. (issue27122) if exc is value: return False # Likewise, avoid suppressing if a StopIteration exception # was passed to throw() and later wrapped into a RuntimeError # (see PEP 479). - if exc.__cause__ is value: + if type is StopIteration and exc.__cause__ is value: return False raise except: @@ -144,7 +175,66 @@ class _GeneratorContextManager(ContextDecorator): # fixes the impedance mismatch between the throw() protocol # and the __exit__() protocol. # - if sys.exc_info()[1] is not value: + # This cannot use 'except BaseException as exc' (as in the + # async implementation) to maintain compatibility with + # Python 2, where old-style class exceptions are not caught + # by 'except BaseException'. + if sys.exc_info()[1] is value: + return False + raise + raise RuntimeError("generator didn't stop after throw()") + + +class _AsyncGeneratorContextManager(_GeneratorContextManagerBase, + AbstractAsyncContextManager, + AsyncContextDecorator): + """Helper for @asynccontextmanager.""" + + def _recreate_cm(self): + # _AGCM instances are one-shot context managers, so the + # ACM must be recreated each time a decorated function is + # called + return self.__class__(self.func, self.args, self.kwds) + + async def __aenter__(self): + try: + return await self.gen.__anext__() + except StopAsyncIteration: + raise RuntimeError("generator didn't yield") from None + + async def __aexit__(self, typ, value, traceback): + if typ is None: + try: + await self.gen.__anext__() + except StopAsyncIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + value = typ() + # See _GeneratorContextManager.__exit__ for comments on subtleties + # in this implementation + try: + await self.gen.athrow(typ, value, traceback) + raise RuntimeError("generator didn't stop after athrow()") + except StopAsyncIteration as exc: + return exc is not value + except RuntimeError as exc: + if exc is value: + return False + # Avoid suppressing if a StopIteration exception + # was passed to throw() and later wrapped into a RuntimeError + # (see PEP 479 for sync generators; async generators also + # have this behavior). But do this only if the exception wrapped + # by the RuntimeError is actually Stop(Async)Iteration (see + # issue29692). + if isinstance(value, (StopIteration, StopAsyncIteration)): + if exc.__cause__ is value: + return False + raise + except BaseException as exc: + if exc is not value: raise @@ -174,7 +264,6 @@ def contextmanager(func): finally: - """ @wraps(func) def helper(*args, **kwds): @@ -182,7 +271,40 @@ def contextmanager(func): return helper -class closing(object): +def asynccontextmanager(func): + """@asynccontextmanager decorator. + + Typical usage: + + @asynccontextmanager + async def some_async_generator(): + + try: + yield + finally: + + + This makes this: + + async with some_async_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + """ + @wraps(func) + def helper(*args, **kwds): + return _AsyncGeneratorContextManager(func, args, kwds) + return helper + + +class closing(AbstractContextManager): """Context to automatically close something at the end of a block. Code like this: @@ -201,15 +323,39 @@ class closing(object): """ def __init__(self, thing): self.thing = thing - def __enter__(self): return self.thing - def __exit__(self, *exc_info): self.thing.close() -class _RedirectStream(object): +class aclosing(AbstractAsyncContextManager): + """Async context manager for safely finalizing an asynchronously cleaned-up + resource such as an async generator, calling its ``aclose()`` method. + + Code like this: + + async with aclosing(.fetch()) as agen: + + + is equivalent to this: + + agen = .fetch() + try: + + finally: + await agen.aclose() + + """ + def __init__(self, thing): + self.thing = thing + async def __aenter__(self): + return self.thing + async def __aexit__(self, *exc_info): + await self.thing.aclose() + + +class _RedirectStream(AbstractContextManager): _stream = None @@ -249,7 +395,7 @@ class redirect_stderr(_RedirectStream): _stream = "stderr" -class suppress(object): +class suppress(AbstractContextManager): """Context manager to suppress specified exceptions After the exception is suppressed, execution proceeds with the next @@ -279,113 +425,96 @@ class suppress(object): return exctype is not None and issubclass(exctype, self._exceptions) -# Context manipulation helpers -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 +class _BaseExitStack: + """A base class for ExitStack and AsyncExitStack.""" -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 + @staticmethod + def _create_exit_wrapper(cm, cm_exit): + return MethodType(cm_exit, cm) + @staticmethod + def _create_cb_wrapper(callback, /, *args, **kwds): + def _exit_wrapper(exc_type, exc, tb): + callback(*args, **kwds) + return _exit_wrapper -# 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 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""" + """Preserve the context stack by transferring it to a new instance.""" new_stack = type(self)() new_stack._exit_callbacks = self._exit_callbacks self._exit_callbacks = deque() return new_stack - def _push_cm_exit(self, cm, cm_exit): - """Helper to correctly register callbacks to __exit__ methods""" - 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. + """Registers a callback with the standard __exit__ method signature. + Can suppress exceptions the same way __exit__ method can. Also accepts any object with an __exit__ method (registering a call - to the method instead of the object itself) + to the method instead of the object itself). """ # We use an unbound method rather than a bound method to follow - # the standard lookup behaviour for special methods + # the standard lookup behaviour for special methods. _cb_type = type(exit) + try: exit_method = _cb_type.__exit__ except AttributeError: - # Not a context manager, so assume its a callable - self._exit_callbacks.append(exit) + # Not a context manager, so assume it's a callable. + self._push_exit_callback(exit) else: self._push_cm_exit(exit, exit_method) - return exit # Allow use as a decorator - - def callback(self, callback, *args, **kwds): - """Registers an arbitrary callback and arguments. - - Cannot suppress exceptions. - """ - def _exit_wrapper(exc_type, exc, tb): - callback(*args, **kwds) - # We changed the signature, so using @wraps is not appropriate, but - # setting __wrapped__ may still help with introspection - _exit_wrapper.__wrapped__ = callback - self.push(_exit_wrapper) - return callback # Allow use as a decorator + return exit # Allow use as a decorator. def enter_context(self, cm): - """Enters the supplied context manager + """Enters the supplied context manager. If successful, also pushes its __exit__ method as a callback and returns the result of the __enter__ method. """ - # We look up the special methods on the type to match the with statement + # We look up the special methods on the type to match the with + # statement. _cm_type = type(cm) _exit = _cm_type.__exit__ result = _cm_type.__enter__(cm) self._push_cm_exit(cm, _exit) return result - def close(self): - """Immediately unwind the context stack""" - self.__exit__(None, None, None) + def callback(self, callback, /, *args, **kwds): + """Registers an arbitrary callback and arguments. + + Cannot suppress exceptions. + """ + _exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds) + + # We changed the signature, so using @wraps is not appropriate, but + # setting __wrapped__ may still help with introspection. + _exit_wrapper.__wrapped__ = callback + self._push_exit_callback(_exit_wrapper) + return callback # Allow use as a decorator + + def _push_cm_exit(self, cm, cm_exit): + """Helper to correctly register callbacks to __exit__ methods.""" + _exit_wrapper = self._create_exit_wrapper(cm, cm_exit) + self._push_exit_callback(_exit_wrapper, True) + + def _push_exit_callback(self, callback, is_sync=True): + self._exit_callbacks.append((is_sync, callback)) + + +# Inspired by discussions on http://bugs.python.org/issue13585 +class ExitStack(_BaseExitStack, AbstractContextManager): + """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 raise an exception. + """ def __enter__(self): return self @@ -396,14 +525,27 @@ class ExitStack(object): # 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) + 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 # 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() + is_sync, cb = self._exit_callbacks.pop() + assert is_sync try: if cb(*exc_details): suppressed_exc = True @@ -416,33 +558,161 @@ class ExitStack(object): pending_raise = True exc_details = new_exc_details if pending_raise: - _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 + return received_exc and suppressed_exc + + def close(self): + """Immediately unwind the context stack.""" + self.__exit__(None, None, None) + + +# Inspired by discussions on https://bugs.python.org/issue29302 +class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): + """Async context manager for dynamic management of a stack of exit + callbacks. + + For example: + async with AsyncExitStack() as stack: + connections = [await stack.enter_async_context(get_connection()) + for i in range(5)] + # All opened connections will automatically be released at the + # end of the async with statement, even if attempts to open a + # connection later in the list raise an exception. + """ + + @staticmethod + def _create_async_exit_wrapper(cm, cm_exit): + return MethodType(cm_exit, cm) + + @staticmethod + def _create_async_cb_wrapper(callback, /, *args, **kwds): + async def _exit_wrapper(exc_type, exc, tb): + await callback(*args, **kwds) + return _exit_wrapper + + async def enter_async_context(self, cm): + """Enters the supplied async context manager. + + If successful, also pushes its __aexit__ method as a callback and + returns the result of the __aenter__ method. + """ + _cm_type = type(cm) + _exit = _cm_type.__aexit__ + result = await _cm_type.__aenter__(cm) + self._push_async_cm_exit(cm, _exit) + return result + + def push_async_exit(self, exit): + """Registers a coroutine function with the standard __aexit__ method + signature. + + Can suppress exceptions the same way __aexit__ method can. + Also accepts any object with an __aexit__ method (registering a call + to the method instead of the object itself). + """ + _cb_type = type(exit) + try: + exit_method = _cb_type.__aexit__ + except AttributeError: + # Not an async context manager, so assume it's a coroutine function + self._push_exit_callback(exit, False) + else: + self._push_async_cm_exit(exit, exit_method) + return exit # Allow use as a decorator + + def push_async_callback(self, callback, /, *args, **kwds): + """Registers an arbitrary coroutine function and arguments. + + Cannot suppress exceptions. + """ + _exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds) + + # We changed the signature, so using @wraps is not appropriate, but + # setting __wrapped__ may still help with introspection. + _exit_wrapper.__wrapped__ = callback + self._push_exit_callback(_exit_wrapper, False) + return callback # Allow use as a decorator + + async def aclose(self): + """Immediately unwind the context stack.""" + await self.__aexit__(None, None, None) + + def _push_async_cm_exit(self, cm, cm_exit): + """Helper to correctly register coroutine function to __aexit__ + method.""" + _exit_wrapper = self._create_async_exit_wrapper(cm, cm_exit) + self._push_exit_callback(_exit_wrapper, False) + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc_details): + 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] + 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 + + # Callbacks are invoked in LIFO order to match the behaviour of + # nested context managers + suppressed_exc = False + pending_raise = False + while self._exit_callbacks: + is_sync, cb = self._exit_callbacks.pop() + try: + if is_sync: + cb_suppress = cb(*exc_details) + else: + cb_suppress = await cb(*exc_details) + + if cb_suppress: + suppressed_exc = True + pending_raise = False + exc_details = (None, None, None) + 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: + 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 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) - - def register(self, callback, *args, **kwds): - return self.callback(callback, *args, **kwds) - - def preserve(self): - return self.pop_all() - - -class nullcontext(AbstractContextManager): +class nullcontext(AbstractContextManager, AbstractAsyncContextManager): """Context manager that does no additional processing. + Used as a stand-in for a normal context manager, when a particular block of code is only sometimes used with a normal context manager: + cm = optional_cm if condition else nullcontext() with cm: # Perform operation, using optional_cm if condition is True @@ -456,3 +726,9 @@ class nullcontext(AbstractContextManager): def __exit__(self, *excinfo): pass + + async def __aenter__(self): + return self.enter_result + + async def __aexit__(self, *excinfo): + pass From cf0cece8370d09e86bcb05941a34e8b73e545e7e Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 16:40:28 +1000 Subject: [PATCH 2/7] Add back refresh_cm --- contextlib2.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contextlib2.py b/contextlib2.py index 3599644..c8b4ad0 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -63,6 +63,23 @@ class AbstractAsyncContextManager(abc.ABC): 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. From 6a40a1ee80a894f877abe1891aaad1948942afb9 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 16:49:00 +1000 Subject: [PATCH 3/7] Fix syntax and import compatibility --- contextlib2.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/contextlib2.py b/contextlib2.py index c8b4ad0..b529328 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -6,7 +6,17 @@ import warnings import _collections_abc from collections import deque from functools import wraps -from types import MethodType, GenericAlias +from types import MethodType + +# Python 3.6/3.7/3.8 compatibility: GenericAlias may not be defined +try: + from types import GenericAlias +except ImportError: + # If the real GenericAlias type doesn't exist, __class_getitem__ won't be used, + # so the fallback placeholder doesn't need to provide any meaningful behaviour + class GenericAlias: + pass + __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", "AbstractContextManager", "AbstractAsyncContextManager", @@ -450,7 +460,9 @@ class _BaseExitStack: return MethodType(cm_exit, cm) @staticmethod - def _create_cb_wrapper(callback, /, *args, **kwds): + def _create_cb_wrapper(*args, **kwds): + # Python 3.6/3.7 compatibility: no native positional-only args syntax + callback, *args = args def _exit_wrapper(exc_type, exc, tb): callback(*args, **kwds) return _exit_wrapper @@ -499,11 +511,13 @@ class _BaseExitStack: self._push_cm_exit(cm, _exit) return result - def callback(self, callback, /, *args, **kwds): + def callback(*args, **kwds): """Registers an arbitrary callback and arguments. Cannot suppress exceptions. """ + # Python 3.6/3.7 compatibility: no native positional-only args syntax + self, callback, *args = args _exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds) # We changed the signature, so using @wraps is not appropriate, but @@ -609,7 +623,9 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): return MethodType(cm_exit, cm) @staticmethod - def _create_async_cb_wrapper(callback, /, *args, **kwds): + def _create_async_cb_wrapper(*args, **kwds): + # Python 3.6/3.7 compatibility: no native positional-only args syntax + callback, *args = args async def _exit_wrapper(exc_type, exc, tb): await callback(*args, **kwds) return _exit_wrapper @@ -644,11 +660,13 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): self._push_async_cm_exit(exit, exit_method) return exit # Allow use as a decorator - def push_async_callback(self, callback, /, *args, **kwds): + def push_async_callback(*args, **kwds): """Registers an arbitrary coroutine function and arguments. Cannot suppress exceptions. """ + # Python 3.6/3.7 compatibility: no native positional-only args syntax + self, callback, *args = args _exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds) # We changed the signature, so using @wraps is not appropriate, but From 630f73a3a52d87b91f4ad153b32a76a655034e89 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 16:50:39 +1000 Subject: [PATCH 4/7] Add back ContextStack alias --- contextlib2.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contextlib2.py b/contextlib2.py index b529328..0276849 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -767,3 +767,22 @@ class nullcontext(AbstractContextManager, AbstractAsyncContextManager): async def __aexit__(self, *excinfo): pass + + +# 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) + + def register(self, callback, *args, **kwds): + return self.callback(callback, *args, **kwds) + + def preserve(self): + return self.pop_all() From 94a3b86586903b01ff5dc04284404230102d362d Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 16:55:09 +1000 Subject: [PATCH 5/7] Sync module, mostly keeping old test suite --- test_contextlib2.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test_contextlib2.py b/test_contextlib2.py index 994798d..22c9664 100755 --- a/test_contextlib2.py +++ b/test_contextlib2.py @@ -455,7 +455,7 @@ class TestExitStack(unittest.TestCase): else: f = stack.callback(_exit) self.assertIs(f, _exit) - for wrapper in stack._exit_callbacks: + for __, wrapper in stack._exit_callbacks: self.assertIs(wrapper.__wrapped__, _exit) self.assertNotEqual(wrapper.__name__, _exit.__name__) self.assertIsNone(wrapper.__doc__, _exit.__doc__) @@ -480,19 +480,19 @@ class TestExitStack(unittest.TestCase): self.check_exc(*exc_details) with ExitStack() as stack: stack.push(_expect_ok) - self.assertIs(stack._exit_callbacks[-1], _expect_ok) + self.assertIs(stack._exit_callbacks[-1][1], _expect_ok) cm = ExitCM(_expect_ok) stack.push(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) stack.push(_suppress_exc) - self.assertIs(stack._exit_callbacks[-1], _suppress_exc) + self.assertIs(stack._exit_callbacks[-1][1], _suppress_exc) cm = ExitCM(_expect_exc) stack.push(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) stack.push(_expect_exc) - self.assertIs(stack._exit_callbacks[-1], _expect_exc) + self.assertIs(stack._exit_callbacks[-1][1], _expect_exc) stack.push(_expect_exc) - self.assertIs(stack._exit_callbacks[-1], _expect_exc) + self.assertIs(stack._exit_callbacks[-1][1], _expect_exc) 1/0 def test_enter_context(self): @@ -510,7 +510,7 @@ class TestExitStack(unittest.TestCase): result.append(4) self.assertIsNotNone(_exit) stack.enter_context(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) result.append(2) self.assertEqual(result, [1, 2, 3, 4]) @@ -742,7 +742,7 @@ class TestExitStack(unittest.TestCase): stack = ExitStack() self.assertRaises(AttributeError, stack.enter_context, cm) stack.push(cm) - self.assertIs(stack._exit_callbacks[-1], cm) + self.assertIs(stack._exit_callbacks[-1][1], cm) def test_default_class_semantics(self): # For Python 2.x, this ensures compatibility with old-style classes @@ -761,13 +761,13 @@ class TestExitStack(unittest.TestCase): cb = DefaultCallable() with ExitStack() as stack: stack.enter_context(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) stack.push(cb) stack.push(cm) - self.assertIs(stack._exit_callbacks[-1].__self__, cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) result.append("Running") stack.callback(cb) - self.assertIs(stack._exit_callbacks[-1].__wrapped__, cb) + self.assertIs(stack._exit_callbacks[-1][1].__wrapped__, cb) self.assertEqual(result, ["Enter", "Running", "Callback", "Exit", "Callback", "Exit", From 4b39470cbb399422c2ee27d8d5265d0fb93c64ca Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 17:49:42 +1000 Subject: [PATCH 6/7] Sync test suite, add notes on sync process --- MANIFEST.in | 4 +- NEWS.rst | 23 +- README.rst | 30 +- contextlib2.py | 14 +- dev/py3_10_contextlib_to_contextlib2.patch | 147 +++++ test/__init__.py | 1 + test/support/__init__.py | 6 + test/support/os_helper.py | 4 + .../test_contextlib.py | 347 +++++++---- test/test_contextlib_async.py | 551 ++++++++++++++++++ tox.ini | 2 +- 11 files changed, 980 insertions(+), 149 deletions(-) create mode 100644 dev/py3_10_contextlib_to_contextlib2.patch create mode 100644 test/__init__.py create mode 100644 test/support/__init__.py create mode 100644 test/support/os_helper.py rename test_contextlib2.py => test/test_contextlib.py (79%) mode change 100755 => 100644 create mode 100644 test/test_contextlib_async.py diff --git a/MANIFEST.in b/MANIFEST.in index d26c62c..899523c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include *.py *.txt *.rst *.md MANIFEST.in -recursive-include docs *.rst *.py make.bat Makefile +include *.py *.txt *.rst *.md *.ini MANIFEST.in +recursive-include test docs *.rst *.py make.bat Makefile diff --git a/NEWS.rst b/NEWS.rst index 920275b..cfbe814 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,25 +1,23 @@ Release History --------------- -21.6.0 (2021-06-TBD) -^^^^^^^^^^^^^^^^^^^^^^^^ +21.6.0 (2021-06-27) +^^^^^^^^^^^^^^^^^^^ * Switched to calendar based versioning rather than continuing with pre-1.0 semantic versioning (`#29 `__) * Due to the inclusion of asynchronous features from Python 3.7+, the minimum supported Python version is now Python 3.6 (`#29 `__) -* (WIP) Synchronised with the Python 3.10 version of contextlib, bringing the - following new features to Python 3.6+ ( - `#12 `__, - `#19 `__, - `#27 `__): +* Synchronised with the Python 3.10 version of contextlib + (`#12 `__), making the + following new features available on Python 3.6+: - * ``asyncontextmanager`` (Python 3.7) - * ``aclosing`` (Python 3.10) - * ``AbstractAsyncContextManager`` (Python 3.7) - * ``AsyncContextDecorator`` (Python 3.10) - * ``AsyncExitStack`` (Python 3.7) + * ``asyncontextmanager`` (added in Python 3.7, enhanced in Python 3.10) + * ``aclosing`` (added in Python 3.10) + * ``AbstractAsyncContextManager`` (added in Python 3.7) + * ``AsyncContextDecorator`` (added in Python 3.10) + * ``AsyncExitStack`` (added in Python 3.7) * async support in ``nullcontext`` (Python 3.10) * Updates to the default compatibility testing matrix: @@ -27,7 +25,6 @@ Release History * Added: CPython 3.9, CPython 3.10 * Dropped: CPython 2.7, CPython 3.5, PyPy2 - 0.6.0.post1 (2019-10-10) ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/README.rst b/README.rst index 84f25c5..d64ba87 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ :alt: Latest Docs contextlib2 is a backport of the `standard library's contextlib -module `_ to +module `_ to earlier Python versions. It also serves as a real world proving ground for possible future @@ -28,7 +28,9 @@ contextlib2 has no runtime dependencies, but requires ``unittest2`` for testing on Python 2.x, as well as ``setuptools`` and ``wheel`` to generate universal wheel archives. -Local testing is just a matter of running ``python test_contextlib2.py``. +Local testing is a matter of running:: + + python3 -m unittest discover -t . -s test You can test against multiple versions of Python with `tox `_:: @@ -38,10 +40,28 @@ You can test against multiple versions of Python with Versions currently tested in both tox and GitHub Actions are: -* CPython 2.7 -* CPython 3.5 * CPython 3.6 * CPython 3.7 * CPython 3.8 -* PyPy +* CPython 3.9 +* CPython 3.10 * PyPy3 + +Updating to a new stdlib reference version +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As of Python 3.10, 3 files needed to be copied from the CPython reference +implementation to contextlib2: + +* ``Lib/contextlib.py`` -> ``contextlib2.py`` +* ``Lib/test/test_contextlib.py`` -> ``test/test_contextlib.py`` +* ``Lib/test/test_contextlib_async.py`` -> ``test/test_contextlib_async.py`` + +For the 3.10 sync, the only changes needed to the test files were to import from +``contextlib2`` rather than ``contextlib``. The test directory is laid out so +that the test suite's imports from ``test.support`` work the same way they do in +the main CPython test suite. + +The changes made to the ``contextlib2.py`` file to get it to run on the older +versions (and to add back in the deprecated APIs that never graduated to the +standard library version) are saved as a patch file in the ``dev`` directory. diff --git a/contextlib2.py b/contextlib2.py index 0276849..d6c0c4a 100644 --- a/contextlib2.py +++ b/contextlib2.py @@ -517,7 +517,12 @@ class _BaseExitStack: Cannot suppress exceptions. """ # Python 3.6/3.7 compatibility: no native positional-only args syntax - self, callback, *args = args + try: + self, callback, *args = args + except ValueError as exc: + exc_details = str(exc).partition("(")[2] + msg = "Not enough positional arguments {}".format(exc_details) + raise TypeError(msg) from None _exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds) # We changed the signature, so using @wraps is not appropriate, but @@ -666,7 +671,12 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): Cannot suppress exceptions. """ # Python 3.6/3.7 compatibility: no native positional-only args syntax - self, callback, *args = args + try: + self, callback, *args = args + except ValueError as exc: + exc_details = str(exc).partition("(")[2] + msg = "Not enough positional arguments {}".format(exc_details) + raise TypeError(msg) from None _exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds) # We changed the signature, so using @wraps is not appropriate, but diff --git a/dev/py3_10_contextlib_to_contextlib2.patch b/dev/py3_10_contextlib_to_contextlib2.patch new file mode 100644 index 0000000..41d8919 --- /dev/null +++ b/dev/py3_10_contextlib_to_contextlib2.patch @@ -0,0 +1,147 @@ +--- ../cpython/Lib/contextlib.py 2021-06-26 16:28:03.835372955 +1000 ++++ contextlib2.py 2021-06-26 17:40:30.047079570 +1000 +@@ -1,19 +1,32 @@ +-"""Utilities for with-statement contexts. See PEP 343.""" ++"""contextlib2 - backports and enhancements to the contextlib module""" ++ + import abc + import sys ++import warnings + import _collections_abc + from collections import deque + from functools import wraps +-from types import MethodType, GenericAlias ++from types import MethodType ++ ++# Python 3.6/3.7/3.8 compatibility: GenericAlias may not be defined ++try: ++ from types import GenericAlias ++except ImportError: ++ # If the real GenericAlias type doesn't exist, __class_getitem__ won't be used, ++ # so the fallback placeholder doesn't need to provide any meaningful behaviour ++ class GenericAlias: ++ pass ++ + + __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", + "AbstractContextManager", "AbstractAsyncContextManager", + "AsyncExitStack", "ContextDecorator", "ExitStack", + "redirect_stdout", "redirect_stderr", "suppress", "aclosing"] + ++# Backwards compatibility ++__all__ += ["ContextStack"] + + class AbstractContextManager(abc.ABC): +- + """An abstract base class for context managers.""" + + __class_getitem__ = classmethod(GenericAlias) +@@ -60,6 +73,23 @@ + 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. + +@@ -430,7 +460,9 @@ + return MethodType(cm_exit, cm) + + @staticmethod +- def _create_cb_wrapper(callback, /, *args, **kwds): ++ def _create_cb_wrapper(*args, **kwds): ++ # Python 3.6/3.7 compatibility: no native positional-only args syntax ++ callback, *args = args + def _exit_wrapper(exc_type, exc, tb): + callback(*args, **kwds) + return _exit_wrapper +@@ -479,11 +511,18 @@ + self._push_cm_exit(cm, _exit) + return result + +- def callback(self, callback, /, *args, **kwds): ++ def callback(*args, **kwds): + """Registers an arbitrary callback and arguments. + + Cannot suppress exceptions. + """ ++ # Python 3.6/3.7 compatibility: no native positional-only args syntax ++ try: ++ self, callback, *args = args ++ except ValueError as exc: ++ exc_details = str(exc).partition("(")[2] ++ msg = "Not enough positional arguments {}".format(exc_details) ++ raise TypeError(msg) from None + _exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds) + + # We changed the signature, so using @wraps is not appropriate, but +@@ -589,7 +628,9 @@ + return MethodType(cm_exit, cm) + + @staticmethod +- def _create_async_cb_wrapper(callback, /, *args, **kwds): ++ def _create_async_cb_wrapper(*args, **kwds): ++ # Python 3.6/3.7 compatibility: no native positional-only args syntax ++ callback, *args = args + async def _exit_wrapper(exc_type, exc, tb): + await callback(*args, **kwds) + return _exit_wrapper +@@ -624,11 +665,18 @@ + self._push_async_cm_exit(exit, exit_method) + return exit # Allow use as a decorator + +- def push_async_callback(self, callback, /, *args, **kwds): ++ def push_async_callback(*args, **kwds): + """Registers an arbitrary coroutine function and arguments. + + Cannot suppress exceptions. + """ ++ # Python 3.6/3.7 compatibility: no native positional-only args syntax ++ try: ++ self, callback, *args = args ++ except ValueError as exc: ++ exc_details = str(exc).partition("(")[2] ++ msg = "Not enough positional arguments {}".format(exc_details) ++ raise TypeError(msg) from None + _exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds) + + # We changed the signature, so using @wraps is not appropriate, but +@@ -729,3 +777,22 @@ + + async def __aexit__(self, *excinfo): + pass ++ ++ ++# 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) ++ ++ def register(self, callback, *args, **kwds): ++ return self.callback(callback, *args, **kwds) ++ ++ def preserve(self): ++ return self.pop_all() diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..51c4b33 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +# unittest test discovery requires an __init__.py file in the test directory diff --git a/test/support/__init__.py b/test/support/__init__.py new file mode 100644 index 0000000..1e708d1 --- /dev/null +++ b/test/support/__init__.py @@ -0,0 +1,6 @@ +"""Enough of the test.support APIs to run the contextlib test suite""" +import sys +import unittest + +requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, + "Test requires docstrings") diff --git a/test/support/os_helper.py b/test/support/os_helper.py new file mode 100644 index 0000000..e6e4a6f --- /dev/null +++ b/test/support/os_helper.py @@ -0,0 +1,4 @@ +"""Enough of the test.support.os_helper APIs to run the contextlib test suite""" +import os + +unlink = os.unlink diff --git a/test_contextlib2.py b/test/test_contextlib.py old mode 100755 new mode 100644 similarity index 79% rename from test_contextlib2.py rename to test/test_contextlib.py index 22c9664..8a9176b --- a/test_contextlib2.py +++ b/test/test_contextlib.py @@ -1,16 +1,14 @@ -"""Unit tests for contextlib2.py""" -from __future__ import print_function -from __future__ import unicode_literals +"""Unit tests for contextlib.py, and other context managers.""" import io import sys +import tempfile +import threading import unittest -import __future__ # For PEP 479 conditional test -import contextlib2 from contextlib2 import * # Tests __all__ - -requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, - "Test requires docstrings") +from test import support +from test.support import os_helper +import weakref class TestAbstractContextManager(unittest.TestCase): @@ -31,8 +29,7 @@ class TestAbstractContextManager(unittest.TestCase): MissingExit() def test_structural_subclassing(self): - # New style classes used here - class ManagerFromScratch(object): + class ManagerFromScratch: def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): @@ -46,22 +43,15 @@ class TestAbstractContextManager(unittest.TestCase): self.assertTrue(issubclass(DefaultEnter, AbstractContextManager)) - if sys.version_info[:2] <= (3, 0): - def test_structural_subclassing_classic(self): - # Old style classes used here - class ManagerFromScratch: - def __enter__(self): - return self - def __exit__(self, exc_type, exc_value, traceback): - return None + class NoEnter(ManagerFromScratch): + __enter__ = None - self.assertTrue(issubclass(ManagerFromScratch, AbstractContextManager)) + self.assertFalse(issubclass(NoEnter, AbstractContextManager)) - class DefaultEnter(AbstractContextManager): - def __exit__(self, *args): - super().__exit__(*args) + class NoExit(ManagerFromScratch): + __exit__ = None - self.assertTrue(issubclass(DefaultEnter, AbstractContextManager)) + self.assertFalse(issubclass(NoExit, AbstractContextManager)) class ContextManagerTestCase(unittest.TestCase): @@ -141,7 +131,7 @@ class ContextManagerTestCase(unittest.TestCase): def woohoo(): yield try: - with self.assertWarnsRegex(PendingDeprecationWarning, + with self.assertWarnsRegex(DeprecationWarning, "StopIteration"): with woohoo(): raise stop_exc @@ -150,8 +140,6 @@ class ContextManagerTestCase(unittest.TestCase): 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 @@ -173,6 +161,29 @@ def woohoo(): else: self.fail('StopIteration was suppressed') + def test_contextmanager_do_not_unchain_non_stopiteration_exceptions(self): + @contextmanager + def test_issue29692(): + try: + yield + except Exception as exc: + raise RuntimeError('issue29692:Chained') from exc + try: + with test_issue29692(): + raise ZeroDivisionError + except Exception as ex: + self.assertIs(type(ex), RuntimeError) + self.assertEqual(ex.args[0], 'issue29692:Chained') + self.assertIsInstance(ex.__cause__, ZeroDivisionError) + + try: + with test_issue29692(): + raise StopIteration('issue29692:Unchained') + except Exception as ex: + self.assertIs(type(ex), StopIteration) + self.assertEqual(ex.args[0], 'issue29692:Unchained') + self.assertIsNone(ex.__cause__) + def _create_contextmanager_attribs(self): def attribs(**kw): def decorate(func): @@ -191,12 +202,12 @@ def woohoo(): self.assertEqual(baz.__name__,'baz') self.assertEqual(baz.foo, 'bar') - @requires_docstrings + @support.requires_docstrings def test_contextmanager_doc_attrib(self): baz = self._create_contextmanager_attribs() self.assertEqual(baz.__doc__, "Whee!") - @requires_docstrings + @support.requires_docstrings def test_instance_docstring_given_cm_docstring(self): baz = self._create_contextmanager_attribs()(None) self.assertEqual(baz.__doc__, "Whee!") @@ -209,10 +220,56 @@ def woohoo(): with woohoo(self=11, func=22, args=33, kwds=44) as target: self.assertEqual(target, (11, 22, 33, 44)) + def test_nokeepref(self): + class A: + pass + + @contextmanager + def woohoo(a, b): + a = weakref.ref(a) + b = weakref.ref(b) + self.assertIsNone(a()) + self.assertIsNone(b()) + yield + + with woohoo(A(), b=A()): + pass + + def test_param_errors(self): + @contextmanager + def woohoo(a, *, b): + yield + + with self.assertRaises(TypeError): + woohoo() + with self.assertRaises(TypeError): + woohoo(3, 5) + with self.assertRaises(TypeError): + woohoo(b=3) + + def test_recursive(self): + depth = 0 + @contextmanager + def woohoo(): + nonlocal depth + before = depth + depth += 1 + yield + depth -= 1 + self.assertEqual(depth, before) + + @woohoo() + def recursive(): + if depth < 10: + recursive() + + recursive() + self.assertEqual(depth, 0) + class ClosingTestCase(unittest.TestCase): - @requires_docstrings + @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = closing.__doc__ @@ -244,6 +301,83 @@ class ClosingTestCase(unittest.TestCase): self.assertEqual(state, [1]) +class NullcontextTestCase(unittest.TestCase): + def test_nullcontext(self): + class C: + pass + c = C() + with nullcontext(c) as c_in: + self.assertIs(c_in, c) + + +class FileContextTestCase(unittest.TestCase): + + def testWithOpen(self): + tfn = tempfile.mktemp() + try: + f = None + with open(tfn, "w", encoding="utf-8") as f: + self.assertFalse(f.closed) + f.write("Booh\n") + self.assertTrue(f.closed) + f = None + with self.assertRaises(ZeroDivisionError): + with open(tfn, "r", encoding="utf-8") as f: + self.assertFalse(f.closed) + self.assertEqual(f.read(), "Booh\n") + 1 / 0 + self.assertTrue(f.closed) + finally: + os_helper.unlink(tfn) + +class LockContextTestCase(unittest.TestCase): + + def boilerPlate(self, lock, locked): + self.assertFalse(locked()) + with lock: + self.assertTrue(locked()) + self.assertFalse(locked()) + with self.assertRaises(ZeroDivisionError): + with lock: + self.assertTrue(locked()) + 1 / 0 + self.assertFalse(locked()) + + def testWithLock(self): + lock = threading.Lock() + self.boilerPlate(lock, lock.locked) + + def testWithRLock(self): + lock = threading.RLock() + self.boilerPlate(lock, lock._is_owned) + + def testWithCondition(self): + lock = threading.Condition() + def locked(): + return lock._is_owned() + self.boilerPlate(lock, locked) + + def testWithSemaphore(self): + lock = threading.Semaphore() + def locked(): + if lock.acquire(False): + lock.release() + return False + else: + return True + self.boilerPlate(lock, locked) + + def testWithBoundedSemaphore(self): + lock = threading.BoundedSemaphore() + def locked(): + if lock.acquire(False): + lock.release() + return False + else: + return True + self.boilerPlate(lock, locked) + + class mycontext(ContextDecorator): """Example decoration-compatible context manager for testing""" started = False @@ -261,7 +395,7 @@ class mycontext(ContextDecorator): class TestContextDecorator(unittest.TestCase): - @requires_docstrings + @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = mycontext.__doc__ @@ -418,17 +552,19 @@ class TestContextDecorator(unittest.TestCase): test('something else') self.assertEqual(state, [1, 'something else', 999]) -class TestExitStack(unittest.TestCase): - @requires_docstrings +class TestBaseExitStack: + exit_stack = None + + @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings - cm_docstring = ExitStack.__doc__ - obj = ExitStack() + cm_docstring = self.exit_stack.__doc__ + obj = self.exit_stack() self.assertEqual(obj.__doc__, cm_docstring) def test_no_resources(self): - with ExitStack(): + with self.exit_stack(): pass def test_callback(self): @@ -439,12 +575,13 @@ class TestExitStack(unittest.TestCase): ((), dict(example=1)), ((1,), dict(example=1)), ((1,2), dict(example=1)), + ((1,2), dict(self=3, callback=4)), ] result = [] def _exit(*args, **kwds): """Test metadata propagation""" result.append((args, kwds)) - with ExitStack() as stack: + with self.exit_stack() as stack: for args, kwds in reversed(expected): if args and kwds: f = stack.callback(_exit, *args, **kwds) @@ -455,12 +592,22 @@ class TestExitStack(unittest.TestCase): else: f = stack.callback(_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__) + for wrapper in stack._exit_callbacks: + self.assertIs(wrapper[1].__wrapped__, _exit) + self.assertNotEqual(wrapper[1].__name__, _exit.__name__) + self.assertIsNone(wrapper[1].__doc__, _exit.__doc__) self.assertEqual(result, expected) + result = [] + with self.exit_stack() as stack: + with self.assertRaises(TypeError): + stack.callback(arg=1) + with self.assertRaises(TypeError): + self.exit_stack.callback(arg=2) + with self.assertRaises(TypeError): + stack.callback(callback=_exit, arg=3) + self.assertEqual(result, []) + def test_push(self): exc_raised = ZeroDivisionError def _expect_exc(exc_type, exc, exc_tb): @@ -478,7 +625,7 @@ class TestExitStack(unittest.TestCase): self.fail("Should not be called!") def __exit__(self, *exc_details): self.check_exc(*exc_details) - with ExitStack() as stack: + with self.exit_stack() as stack: stack.push(_expect_ok) self.assertIs(stack._exit_callbacks[-1][1], _expect_ok) cm = ExitCM(_expect_ok) @@ -504,7 +651,7 @@ class TestExitStack(unittest.TestCase): result = [] cm = TestCM() - with ExitStack() as stack: + with self.exit_stack() as stack: @stack.callback # Registered first => cleaned up last def _exit(): result.append(4) @@ -516,7 +663,7 @@ class TestExitStack(unittest.TestCase): def test_close(self): result = [] - with ExitStack() as stack: + with self.exit_stack() as stack: @stack.callback def _exit(): result.append(1) @@ -527,7 +674,7 @@ class TestExitStack(unittest.TestCase): def test_pop_all(self): result = [] - with ExitStack() as stack: + with self.exit_stack() as stack: @stack.callback def _exit(): result.append(3) @@ -540,12 +687,12 @@ class TestExitStack(unittest.TestCase): def test_exit_raise(self): with self.assertRaises(ZeroDivisionError): - with ExitStack() as stack: + with self.exit_stack() as stack: stack.push(lambda *exc: False) 1/0 def test_exit_suppress(self): - with ExitStack() as stack: + with self.exit_stack() as stack: stack.push(lambda *exc: True) 1/0 @@ -576,7 +723,7 @@ class TestExitStack(unittest.TestCase): def __enter__(self): return self def __exit__(self, *exc_details): - self.__class__.saved_details = exc_details + type(self).saved_details = exc_details return True try: @@ -602,13 +749,14 @@ class TestExitStack(unittest.TestCase): def raise_exc(exc): raise exc - saved_details = [None] + saved_details = None def suppress_exc(*exc_details): - saved_details[0] = exc_details + nonlocal saved_details + saved_details = exc_details return True try: - with ExitStack() as stack: + with self.exit_stack() as stack: stack.callback(raise_exc, IndexError) stack.callback(raise_exc, KeyError) stack.callback(raise_exc, AttributeError) @@ -623,7 +771,7 @@ class TestExitStack(unittest.TestCase): else: self.fail("Expected IndexError, but no exception was raised") # Check the inner exceptions - inner_exc = saved_details[0][1] + inner_exc = saved_details[1] self.assertIsInstance(inner_exc, ValueError) self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) @@ -636,7 +784,7 @@ class TestExitStack(unittest.TestCase): return True try: - with ExitStack() as stack: + with self.exit_stack() as stack: stack.callback(lambda: None) stack.callback(raise_exc, IndexError) except Exception as exc: @@ -645,7 +793,7 @@ class TestExitStack(unittest.TestCase): self.fail("Expected IndexError, but no exception was raised") try: - with ExitStack() as stack: + with self.exit_stack() as stack: stack.callback(raise_exc, KeyError) stack.push(suppress_exc) stack.callback(raise_exc, IndexError) @@ -672,7 +820,7 @@ class TestExitStack(unittest.TestCase): # fix, ExitStack would try to fix it *again* and get into an # infinite self-referential loop try: - with ExitStack() as stack: + with self.exit_stack() 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)) @@ -683,7 +831,7 @@ class TestExitStack(unittest.TestCase): self.assertIs(exc.__context__.__context__, exc2) self.assertIs(exc.__context__.__context__.__context__, exc1) self.assertIsNone( - exc.__context__.__context__.__context__.__context__) + exc.__context__.__context__.__context__.__context__) def test_exit_exception_with_existing_context(self): # Addresses a lack of test coverage discovered after checking in a @@ -699,7 +847,7 @@ class TestExitStack(unittest.TestCase): exc4 = Exception(4) exc5 = Exception(5) try: - with ExitStack() as stack: + with self.exit_stack() as stack: stack.callback(raise_nested, exc4, exc5) stack.callback(raise_nested, exc2, exc3) raise exc1 @@ -709,7 +857,7 @@ class TestExitStack(unittest.TestCase): self.assertIs(exc.__context__.__context__, exc3) self.assertIs(exc.__context__.__context__.__context__, exc2) self.assertIs( - exc.__context__.__context__.__context__.__context__, exc1) + exc.__context__.__context__.__context__.__context__, exc1) self.assertIsNone( exc.__context__.__context__.__context__.__context__.__context__) @@ -717,21 +865,21 @@ class TestExitStack(unittest.TestCase): def suppress_exc(*exc_details): return True try: - with ExitStack() as stack: + with self.exit_stack() 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: + with self.exit_stack() 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: + with self.exit_stack() as stack: for i in range(10000): stack.callback(int) @@ -739,44 +887,11 @@ class TestExitStack(unittest.TestCase): class Example(object): pass cm = Example() cm.__exit__ = object() - stack = ExitStack() + stack = self.exit_stack() self.assertRaises(AttributeError, stack.enter_context, cm) stack.push(cm) self.assertIs(stack._exit_callbacks[-1][1], cm) - def test_default_class_semantics(self): - # For Python 2.x, this ensures compatibility with old-style classes - # For Python 3.x, it just reruns some of the other tests - class DefaultCM: - def __enter__(self): - result.append("Enter") - def __exit__(self, *exc_details): - result.append("Exit") - class DefaultCallable: - def __call__(self, *exc_details): - result.append("Callback") - - result = [] - cm = DefaultCM() - cb = DefaultCallable() - with ExitStack() as stack: - stack.enter_context(cm) - self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) - stack.push(cb) - stack.push(cm) - self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) - result.append("Running") - stack.callback(cb) - self.assertIs(stack._exit_callbacks[-1][1].__wrapped__, cb) - self.assertEqual(result, ["Enter", "Running", - "Callback", "Exit", - "Callback", "Exit", - ]) - - with ExitStack(): - pass - - def test_dont_reraise_RuntimeError(self): # https://bugs.python.org/issue27122 class UniqueException(Exception): pass @@ -787,10 +902,7 @@ class TestExitStack(unittest.TestCase): try: yield 1 except Exception as exc: - # Py2 compatible explicit exception chaining - new_exc = UniqueException("new exception") - new_exc.__cause__ = exc - raise new_exc + raise UniqueException("new exception") from exc @contextmanager def first(): @@ -802,17 +914,21 @@ class TestExitStack(unittest.TestCase): # The UniqueRuntimeError should be caught by second()'s exception # handler which chain raised a new UniqueException. with self.assertRaises(UniqueException) as err_ctx: - with ExitStack() as es_ctx: + with self.exit_stack() as es_ctx: es_ctx.enter_context(second()) es_ctx.enter_context(first()) raise UniqueRuntimeError("please no infinite loop.") exc = err_ctx.exception self.assertIsInstance(exc, UniqueException) - self.assertIsInstance(exc.__cause__, UniqueRuntimeError) - self.assertIs(exc.__context__, exc.__cause__) - self.assertIsNone(exc.__cause__.__context__) - self.assertIsNone(exc.__cause__.__cause__) + self.assertIsInstance(exc.__context__, UniqueRuntimeError) + self.assertIsNone(exc.__context__.__context__) + self.assertIsNone(exc.__context__.__cause__) + self.assertIs(exc.__cause__, exc.__context__) + + +class TestExitStack(TestBaseExitStack, unittest.TestCase): + exit_stack = ExitStack class TestRedirectStream: @@ -820,7 +936,7 @@ class TestRedirectStream: redirect_stream = None orig_stream = None - @requires_docstrings + @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = self.redirect_stream.__doc__ @@ -871,11 +987,6 @@ class TestRedirectStream: s = f.getvalue() self.assertEqual(s, "Hello World!\n") - def test_cm_is_exitstack_compatible(self): - with ExitStack() as stack: - # This shouldn't raise an exception. - stack.enter_context(self.redirect_stream(io.StringIO())) - class TestRedirectStdout(TestRedirectStream, unittest.TestCase): @@ -891,7 +1002,7 @@ class TestRedirectStderr(TestRedirectStream, unittest.TestCase): class TestSuppress(unittest.TestCase): - @requires_docstrings + @support.requires_docstrings def test_instance_docs(self): # Issue 19330: ensure context manager instances have good docstrings cm_docstring = suppress.__doc__ @@ -943,21 +1054,5 @@ class TestSuppress(unittest.TestCase): 1/0 self.assertTrue(outer_continued) - def test_cm_is_exitstack_compatible(self): - with ExitStack() as stack: - # This shouldn't raise an exception. - stack.enter_context(suppress()) - - -class NullcontextTestCase(unittest.TestCase): - def test_nullcontext(self): - class C: - pass - c = C() - with nullcontext(c) as c_in: - self.assertIs(c_in, c) - - if __name__ == "__main__": - import unittest unittest.main() diff --git a/test/test_contextlib_async.py b/test/test_contextlib_async.py new file mode 100644 index 0000000..c131724 --- /dev/null +++ b/test/test_contextlib_async.py @@ -0,0 +1,551 @@ +import asyncio +from contextlib2 import ( + asynccontextmanager, AbstractAsyncContextManager, + AsyncExitStack, nullcontext, aclosing) +import functools +from test import support +import unittest + +from test.test_contextlib import TestBaseExitStack + + +def _async_test(func): + """Decorator to turn an async function into a test case.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + coro = func(*args, **kwargs) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coro) + finally: + loop.close() + asyncio.set_event_loop_policy(None) + return wrapper + + +class TestAbstractAsyncContextManager(unittest.TestCase): + + @_async_test + async def test_enter(self): + class DefaultEnter(AbstractAsyncContextManager): + async def __aexit__(self, *args): + await super().__aexit__(*args) + + manager = DefaultEnter() + self.assertIs(await manager.__aenter__(), manager) + + async with manager as context: + self.assertIs(manager, context) + + @_async_test + async def test_async_gen_propagates_generator_exit(self): + # A regression test for https://bugs.python.org/issue33786. + + @asynccontextmanager + async def ctx(): + yield + + async def gen(): + async with ctx(): + yield 11 + + ret = [] + exc = ValueError(22) + with self.assertRaises(ValueError): + async with ctx(): + async for val in gen(): + ret.append(val) + raise exc + + self.assertEqual(ret, [11]) + + def test_exit_is_abstract(self): + class MissingAexit(AbstractAsyncContextManager): + pass + + with self.assertRaises(TypeError): + MissingAexit() + + def test_structural_subclassing(self): + class ManagerFromScratch: + async def __aenter__(self): + return self + async def __aexit__(self, exc_type, exc_value, traceback): + return None + + self.assertTrue(issubclass(ManagerFromScratch, AbstractAsyncContextManager)) + + class DefaultEnter(AbstractAsyncContextManager): + async def __aexit__(self, *args): + await super().__aexit__(*args) + + self.assertTrue(issubclass(DefaultEnter, AbstractAsyncContextManager)) + + class NoneAenter(ManagerFromScratch): + __aenter__ = None + + self.assertFalse(issubclass(NoneAenter, AbstractAsyncContextManager)) + + class NoneAexit(ManagerFromScratch): + __aexit__ = None + + self.assertFalse(issubclass(NoneAexit, AbstractAsyncContextManager)) + + +class AsyncContextManagerTestCase(unittest.TestCase): + + @_async_test + async def test_contextmanager_plain(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + yield 42 + state.append(999) + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_finally(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + try: + yield 42 + finally: + state.append(999) + with self.assertRaises(ZeroDivisionError): + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + raise ZeroDivisionError() + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_no_reraise(self): + @asynccontextmanager + async def whee(): + yield + ctx = whee() + await ctx.__aenter__() + # Calling __aexit__ should not result in an exception + self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None)) + + @_async_test + async def test_contextmanager_trap_yield_after_throw(self): + @asynccontextmanager + async def whoo(): + try: + yield + except: + yield + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(RuntimeError): + await ctx.__aexit__(TypeError, TypeError('foo'), None) + + @_async_test + async def test_contextmanager_trap_no_yield(self): + @asynccontextmanager + async def whoo(): + if False: + yield + ctx = whoo() + with self.assertRaises(RuntimeError): + await ctx.__aenter__() + + @_async_test + async def test_contextmanager_trap_second_yield(self): + @asynccontextmanager + async def whoo(): + yield + yield + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(RuntimeError): + await ctx.__aexit__(None, None, None) + + @_async_test + async def test_contextmanager_non_normalised(self): + @asynccontextmanager + async def whoo(): + try: + yield + except RuntimeError: + raise SyntaxError + + ctx = whoo() + await ctx.__aenter__() + with self.assertRaises(SyntaxError): + await ctx.__aexit__(RuntimeError, None, None) + + @_async_test + async def test_contextmanager_except(self): + state = [] + @asynccontextmanager + async def woohoo(): + state.append(1) + try: + yield 42 + except ZeroDivisionError as e: + state.append(e.args[0]) + self.assertEqual(state, [1, 42, 999]) + async with woohoo() as x: + self.assertEqual(state, [1]) + self.assertEqual(x, 42) + state.append(x) + raise ZeroDivisionError(999) + self.assertEqual(state, [1, 42, 999]) + + @_async_test + async def test_contextmanager_except_stopiter(self): + @asynccontextmanager + async def woohoo(): + yield + + for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')): + with self.subTest(type=type(stop_exc)): + try: + async with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail(f'{stop_exc} was suppressed') + + @_async_test + async def test_contextmanager_wrap_runtimeerror(self): + @asynccontextmanager + async def woohoo(): + try: + yield + except Exception as exc: + raise RuntimeError(f'caught {exc}') from exc + + with self.assertRaises(RuntimeError): + async with woohoo(): + 1 / 0 + + # If the context manager wrapped StopAsyncIteration in a RuntimeError, + # we also unwrap it, because we can't tell whether the wrapping was + # done by the generator machinery or by the generator itself. + with self.assertRaises(StopAsyncIteration): + async with woohoo(): + raise StopAsyncIteration + + def _create_contextmanager_attribs(self): + def attribs(**kw): + def decorate(func): + for k,v in kw.items(): + setattr(func,k,v) + return func + return decorate + @asynccontextmanager + @attribs(foo='bar') + async def baz(spam): + """Whee!""" + yield + return baz + + def test_contextmanager_attribs(self): + baz = self._create_contextmanager_attribs() + self.assertEqual(baz.__name__,'baz') + self.assertEqual(baz.foo, 'bar') + + @support.requires_docstrings + def test_contextmanager_doc_attrib(self): + baz = self._create_contextmanager_attribs() + self.assertEqual(baz.__doc__, "Whee!") + + @support.requires_docstrings + @_async_test + async def test_instance_docstring_given_cm_docstring(self): + baz = self._create_contextmanager_attribs()(None) + self.assertEqual(baz.__doc__, "Whee!") + async with baz: + pass # suppress warning + + @_async_test + async def test_keywords(self): + # Ensure no keyword arguments are inhibited + @asynccontextmanager + async def woohoo(self, func, args, kwds): + yield (self, func, args, kwds) + async with woohoo(self=11, func=22, args=33, kwds=44) as target: + self.assertEqual(target, (11, 22, 33, 44)) + + @_async_test + async def test_recursive(self): + depth = 0 + ncols = 0 + + @asynccontextmanager + async def woohoo(): + nonlocal ncols + ncols += 1 + + nonlocal depth + before = depth + depth += 1 + yield + depth -= 1 + self.assertEqual(depth, before) + + @woohoo() + async def recursive(): + if depth < 10: + await recursive() + + await recursive() + + self.assertEqual(ncols, 10) + self.assertEqual(depth, 0) + + +class AclosingTestCase(unittest.TestCase): + + @support.requires_docstrings + def test_instance_docs(self): + cm_docstring = aclosing.__doc__ + obj = aclosing(None) + self.assertEqual(obj.__doc__, cm_docstring) + + @_async_test + async def test_aclosing(self): + state = [] + class C: + async def aclose(self): + state.append(1) + x = C() + self.assertEqual(state, []) + async with aclosing(x) as y: + self.assertEqual(x, y) + self.assertEqual(state, [1]) + + @_async_test + async def test_aclosing_error(self): + state = [] + class C: + async def aclose(self): + state.append(1) + x = C() + self.assertEqual(state, []) + with self.assertRaises(ZeroDivisionError): + async with aclosing(x) as y: + self.assertEqual(x, y) + 1 / 0 + self.assertEqual(state, [1]) + + @_async_test + async def test_aclosing_bpo41229(self): + state = [] + + class Resource: + def __del__(self): + state.append(1) + + async def agenfunc(): + r = Resource() + yield -1 + yield -2 + + x = agenfunc() + self.assertEqual(state, []) + with self.assertRaises(ZeroDivisionError): + async with aclosing(x) as y: + self.assertEqual(x, y) + self.assertEqual(-1, await x.__anext__()) + 1 / 0 + self.assertEqual(state, [1]) + + +class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): + class SyncAsyncExitStack(AsyncExitStack): + @staticmethod + def run_coroutine(coro): + loop = asyncio.get_event_loop_policy().get_event_loop() + t = loop.create_task(coro) + t.add_done_callback(lambda f: loop.stop()) + loop.run_forever() + + exc = t.exception() + if not exc: + return t.result() + else: + context = exc.__context__ + + try: + raise exc + except: + exc.__context__ = context + raise exc + + def close(self): + return self.run_coroutine(self.aclose()) + + def __enter__(self): + return self.run_coroutine(self.__aenter__()) + + def __exit__(self, *exc_details): + return self.run_coroutine(self.__aexit__(*exc_details)) + + exit_stack = SyncAsyncExitStack + + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + self.addCleanup(self.loop.close) + self.addCleanup(asyncio.set_event_loop_policy, None) + + @_async_test + async def test_async_callback(self): + expected = [ + ((), {}), + ((1,), {}), + ((1,2), {}), + ((), dict(example=1)), + ((1,), dict(example=1)), + ((1,2), dict(example=1)), + ] + result = [] + async def _exit(*args, **kwds): + """Test metadata propagation""" + result.append((args, kwds)) + + async with AsyncExitStack() as stack: + for args, kwds in reversed(expected): + if args and kwds: + f = stack.push_async_callback(_exit, *args, **kwds) + elif args: + f = stack.push_async_callback(_exit, *args) + elif kwds: + f = stack.push_async_callback(_exit, **kwds) + else: + f = stack.push_async_callback(_exit) + self.assertIs(f, _exit) + for wrapper in stack._exit_callbacks: + self.assertIs(wrapper[1].__wrapped__, _exit) + self.assertNotEqual(wrapper[1].__name__, _exit.__name__) + self.assertIsNone(wrapper[1].__doc__, _exit.__doc__) + + self.assertEqual(result, expected) + + result = [] + async with AsyncExitStack() as stack: + with self.assertRaises(TypeError): + stack.push_async_callback(arg=1) + with self.assertRaises(TypeError): + self.exit_stack.push_async_callback(arg=2) + with self.assertRaises(TypeError): + stack.push_async_callback(callback=_exit, arg=3) + self.assertEqual(result, []) + + @_async_test + async def test_async_push(self): + exc_raised = ZeroDivisionError + async def _expect_exc(exc_type, exc, exc_tb): + self.assertIs(exc_type, exc_raised) + async def _suppress_exc(*exc_details): + return True + async 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 + async def __aenter__(self): + self.fail("Should not be called!") + async def __aexit__(self, *exc_details): + await self.check_exc(*exc_details) + + async with self.exit_stack() as stack: + stack.push_async_exit(_expect_ok) + self.assertIs(stack._exit_callbacks[-1][1], _expect_ok) + cm = ExitCM(_expect_ok) + stack.push_async_exit(cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) + stack.push_async_exit(_suppress_exc) + self.assertIs(stack._exit_callbacks[-1][1], _suppress_exc) + cm = ExitCM(_expect_exc) + stack.push_async_exit(cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) + stack.push_async_exit(_expect_exc) + self.assertIs(stack._exit_callbacks[-1][1], _expect_exc) + stack.push_async_exit(_expect_exc) + self.assertIs(stack._exit_callbacks[-1][1], _expect_exc) + 1/0 + + @_async_test + async def test_async_enter_context(self): + class TestCM(object): + async def __aenter__(self): + result.append(1) + async def __aexit__(self, *exc_details): + result.append(3) + + result = [] + cm = TestCM() + + async with AsyncExitStack() as stack: + @stack.push_async_callback # Registered first => cleaned up last + async def _exit(): + result.append(4) + self.assertIsNotNone(_exit) + await stack.enter_async_context(cm) + self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) + result.append(2) + + self.assertEqual(result, [1, 2, 3, 4]) + + @_async_test + async def test_async_exit_exception_chaining(self): + # Ensure exception chaining matches the reference behaviour + async def raise_exc(exc): + raise exc + + saved_details = None + async def suppress_exc(*exc_details): + nonlocal saved_details + saved_details = exc_details + return True + + try: + async with self.exit_stack() as stack: + stack.push_async_callback(raise_exc, IndexError) + stack.push_async_callback(raise_exc, KeyError) + stack.push_async_callback(raise_exc, AttributeError) + stack.push_async_exit(suppress_exc) + stack.push_async_callback(raise_exc, ValueError) + 1 / 0 + except IndexError as exc: + 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[1] + self.assertIsInstance(inner_exc, ValueError) + self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + + +class TestAsyncNullcontext(unittest.TestCase): + @_async_test + async def test_async_nullcontext(self): + class C: + pass + c = C() + async with nullcontext(c) as c_in: + self.assertIs(c_in, c) + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini index 77b66ed..6b0c293 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ skip_missing_interpreters = True [testenv] commands = - coverage run test_contextlib2.py + coverage run -m unittest discover -t . -s test coverage report coverage xml deps = From 032662d6e9fb7725712004d6f5d62c289d08d1a5 Mon Sep 17 00:00:00 2001 From: Nick Coghlan Date: Sat, 26 Jun 2021 18:30:55 +1000 Subject: [PATCH 7/7] Fix test incompatibility with PyPy3 --- MANIFEST.in | 2 +- test/test_contextlib.py | 3 +++ test/test_contextlib_async.py | 15 +++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 899523c..5b839f9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include *.py *.txt *.rst *.md *.ini MANIFEST.in -recursive-include test docs *.rst *.py make.bat Makefile +recursive-include dev test docs *.rst *.py make.bat Makefile diff --git a/test/test_contextlib.py b/test/test_contextlib.py index 8a9176b..f09090a 100644 --- a/test/test_contextlib.py +++ b/test/test_contextlib.py @@ -9,6 +9,7 @@ from contextlib2 import * # Tests __all__ from test import support from test.support import os_helper import weakref +import gc class TestAbstractContextManager(unittest.TestCase): @@ -228,6 +229,8 @@ def woohoo(): def woohoo(a, b): a = weakref.ref(a) b = weakref.ref(b) + # Allow test to work with a non-refcounted GC + gc.collect(); gc.collect(); gc.collect() self.assertIsNone(a()) self.assertIsNone(b()) yield diff --git a/test/test_contextlib_async.py b/test/test_contextlib_async.py index c131724..eb2ed72 100644 --- a/test/test_contextlib_async.py +++ b/test/test_contextlib_async.py @@ -1,7 +1,7 @@ import asyncio from contextlib2 import ( asynccontextmanager, AbstractAsyncContextManager, - AsyncExitStack, nullcontext, aclosing) + AsyncExitStack, nullcontext, aclosing, contextmanager) import functools from test import support import unittest @@ -346,14 +346,17 @@ class AclosingTestCase(unittest.TestCase): async def test_aclosing_bpo41229(self): state = [] - class Resource: - def __del__(self): + @contextmanager + def sync_resource(): + try: + yield + finally: state.append(1) async def agenfunc(): - r = Resource() - yield -1 - yield -2 + with sync_resource(): + yield -1 + yield -2 x = agenfunc() self.assertEqual(state, [])