From f64cf04df8a1f6a32ce2095192b4638d229ff25e Mon Sep 17 00:00:00 2001 From: Alyssa Coghlan Date: Thu, 23 May 2024 17:55:20 +1000 Subject: [PATCH] Sync with CPython 3.12.3 (#60) * generated new diff files covering this sync * added some helper scripts for the sync process * fixed RTD theme regression (due to RTD changes since the last release) Closes #43 --- .gitignore | 4 + .readthedocs.yaml | 6 +- MANIFEST.in | 4 +- NEWS.rst | 16 +- README.rst | 40 +- contextlib2/__init__.py | 312 +++++++----- contextlib2/__init__.pyi | 25 +- ...10_contextlib_pyi_to_contextlib2_pyi.patch | 79 --- ...10_contextlib_rst_to_contextlib2_rst.patch | 194 -------- dev/py3_10_contextlib_to_contextlib2.patch | 147 ------ dev/py3_12_py_to_contextlib2.patch | 116 +++++ dev/py3_12_pyi_to_contextlib2.patch | 112 +++++ dev/py3_12_rst_to_contextlib2.patch | 453 ++++++++++++++++++ dev/py3_12_test_async_to_contextlib2.patch | 67 +++ dev/py3_12_test_to_contextlib2.patch | 106 ++++ dev/save_diff_snapshot.sh | 27 ++ dev/sync_from_cpython.sh | 19 + dev/typeshed_contextlib.pyi | 209 ++++++++ docs/conf.py | 13 +- docs/contextlib2.rst | 171 ++++--- docs/index.rst | 15 +- docs/requirements.txt | 1 + test/data/README.txt | 1 + test/support/__init__.py | 95 ++++ test/support/testcase.py | 33 ++ test/test_contextlib.py | 349 +++++++++++++- test/test_contextlib_async.py | 262 +++++++++- test/ziptestdata/README.txt | 1 + 28 files changed, 2196 insertions(+), 681 deletions(-) delete mode 100644 dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch delete mode 100644 dev/py3_10_contextlib_rst_to_contextlib2_rst.patch delete mode 100644 dev/py3_10_contextlib_to_contextlib2.patch create mode 100644 dev/py3_12_py_to_contextlib2.patch create mode 100644 dev/py3_12_pyi_to_contextlib2.patch create mode 100644 dev/py3_12_rst_to_contextlib2.patch create mode 100644 dev/py3_12_test_async_to_contextlib2.patch create mode 100644 dev/py3_12_test_to_contextlib2.patch create mode 100755 dev/save_diff_snapshot.sh create mode 100755 dev/sync_from_cpython.sh create mode 100644 dev/typeshed_contextlib.pyi create mode 100644 docs/requirements.txt create mode 100644 test/data/README.txt create mode 100644 test/support/testcase.py create mode 100644 test/ziptestdata/README.txt diff --git a/.gitignore b/.gitignore index d8334e3..b581368 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ MANIFEST .coverage coverage.xml htmlcov/ + +# Patching output files +*.orig +*.rej diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dd2aa46..31dbf0d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -30,6 +30,6 @@ sphinx: # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt +python: + install: + - requirements: docs/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index d67e69f..0269b69 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ -include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in dev/mypy.allowlist +include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in recursive-include contextlib2 *.py *.pyi py.typed recursive-include docs *.rst *.py make.bat Makefile recursive-include test *.py -recursive-include dev *.patch +recursive-include dev *.patch *.allowlist *.sh diff --git a/NEWS.rst b/NEWS.rst index 1d56854..a7aa7e3 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -4,8 +4,22 @@ Release History 24.6.0 (2024-06-??) ^^^^^^^^^^^^^^^^^^^ -* Due to the use of positional-only argument syntax, the minimum supported +* To allow the use of positional-only argument syntax, the minimum supported Python version is now Python 3.8. +* Synchronised with the Python 3.12.3 (and 3.13.0) version of contextlib + (`#12 `__), making the + following new features available on Python 3.8+: + + * :class:`chdir` (added in Python 3.11) + * :func:`suppress` filters the contents of ``BaseExceptionGroup`` (Python 3.12) + * improved handling of :class:`StopIteration` subclasses (Python 3.11) +* The exception thrown by :meth:`ExitStack.enter_context` and + :meth:`AsyncExitStack.enter_async_context` when the given object does not + implement the relevant context management protocol is now version-dependent + (:class:`TypeError` on 3.11+, :class:`AttributeError` on earlier versions). + This provides consistency with the ``with`` and ``async with`` behaviour on + the corresponding versions. +* No longer needed object references are now released more promptly * Update ``mypy stubtest`` to work with recent mypy versions (mypy 1.8.0 tested) (`#54 `__) * The ``dev/mypy.allowlist`` file needed for the ``mypy stubtest`` step in the diff --git a/README.rst b/README.rst index 6274896..c285a88 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ The one exception is the included type hints file, which comes from the Development ----------- -contextlib2 has no runtime dependencies, but requires ``setuptools`` and +``contextlib2`` has no runtime dependencies, but requires ``setuptools`` and ``wheel`` at build time to generate universal wheel archives. Local testing is a matter of running:: @@ -49,20 +49,20 @@ You can test against multiple versions of Python with Versions currently tested in both tox and GitHub Actions are: -* CPython 3.6 -* CPython 3.7 * CPython 3.8 * CPython 3.9 * CPython 3.10 -* PyPy3 +* CPython 3.11 +* CPython 3.12 +* PyPy3 (specifically 3.10 in GitHub Actions) Updating to a new stdlib reference version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -As of Python 3.10, 4 files needed to be copied from the CPython reference +As of Python 3.12.3, 4 files needed to be copied from the CPython reference implementation to contextlib2: -* ``Doc/contextlib.rst`` -> ``docs/contextlib2.rst`` +* ``Doc/library/contextlib.rst`` -> ``docs/contextlib2.rst`` * ``Lib/contextlib.py`` -> ``contextlib2/__init__.py`` * ``Lib/test/test_contextlib.py`` -> ``test/test_contextlib.py`` * ``Lib/test/test_contextlib_async.py`` -> ``test/test_contextlib_async.py`` @@ -72,19 +72,31 @@ retrieved from the ``typeshed`` project:: wget https://raw.githubusercontent.com/python/typeshed/master/stdlib/contextlib.pyi -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 following patch files are saved in the ``dev`` directory: -* changes made to ``contextlib2/__init__.py`` to get it to run on the older +* changes to ``contextlib2/__init__.py`` 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) -* changes made to ``contextlib2/__init__.pyi`` to make the Python version +* changes to ``test/test_contextlib.py`` and ``test/test_contextlib_async.py`` + to get them to run on the older versions +* changes to ``contextlib2/__init__.pyi`` to make the Python version guards unconditional (since the ``contextlib2`` API is the same on all supported versions) -* changes made to ``docs/contextlib2.rst`` to use ``contextlib2`` version +* changes to ``docs/contextlib2.rst`` to use ``contextlib2`` version numbers in the version added/changed notes and to integrate the module documentation with the rest of the project documentation + +When the upstream changes between releases are minor, these patch files may be +used directly to reapply the ``contextlib2`` specific changes after syncing a +new version. Even when the patches do not apply cleanly, they're still a useful +guide as to the changes that are needed to restore compatibility with older +Python versions and make any other ``contextlib2`` specific updates. + +The test directory is laid out so that the test suite's imports from +``test.support`` work the same way as they do in the main CPython test suite. +These files are selective copies rather than complete ones as the ``contextlib`` +tests only need a tiny fraction of the features available in the real +``test.support`` module. + +The ``dev/sync_from_cpython.sh`` and ``dev/save_diff_snapshot.sh`` scripts +automate some of the steps in the sync process. diff --git a/contextlib2/__init__.py b/contextlib2/__init__.py index 33b3b47..8278201 100644 --- a/contextlib2/__init__.py +++ b/contextlib2/__init__.py @@ -1,30 +1,60 @@ -"""contextlib2 - backports and enhancements to the contextlib module""" - +"""Utilities for with-statement contexts. See PEP 343.""" import abc +import os import sys -import warnings import _collections_abc from collections import deque from functools import wraps from types import MethodType -# Python 3.7/3.8 compatibility: GenericAlias may not be defined +# Python 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 + # (typecheckers may still be unhappy, but for that problem the answer is + # "use a newer Python version with better typechecking support") class GenericAlias: pass +# Python 3.10 and earlier compatibility: BaseExceptionGroup may not be defined +try: + BaseExceptionGroup +except NameError: + # If the real BaseExceptionGroup type doesn't exist, it will never actually + # be raised. This means the fallback placeholder doesn't need to provide + # any meaningful behaviour, it just needs to be compatible with 'issubclass' + class BaseExceptionGroup(BaseException): + pass + +# Python 3.9 and earlier compatibility: anext may not be defined +try: + anext +except NameError: + def anext(obj, /): + return obj.__anext__() + +# Python 3.11+ behaviour consistency: replace AttributeError with TypeError +if sys.version_info >= (3, 11): + # enter_context() and enter_async_context() follow the change in the + # exception type raised by with statements and async with statements + _CL2_ERROR_TO_CONVERT = AttributeError +else: + # On older versions, raise AttributeError without any changes + class _CL2_ERROR_TO_CONVERT(Exception): + pass + __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", "AbstractContextManager", "AbstractAsyncContextManager", "AsyncExitStack", "ContextDecorator", "ExitStack", - "redirect_stdout", "redirect_stderr", "suppress", "aclosing"] + "redirect_stdout", "redirect_stderr", "suppress", "aclosing", + "chdir"] class AbstractContextManager(abc.ABC): + """An abstract base class for context managers.""" __class_getitem__ = classmethod(GenericAlias) @@ -84,6 +114,7 @@ class ContextDecorator(object): DEPRECATED: refresh_cm was never added to the standard library's ContextDecorator API """ + import warnings # Only import if needed for the deprecation warning warnings.warn("refresh_cm was never added to the standard library", DeprecationWarning) return self._recreate_cm() @@ -141,18 +172,20 @@ class _GeneratorContextManagerBase: # 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 + # _GCMB 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) + +class _GeneratorContextManager( + _GeneratorContextManagerBase, + AbstractContextManager, + ContextDecorator, +): + """Helper for @contextmanager decorator.""" + def __enter__(self): # do not keep args and kwds alive unnecessarily # they are only needed for recreation, which is not possible anymore @@ -162,21 +195,24 @@ class _GeneratorContextManager(_GeneratorContextManagerBase, except StopIteration: raise RuntimeError("generator didn't yield") from None - def __exit__(self, type, value, traceback): - if type is None: + def __exit__(self, typ, value, traceback): + if typ is None: try: next(self.gen) except StopIteration: return False else: - raise RuntimeError("generator didn't stop") + try: + raise RuntimeError("generator didn't stop") + finally: + self.gen.close() else: if value is None: # Need to force instantiation so we can reliably # tell if we get the same exception back - value = type() + value = typ() try: - self.gen.throw(type, value, traceback) + self.gen.throw(value) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration @@ -185,68 +221,7 @@ class _GeneratorContextManager(_GeneratorContextManagerBase, except RuntimeError as exc: # 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 type is StopIteration 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 - # an exception unless __exit__() itself failed. But throw() - # has to raise the exception to signal propagation, so this - # fixes the impedance mismatch between the throw() protocol - # and the __exit__() protocol. - # - # 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: + exc.__traceback__ = traceback return False # Avoid suppressing if a StopIteration exception # was passed to throw() and later wrapped into a RuntimeError @@ -254,13 +229,101 @@ class _AsyncGeneratorContextManager(_GeneratorContextManagerBase, # 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 + if ( + isinstance(value, StopIteration) + and exc.__cause__ is value + ): + value.__traceback__ = traceback + return False raise except BaseException as exc: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. if exc is not value: raise + exc.__traceback__ = traceback + return False + try: + raise RuntimeError("generator didn't stop after throw()") + finally: + self.gen.close() + +class _AsyncGeneratorContextManager( + _GeneratorContextManagerBase, + AbstractAsyncContextManager, + AsyncContextDecorator, +): + """Helper for @asynccontextmanager decorator.""" + + async def __aenter__(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 await anext(self.gen) + except StopAsyncIteration: + raise RuntimeError("generator didn't yield") from None + + async def __aexit__(self, typ, value, traceback): + if typ is None: + try: + await anext(self.gen) + except StopAsyncIteration: + return False + else: + try: + raise RuntimeError("generator didn't stop") + finally: + await self.gen.aclose() + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = typ() + try: + await self.gen.athrow(value) + except StopAsyncIteration 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. (issue27122) + if exc is value: + exc.__traceback__ = traceback + return False + # Avoid suppressing if a Stop(Async)Iteration exception + # was passed to athrow() 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)) + and exc.__cause__ is value + ): + value.__traceback__ = traceback + return False + raise + except BaseException as exc: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. + if exc is not value: + raise + exc.__traceback__ = traceback + return False + try: + raise RuntimeError("generator didn't stop after athrow()") + finally: + await self.gen.aclose() def contextmanager(func): @@ -447,7 +510,16 @@ class suppress(AbstractContextManager): # 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) + if exctype is None: + return + if issubclass(exctype, self._exceptions): + return True + if issubclass(exctype, BaseExceptionGroup): + match, rest = excinst.split(self._exceptions) + if rest is None: + return True + raise rest + return False class _BaseExitStack: @@ -458,9 +530,7 @@ class _BaseExitStack: return MethodType(cm_exit, cm) @staticmethod - def _create_cb_wrapper(*args, **kwds): - # Python 3.6/3.7 compatibility: no native positional-only args syntax - callback, *args = args + def _create_cb_wrapper(callback, /, *args, **kwds): def _exit_wrapper(exc_type, exc, tb): callback(*args, **kwds) return _exit_wrapper @@ -503,24 +573,22 @@ class _BaseExitStack: """ # 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) + cls = type(cm) + try: + _enter = cls.__enter__ + _exit = cls.__exit__ + except _CL2_ERROR_TO_CONVERT: + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + f"not support the context manager protocol") from None + result = _enter(cm) self._push_cm_exit(cm, _exit) return result - def callback(*args, **kwds): + def callback(self, 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 @@ -563,10 +631,10 @@ class ExitStack(_BaseExitStack, AbstractContextManager): # 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: + if exc_context is None or 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: + if exc_context is frame_exc: break new_exc = exc_context # Change the end of the chain to point to the exception @@ -626,9 +694,7 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): return MethodType(cm_exit, cm) @staticmethod - def _create_async_cb_wrapper(*args, **kwds): - # Python 3.6/3.7 compatibility: no native positional-only args syntax - callback, *args = args + def _create_async_cb_wrapper(callback, /, *args, **kwds): async def _exit_wrapper(exc_type, exc, tb): await callback(*args, **kwds) return _exit_wrapper @@ -639,9 +705,15 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): 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) + cls = type(cm) + try: + _enter = cls.__aenter__ + _exit = cls.__aexit__ + except _CL2_ERROR_TO_CONVERT: + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + f"not support the asynchronous context manager protocol" + ) from None + result = await _enter(cm) self._push_async_cm_exit(cm, _exit) return result @@ -663,18 +735,11 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): self._push_async_cm_exit(exit, exit_method) return exit # Allow use as a decorator - def push_async_callback(*args, **kwds): + def push_async_callback(self, 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 @@ -706,10 +771,10 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): # 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: + if exc_context is None or 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: + if exc_context is frame_exc: break new_exc = exc_context # Change the end of the chain to point to the exception @@ -777,11 +842,26 @@ class nullcontext(AbstractContextManager, AbstractAsyncContextManager): pass +class chdir(AbstractContextManager): + """Non thread-safe context manager to change the current working directory.""" + + def __init__(self, path): + self.path = path + self._old_cwd = [] + + def __enter__(self): + self._old_cwd.append(os.getcwd()) + os.chdir(self.path) + + def __exit__(self, *excinfo): + os.chdir(self._old_cwd.pop()) + # Preserve backwards compatibility class ContextStack(ExitStack): - """Backwards compatibility alias for ExitStack""" + """(DEPRECATED) Backwards compatibility alias for ExitStack""" def __init__(self): + import warnings # Only import if needed for the deprecation warning warnings.warn("ContextStack has been renamed to ExitStack", DeprecationWarning) super(ContextStack, self).__init__() diff --git a/contextlib2/__init__.pyi b/contextlib2/__init__.pyi index 1f2dd67..2d4b977 100644 --- a/contextlib2/__init__.pyi +++ b/contextlib2/__init__.pyi @@ -5,6 +5,7 @@ # Last updated: 2024-05-22 # Updated from: https://github.com/python/typeshed/blob/aa2d33df211e1e4f70883388febf750ac524d2bb/stdlib/contextlib.pyi +# Saved to: dev/typeshed_contextlib.pyi # contextlib2 API adaptation notes: # * the various 'if True:' guards replace sys.version checks in the original @@ -41,8 +42,8 @@ __all__ = [ if True: __all__ += ["aclosing"] -# if True: -# __all__ += ["chdir"] +if True: + __all__ += ["chdir"] _T = TypeVar("_T") _T_co = TypeVar("_T_co", covariant=True) @@ -81,14 +82,10 @@ class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], Conte func: Callable[..., Generator[_T_co, Any, Any]] args: tuple[Any, ...] kwds: dict[str, Any] - if False: + if True: def __exit__( self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None ) -> bool | None: ... - else: - def __exit__( - self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool | None: ... def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... @@ -194,11 +191,11 @@ if True: async def __aenter__(self) -> _T: ... async def __aexit__(self, *exctype: Unused) -> None: ... -# if True: -# _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) +if True: + _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) -# class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): -# path: _T_fd_or_any_path -# def __init__(self, path: _T_fd_or_any_path) -> None: ... -# def __enter__(self) -> None: ... -# def __exit__(self, *excinfo: Unused) -> None: ... + class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): + path: _T_fd_or_any_path + def __init__(self, path: _T_fd_or_any_path) -> None: ... + def __enter__(self) -> None: ... + def __exit__(self, *excinfo: Unused) -> None: ... diff --git a/dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch b/dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch deleted file mode 100644 index 31d9882..0000000 --- a/dev/py3_10_contextlib_pyi_to_contextlib2_pyi.patch +++ /dev/null @@ -1,79 +0,0 @@ ---- ../contextlib.pyi 2021-07-16 08:26:16.409945194 +0100 -+++ contextlib2/__init__.pyi 2021-07-20 14:10:49.571136279 +0100 -@@ -1,5 +1,8 @@ -+# Type hints copied from the typeshed project under the Apache License 2.0 -+# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE -+ - import sys --from _typeshed import Self -+from ._typeshed import Self - from types import TracebackType - from typing import ( - IO, -@@ -17,8 +20,14 @@ - ) - from typing_extensions import ParamSpec, Protocol - -+# contextlib2 API adaptation notes: -+# * the various 'if True:' guards replace sys.version checks in the original -+# typeshed file (those APIs are available on all supported versions) -+# * deliberately omitted APIs are listed in `dev/mypy.allowlist` -+# (e.g. deprecated experimental APIs that never graduated to the stdlib) -+ - AbstractContextManager = ContextManager --if sys.version_info >= (3, 7): -+if True: - AbstractAsyncContextManager = AsyncContextManager - - _T = TypeVar("_T") -@@ -36,7 +45,7 @@ - # type ignore to deal with incomplete ParamSpec support in mypy - def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, _GeneratorContextManager[_T]]: ... # type: ignore - --if sys.version_info >= (3, 7): -+if True: - def asynccontextmanager(func: Callable[_P, AsyncIterator[_T]]) -> Callable[_P, AsyncContextManager[_T]]: ... # type: ignore - - class _SupportsClose(Protocol): -@@ -47,7 +56,7 @@ - class closing(ContextManager[_SupportsCloseT]): - def __init__(self, thing: _SupportsCloseT) -> None: ... - --if sys.version_info >= (3, 10): -+if True: - class _SupportsAclose(Protocol): - def aclose(self) -> Awaitable[object]: ... - _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) -@@ -87,7 +96,7 @@ - __traceback: Optional[TracebackType], - ) -> bool: ... - --if sys.version_info >= (3, 7): -+if True: - _ExitCoroFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], Awaitable[bool]] - _CallbackCoroFunc = Callable[..., Awaitable[Any]] - _ACM_EF = TypeVar("_ACM_EF", AsyncContextManager[Any], _ExitCoroFunc) -@@ -109,7 +118,8 @@ - __traceback: Optional[TracebackType], - ) -> Awaitable[bool]: ... - --if sys.version_info >= (3, 10): -+ -+if True: - class nullcontext(AbstractContextManager[_T], AbstractAsyncContextManager[_T]): - enter_result: _T - @overload -@@ -120,13 +130,3 @@ - def __exit__(self, *exctype: Any) -> None: ... - async def __aenter__(self) -> _T: ... - async def __aexit__(self, *exctype: Any) -> None: ... -- --elif sys.version_info >= (3, 7): -- class nullcontext(AbstractContextManager[_T]): -- enter_result: _T -- @overload -- def __init__(self: nullcontext[None], enter_result: None = ...) -> None: ... -- @overload -- def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... -- def __enter__(self) -> _T: ... -- def __exit__(self, *exctype: Any) -> None: ... diff --git a/dev/py3_10_contextlib_rst_to_contextlib2_rst.patch b/dev/py3_10_contextlib_rst_to_contextlib2_rst.patch deleted file mode 100644 index 959b39c..0000000 --- a/dev/py3_10_contextlib_rst_to_contextlib2_rst.patch +++ /dev/null @@ -1,194 +0,0 @@ ---- ../cpython/Doc/library/contextlib.rst 2021-06-26 18:31:45.179532455 +1000 -+++ docs/contextlib2.rst 2021-06-26 21:19:00.172517765 +1000 -@@ -1,20 +1,5 @@ --:mod:`!contextlib` --- Utilities for :keyword:`!with`\ -statement contexts --========================================================================== -- --.. module:: contextlib -- :synopsis: Utilities for with-statement contexts. -- --**Source code:** :source:`Lib/contextlib.py` -- ---------------- -- --This module provides utilities for common tasks involving the :keyword:`with` --statement. For more information see also :ref:`typecontextmanager` and --:ref:`context-managers`. -- -- --Utilities ----------- -+API Reference -+------------- - - Functions and classes provided: - -@@ -26,8 +11,8 @@ - ``self`` while :meth:`object.__exit__` is an abstract method which by default - returns ``None``. See also the definition of :ref:`typecontextmanager`. - -- .. versionadded:: 3.6 -- -+ .. versionadded:: 0.6.0 -+ Part of the standard library in Python 3.6 and later - - .. class:: AbstractAsyncContextManager - -@@ -38,8 +23,8 @@ - returns ``None``. See also the definition of - :ref:`async-context-managers`. - -- .. versionadded:: 3.7 -- -+ .. versionadded:: 21.6.0 -+ Part of the standard library in Python 3.7 and later - - .. decorator:: contextmanager - -@@ -93,9 +78,6 @@ - created by :func:`contextmanager` to meet the requirement that context - managers support multiple invocations in order to be used as decorators). - -- .. versionchanged:: 3.2 -- Use of :class:`ContextDecorator`. -- - - .. decorator:: asynccontextmanager - -@@ -124,7 +106,10 @@ - async with get_connection() as conn: - return conn.query('SELECT ...') - -- .. versionadded:: 3.7 -+ .. versionadded:: 21.6.0 -+ Part of the standard library in Python 3.7 and later, enhanced in -+ Python 3.10 and later to allow created async context managers to be used -+ as async function decorators. - - Context managers defined with :func:`asynccontextmanager` can be used - either as decorators or with :keyword:`async with` statements:: -@@ -147,10 +132,6 @@ - created by :func:`asynccontextmanager` to meet the requirement that context - managers support multiple invocations in order to be used as decorators. - -- .. versionchanged:: 3.10 -- Async context managers created with :func:`asynccontextmanager` can -- be used as decorators. -- - - .. function:: closing(thing) - -@@ -209,7 +190,8 @@ - variables work as expected, and the exit code isn't run after the - lifetime of some task it depends on). - -- .. versionadded:: 3.10 -+ .. versionadded:: 21.6.0 -+ Part of the standard library in Python 3.10 and later - - - .. _simplifying-support-for-single-optional-context-managers: -@@ -257,11 +239,11 @@ - async with cm as session: - # Send http requests with session - -- .. versionadded:: 3.7 -- -- .. versionchanged:: 3.10 -- :term:`asynchronous context manager` support was added. -+ .. versionadded:: 0.6.0 -+ Part of the standard library in Python 3.7 and later - -+ .. versionchanged:: 21.6.0 -+ Updated to Python 3.10 version with :term:`asynchronous context manager` support - - - .. function:: suppress(*exceptions) -@@ -300,7 +282,8 @@ - - This context manager is :ref:`reentrant `. - -- .. versionadded:: 3.4 -+ .. versionadded:: 0.5 -+ Part of the standard library in Python 3.4 and later - - - .. function:: redirect_stdout(new_target) -@@ -340,7 +323,8 @@ - - This context manager is :ref:`reentrant `. - -- .. versionadded:: 3.4 -+ .. versionadded:: 0.5 -+ Part of the standard library in Python 3.4 and later - - - .. function:: redirect_stderr(new_target) -@@ -350,7 +334,8 @@ - - This context manager is :ref:`reentrant `. - -- .. versionadded:: 3.5 -+ .. versionadded:: 0.5 -+ Part of the standard library in Python 3.5 and later - - - .. class:: ContextDecorator() -@@ -426,8 +411,6 @@ - statements. If this is not the case, then the original construct with the - explicit :keyword:`!with` statement inside the function should be used. - -- .. versionadded:: 3.2 -- - - .. class:: AsyncContextDecorator - -@@ -465,7 +448,8 @@ - The bit in the middle - Finishing - -- .. versionadded:: 3.10 -+ .. versionadded:: 21.6.0 -+ Part of the standard library in Python 3.10 and later - - - .. class:: ExitStack() -@@ -504,7 +488,8 @@ - foundation for higher level context managers that manipulate the exit - stack in application specific ways. - -- .. versionadded:: 3.3 -+ .. versionadded:: 0.4 -+ Part of the standard library in Python 3.3 and later - - .. method:: enter_context(cm) - -@@ -580,7 +565,7 @@ - The :meth:`close` method is not implemented, :meth:`aclose` must be used - instead. - -- .. coroutinemethod:: enter_async_context(cm) -+ .. method:: enter_async_context(cm) - - Similar to :meth:`enter_context` but expects an asynchronous context - manager. -@@ -594,7 +579,7 @@ - - Similar to :meth:`callback` but expects a coroutine function. - -- .. coroutinemethod:: aclose() -+ .. method:: aclose() - - Similar to :meth:`close` but properly handles awaitables. - -@@ -607,7 +592,9 @@ - # the async with statement, even if attempts to open a connection - # later in the list raise an exception. - -- .. versionadded:: 3.7 -+ .. versionadded:: 21.6.0 -+ Part of the standard library in Python 3.7 and later -+ - - Examples and Recipes - -------------------- diff --git a/dev/py3_10_contextlib_to_contextlib2.patch b/dev/py3_10_contextlib_to_contextlib2.patch deleted file mode 100644 index 41d8919..0000000 --- a/dev/py3_10_contextlib_to_contextlib2.patch +++ /dev/null @@ -1,147 +0,0 @@ ---- ../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/dev/py3_12_py_to_contextlib2.patch b/dev/py3_12_py_to_contextlib2.patch new file mode 100644 index 0000000..a73b309 --- /dev/null +++ b/dev/py3_12_py_to_contextlib2.patch @@ -0,0 +1,116 @@ +--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/contextlib.py 2024-05-23 11:57:09.210023505 +1000 ++++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.py 2024-05-23 17:05:06.549142813 +1000 +@@ -5,7 +5,46 @@ + import _collections_abc + from collections import deque + from functools import wraps +-from types import MethodType, GenericAlias ++from types import MethodType ++ ++# Python 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 ++ # (typecheckers may still be unhappy, but for that problem the answer is ++ # "use a newer Python version with better typechecking support") ++ class GenericAlias: ++ pass ++ ++# Python 3.10 and earlier compatibility: BaseExceptionGroup may not be defined ++try: ++ BaseExceptionGroup ++except NameError: ++ # If the real BaseExceptionGroup type doesn't exist, it will never actually ++ # be raised. This means the fallback placeholder doesn't need to provide ++ # any meaningful behaviour, it just needs to be compatible with 'issubclass' ++ class BaseExceptionGroup(BaseException): ++ pass ++ ++# Python 3.9 and earlier compatibility: anext may not be defined ++try: ++ anext ++except NameError: ++ def anext(obj, /): ++ return obj.__anext__() ++ ++# Python 3.11+ behaviour consistency: replace AttributeError with TypeError ++if sys.version_info >= (3, 11): ++ # enter_context() and enter_async_context() follow the change in the ++ # exception type raised by with statements and async with statements ++ _CL2_ERROR_TO_CONVERT = AttributeError ++else: ++ # On older versions, raise AttributeError without any changes ++ class _CL2_ERROR_TO_CONVERT(Exception): ++ pass ++ + + __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", + "AbstractContextManager", "AbstractAsyncContextManager", +@@ -62,6 +101,24 @@ + 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 ++ """ ++ import warnings # Only import if needed for the deprecation warning ++ 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. + +@@ -520,7 +577,7 @@ + try: + _enter = cls.__enter__ + _exit = cls.__exit__ +- except AttributeError: ++ except _CL2_ERROR_TO_CONVERT: + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + f"not support the context manager protocol") from None + result = _enter(cm) +@@ -652,7 +709,7 @@ + try: + _enter = cls.__aenter__ + _exit = cls.__aexit__ +- except AttributeError: ++ except _CL2_ERROR_TO_CONVERT: + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + f"not support the asynchronous context manager protocol" + ) from None +@@ -798,3 +855,22 @@ + + def __exit__(self, *excinfo): + os.chdir(self._old_cwd.pop()) ++ ++# Preserve backwards compatibility ++class ContextStack(ExitStack): ++ """(DEPRECATED) Backwards compatibility alias for ExitStack""" ++ ++ def __init__(self): ++ import warnings # Only import if needed for the deprecation warning ++ 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/dev/py3_12_pyi_to_contextlib2.patch b/dev/py3_12_pyi_to_contextlib2.patch new file mode 100644 index 0000000..a637cd9 --- /dev/null +++ b/dev/py3_12_pyi_to_contextlib2.patch @@ -0,0 +1,112 @@ +--- /home/ncoghlan/devel/contextlib2/dev/typeshed_contextlib.pyi 2024-05-23 12:40:10.170754997 +1000 ++++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.pyi 2024-05-23 16:47:15.874656809 +1000 +@@ -1,3 +1,20 @@ ++# Type hints copied from the typeshed project under the Apache License 2.0 ++# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE ++ ++# For updates: https://github.com/python/typeshed/blob/main/stdlib/contextlib.pyi ++ ++# Last updated: 2024-05-22 ++# Updated from: https://github.com/python/typeshed/blob/aa2d33df211e1e4f70883388febf750ac524d2bb/stdlib/contextlib.pyi ++# Saved to: dev/typeshed_contextlib.pyi ++ ++# contextlib2 API adaptation notes: ++# * the various 'if True:' guards replace sys.version checks in the original ++# typeshed file (those APIs are available on all supported versions) ++# * any commented out 'if True:' guards replace sys.version checks in the original ++# typeshed file where the affected APIs haven't been backported yet ++# * deliberately omitted APIs are listed in `dev/mypy.allowlist` ++# (e.g. deprecated experimental APIs that never graduated to the stdlib) ++ + import abc + import sys + from _typeshed import FileDescriptorOrPath, Unused +@@ -22,10 +39,10 @@ + "nullcontext", + ] + +-if sys.version_info >= (3, 10): ++if True: + __all__ += ["aclosing"] + +-if sys.version_info >= (3, 11): ++if True: + __all__ += ["chdir"] + + _T = TypeVar("_T") +@@ -65,18 +82,14 @@ + func: Callable[..., Generator[_T_co, Any, Any]] + args: tuple[Any, ...] + kwds: dict[str, Any] +- if sys.version_info >= (3, 9): ++ if True: + def __exit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... +- else: +- def __exit__( +- self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None +- ) -> bool | None: ... + + def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... + +-if sys.version_info >= (3, 10): ++if True: + _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) + + class AsyncContextDecorator: +@@ -94,17 +107,6 @@ + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + +-else: +- class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]): +- def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... +- gen: AsyncGenerator[_T_co, Any] +- func: Callable[..., AsyncGenerator[_T_co, Any]] +- args: tuple[Any, ...] +- kwds: dict[str, Any] +- async def __aexit__( +- self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None +- ) -> bool | None: ... +- + def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... + + class _SupportsClose(Protocol): +@@ -116,7 +118,7 @@ + def __init__(self, thing: _SupportsCloseT) -> None: ... + def __exit__(self, *exc_info: Unused) -> None: ... + +-if sys.version_info >= (3, 10): ++if True: + class _SupportsAclose(Protocol): + def aclose(self) -> Awaitable[object]: ... + +@@ -177,7 +179,7 @@ + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> bool: ... + +-if sys.version_info >= (3, 10): ++if True: + class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): + enter_result: _T + @overload +@@ -189,17 +191,7 @@ + async def __aenter__(self) -> _T: ... + async def __aexit__(self, *exctype: Unused) -> None: ... + +-else: +- class nullcontext(AbstractContextManager[_T, None]): +- enter_result: _T +- @overload +- def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... +- @overload +- def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 +- def __enter__(self) -> _T: ... +- def __exit__(self, *exctype: Unused) -> None: ... +- +-if sys.version_info >= (3, 11): ++if True: + _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) + + class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): diff --git a/dev/py3_12_rst_to_contextlib2.patch b/dev/py3_12_rst_to_contextlib2.patch new file mode 100644 index 0000000..5b5c017 --- /dev/null +++ b/dev/py3_12_rst_to_contextlib2.patch @@ -0,0 +1,453 @@ +--- /home/ncoghlan/devel/contextlib2/../cpython/Doc/library/contextlib.rst 2024-05-20 12:53:59.936907756 +1000 ++++ /home/ncoghlan/devel/contextlib2/docs/contextlib2.rst 2024-05-23 17:39:52.671083724 +1000 +@@ -1,20 +1,5 @@ +-:mod:`!contextlib` --- Utilities for :keyword:`!with`\ -statement contexts +-========================================================================== +- +-.. module:: contextlib +- :synopsis: Utilities for with-statement contexts. +- +-**Source code:** :source:`Lib/contextlib.py` +- +--------------- +- +-This module provides utilities for common tasks involving the :keyword:`with` +-statement. For more information see also :ref:`typecontextmanager` and +-:ref:`context-managers`. +- +- +-Utilities +---------- ++API Reference ++------------- + + Functions and classes provided: + +@@ -26,8 +11,8 @@ + ``self`` while :meth:`object.__exit__` is an abstract method which by default + returns ``None``. See also the definition of :ref:`typecontextmanager`. + +- .. versionadded:: 3.6 +- ++ .. versionadded:: 0.6.0 ++ Part of the standard library in Python 3.6 and later + + .. class:: AbstractAsyncContextManager + +@@ -38,8 +23,8 @@ + returns ``None``. See also the definition of + :ref:`async-context-managers`. + +- .. versionadded:: 3.7 +- ++ .. versionadded:: 21.6.0 ++ Part of the standard library in Python 3.7 and later + + .. decorator:: contextmanager + +@@ -49,12 +34,12 @@ + + While many objects natively support use in with statements, sometimes a + resource needs to be managed that isn't a context manager in its own right, +- and doesn't implement a ``close()`` method for use with ``contextlib.closing`` ++ and doesn't implement a ``close()`` method for use with ``contextlib2.closing`` + + An abstract example would be the following to ensure correct resource + management:: + +- from contextlib import contextmanager ++ from contextlib2 import contextmanager + + @contextmanager + def managed_resource(*args, **kwds): +@@ -95,13 +80,10 @@ + created by :func:`contextmanager` to meet the requirement that context + managers support multiple invocations in order to be used as decorators). + +- .. versionchanged:: 3.2 +- Use of :class:`ContextDecorator`. +- + + .. decorator:: asynccontextmanager + +- Similar to :func:`~contextlib.contextmanager`, but creates an ++ Similar to :func:`~contextlib2.contextmanager`, but creates an + :ref:`asynchronous context manager `. + + This function is a :term:`decorator` that can be used to define a factory +@@ -112,7 +94,7 @@ + + A simple example:: + +- from contextlib import asynccontextmanager ++ from contextlib2 import asynccontextmanager + + @asynccontextmanager + async def get_connection(): +@@ -126,13 +108,16 @@ + async with get_connection() as conn: + return conn.query('SELECT ...') + +- .. versionadded:: 3.7 ++ .. versionadded:: 21.6.0 ++ Part of the standard library in Python 3.7 and later, enhanced in ++ Python 3.10 and later to allow created async context managers to be used ++ as async function decorators. + + Context managers defined with :func:`asynccontextmanager` can be used + either as decorators or with :keyword:`async with` statements:: + + import time +- from contextlib import asynccontextmanager ++ from contextlib2 import asynccontextmanager + + @asynccontextmanager + async def timeit(): +@@ -151,17 +136,13 @@ + created by :func:`asynccontextmanager` to meet the requirement that context + managers support multiple invocations in order to be used as decorators. + +- .. versionchanged:: 3.10 +- Async context managers created with :func:`asynccontextmanager` can +- be used as decorators. +- + + .. function:: closing(thing) + + Return a context manager that closes *thing* upon completion of the block. This + is basically equivalent to:: + +- from contextlib import contextmanager ++ from contextlib2 import contextmanager + + @contextmanager + def closing(thing): +@@ -172,7 +153,7 @@ + + And lets you write code like this:: + +- from contextlib import closing ++ from contextlib2 import closing + from urllib.request import urlopen + + with closing(urlopen('https://www.python.org')) as page: +@@ -196,7 +177,7 @@ + Return an async context manager that calls the ``aclose()`` method of *thing* + upon completion of the block. This is basically equivalent to:: + +- from contextlib import asynccontextmanager ++ from contextlib2 import asynccontextmanager + + @asynccontextmanager + async def aclosing(thing): +@@ -209,7 +190,7 @@ + generators when they happen to exit early by :keyword:`break` or an + exception. For example:: + +- from contextlib import aclosing ++ from contextlib2 import aclosing + + async with aclosing(my_generator()) as values: + async for value in values: +@@ -221,7 +202,8 @@ + variables work as expected, and the exit code isn't run after the + lifetime of some task it depends on). + +- .. versionadded:: 3.10 ++ .. versionadded:: 21.6.0 ++ Part of the standard library in Python 3.10 and later + + + .. _simplifying-support-for-single-optional-context-managers: +@@ -235,10 +217,10 @@ + def myfunction(arg, ignore_exceptions=False): + if ignore_exceptions: + # Use suppress to ignore all exceptions. +- cm = contextlib.suppress(Exception) ++ cm = contextlib2.suppress(Exception) + else: + # Do not ignore any exceptions, cm has no effect. +- cm = contextlib.nullcontext() ++ cm = contextlib2.nullcontext() + with cm: + # Do something + +@@ -269,11 +251,11 @@ + async with cm as session: + # Send http requests with session + +- .. versionadded:: 3.7 +- +- .. versionchanged:: 3.10 +- :term:`asynchronous context manager` support was added. ++ .. versionadded:: 0.6.0 ++ Part of the standard library in Python 3.7 and later + ++ .. versionchanged:: 21.6.0 ++ Updated to Python 3.10 version with :term:`asynchronous context manager` support + + + .. function:: suppress(*exceptions) +@@ -290,7 +272,7 @@ + + For example:: + +- from contextlib import suppress ++ from contextlib2 import suppress + + with suppress(FileNotFoundError): + os.remove('somefile.tmp') +@@ -314,13 +296,15 @@ + + If the code within the :keyword:`!with` block raises a + :exc:`BaseExceptionGroup`, suppressed exceptions are removed from the +- group. If any exceptions in the group are not suppressed, a group containing them is re-raised. ++ group. If any exceptions in the group are not suppressed, a group containing ++ them is re-raised. + +- .. versionadded:: 3.4 ++ .. versionadded:: 0.5 ++ Part of the standard library in Python 3.4 and later + +- .. versionchanged:: 3.12 +- ``suppress`` now supports suppressing exceptions raised as +- part of an :exc:`BaseExceptionGroup`. ++ .. versionchanged:: 24.6.0 ++ Updated to Python 3.12 version that supports suppressing exceptions raised ++ as part of a :exc:`BaseExceptionGroup`. + + .. function:: redirect_stdout(new_target) + +@@ -359,17 +343,19 @@ + + This context manager is :ref:`reentrant `. + +- .. versionadded:: 3.4 ++ .. versionadded:: 0.5 ++ Part of the standard library in Python 3.4 and later + + + .. function:: redirect_stderr(new_target) + +- Similar to :func:`~contextlib.redirect_stdout` but redirecting ++ Similar to :func:`~contextlib2.redirect_stdout` but redirecting + :data:`sys.stderr` to another file or file-like object. + + This context manager is :ref:`reentrant `. + +- .. versionadded:: 3.5 ++ .. versionadded:: 0.5 ++ Part of the standard library in Python 3.5 and later + + + .. function:: chdir(path) +@@ -386,7 +372,8 @@ + + This context manager is :ref:`reentrant `. + +- .. versionadded:: 3.11 ++ .. versionadded:: 24.6.0 ++ Part of the standard library in Python 3.11 and later + + + .. class:: ContextDecorator() +@@ -402,7 +389,7 @@ + + Example of ``ContextDecorator``:: + +- from contextlib import ContextDecorator ++ from contextlib2 import ContextDecorator + + class mycontext(ContextDecorator): + def __enter__(self): +@@ -449,7 +436,7 @@ + Existing context managers that already have a base class can be extended by + using ``ContextDecorator`` as a mixin class:: + +- from contextlib import ContextDecorator ++ from contextlib2 import ContextDecorator + + class mycontext(ContextBaseClass, ContextDecorator): + def __enter__(self): +@@ -464,8 +451,6 @@ + statements. If this is not the case, then the original construct with the + explicit :keyword:`!with` statement inside the function should be used. + +- .. versionadded:: 3.2 +- + + .. class:: AsyncContextDecorator + +@@ -474,7 +459,7 @@ + Example of ``AsyncContextDecorator``:: + + from asyncio import run +- from contextlib import AsyncContextDecorator ++ from contextlib2 import AsyncContextDecorator + + class mycontext(AsyncContextDecorator): + async def __aenter__(self): +@@ -505,7 +490,8 @@ + The bit in the middle + Finishing + +- .. versionadded:: 3.10 ++ .. versionadded:: 21.6.0 ++ Part of the standard library in Python 3.10 and later + + + .. class:: ExitStack() +@@ -547,7 +533,8 @@ + foundation for higher level context managers that manipulate the exit + stack in application specific ways. + +- .. versionadded:: 3.3 ++ .. versionadded:: 0.4 ++ Part of the standard library in Python 3.3 and later + + .. method:: enter_context(cm) + +@@ -558,9 +545,10 @@ + These context managers may suppress exceptions just as they normally + would if used directly as part of a :keyword:`with` statement. + +- .. versionchanged:: 3.11 +- Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* +- is not a context manager. ++ .. versionchanged:: 24.6.0 ++ When running on Python 3.11 or later, raises :exc:`TypeError` instead ++ of :exc:`AttributeError` if *cm* is not a context manager. This aligns ++ with the behaviour of :keyword:`with` statements in Python 3.11+. + + .. method:: push(exit) + +@@ -627,14 +615,16 @@ + The :meth:`~ExitStack.close` method is not implemented; :meth:`aclose` must be used + instead. + +- .. coroutinemethod:: enter_async_context(cm) ++ .. method:: enter_async_context(cm) ++ :async: + + Similar to :meth:`ExitStack.enter_context` but expects an asynchronous context + manager. + +- .. versionchanged:: 3.11 +- Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* +- is not an asynchronous context manager. ++ .. versionchanged:: 24.6.0 ++ When running on Python 3.11 or later, raises :exc:`TypeError` instead ++ of :exc:`AttributeError` if *cm* is not an asynchronous context manager. ++ This aligns with the behaviour of ``async with`` statements in Python 3.11+. + + .. method:: push_async_exit(exit) + +@@ -645,7 +635,8 @@ + + Similar to :meth:`ExitStack.callback` but expects a coroutine function. + +- .. coroutinemethod:: aclose() ++ .. method:: aclose() ++ :async: + + Similar to :meth:`ExitStack.close` but properly handles awaitables. + +@@ -658,13 +649,15 @@ + # the async with statement, even if attempts to open a connection + # later in the list raise an exception. + +- .. versionadded:: 3.7 ++ .. versionadded:: 21.6.0 ++ Part of the standard library in Python 3.7 and later ++ + + Examples and Recipes + -------------------- + + This section describes some examples and recipes for making effective use of +-the tools provided by :mod:`contextlib`. ++the tools provided by :mod:`contextlib2`. + + + Supporting a variable number of context managers +@@ -728,7 +721,7 @@ + acquisition and release functions, along with an optional validation function, + and maps them to the context management protocol:: + +- from contextlib import contextmanager, AbstractContextManager, ExitStack ++ from contextlib2 import contextmanager, AbstractContextManager, ExitStack + + class ResourceManager(AbstractContextManager): + +@@ -788,7 +781,7 @@ + execution at the end of a ``with`` statement, and then later decide to skip + executing that callback:: + +- from contextlib import ExitStack ++ from contextlib2 import ExitStack + + with ExitStack() as stack: + stack.callback(cleanup_resources) +@@ -802,7 +795,7 @@ + If a particular application uses this pattern a lot, it can be simplified + even further by means of a small helper class:: + +- from contextlib import ExitStack ++ from contextlib2 import ExitStack + + class Callback(ExitStack): + def __init__(self, callback, /, *args, **kwds): +@@ -822,7 +815,7 @@ + :meth:`ExitStack.callback` to declare the resource cleanup in + advance:: + +- from contextlib import ExitStack ++ from contextlib2 import ExitStack + + with ExitStack() as stack: + @stack.callback +@@ -849,7 +842,7 @@ + inheriting from :class:`ContextDecorator` provides both capabilities in a + single definition:: + +- from contextlib import ContextDecorator ++ from contextlib2 import ContextDecorator + import logging + + logging.basicConfig(level=logging.INFO) +@@ -911,7 +904,7 @@ + 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 ++ >>> from contextlib2 import contextmanager + >>> @contextmanager + ... def singleuse(): + ... print("Before") +@@ -946,7 +939,7 @@ + :func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very + simple example of reentrant use:: + +- >>> from contextlib import redirect_stdout ++ >>> from contextlib2 import redirect_stdout + >>> from io import StringIO + >>> stream = StringIO() + >>> write_to_stream = redirect_stdout(stream) +@@ -992,7 +985,7 @@ + when leaving any with statement, regardless of where those callbacks + were added:: + +- >>> from contextlib import ExitStack ++ >>> from contextlib2 import ExitStack + >>> stack = ExitStack() + >>> with stack: + ... stack.callback(print, "Callback: from first context") +@@ -1026,7 +1019,7 @@ + Using separate :class:`ExitStack` instances instead of reusing a single + instance avoids that problem:: + +- >>> from contextlib import ExitStack ++ >>> from contextlib2 import ExitStack + >>> with ExitStack() as outer_stack: + ... outer_stack.callback(print, "Callback: from outer context") + ... with ExitStack() as inner_stack: diff --git a/dev/py3_12_test_async_to_contextlib2.patch b/dev/py3_12_test_async_to_contextlib2.patch new file mode 100644 index 0000000..a2cbaa6 --- /dev/null +++ b/dev/py3_12_test_async_to_contextlib2.patch @@ -0,0 +1,67 @@ +--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib_async.py 2024-05-23 11:57:09.276022441 +1000 ++++ /home/ncoghlan/devel/contextlib2/test/test_contextlib_async.py 2024-05-23 17:39:05.799797895 +1000 +@@ -1,5 +1,7 @@ ++"""Unit tests for asynchronous features of contextlib2.py""" ++ + import asyncio +-from contextlib import ( ++from contextlib2 import ( + asynccontextmanager, AbstractAsyncContextManager, + AsyncExitStack, nullcontext, aclosing, contextmanager) + import functools +@@ -7,7 +9,7 @@ + import unittest + import traceback + +-from test.test_contextlib import TestBaseExitStack ++from .test_contextlib import TestBaseExitStack + + support.requires_working_socket(module=True) + +@@ -202,7 +204,8 @@ + await ctx.__aexit__(TypeError, TypeError('foo'), None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. +- self.assertFalse(ctx.gen.ag_suspended) ++ if support.cl2_async_gens_have_ag_suspended: ++ self.assertFalse(ctx.gen.ag_suspended) + + @_async_test + async def test_contextmanager_trap_no_yield(self): +@@ -226,7 +229,8 @@ + await ctx.__aexit__(None, None, None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. +- self.assertFalse(ctx.gen.ag_suspended) ++ if support.cl2_async_gens_have_ag_suspended: ++ self.assertFalse(ctx.gen.ag_suspended) + + @_async_test + async def test_contextmanager_non_normalised(self): +@@ -669,12 +673,13 @@ + async def __aenter__(self): + pass + ++ expected_error, expected_text = support.cl2_cm_api_exc_info_async() + async with self.exit_stack() as stack: +- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): ++ with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(LacksEnterAndExit()) +- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): ++ with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(LacksEnter()) +- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): ++ with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(LacksExit()) + self.assertFalse(stack._exit_callbacks) + +@@ -752,7 +757,8 @@ + cm.__aenter__ = object() + cm.__aexit__ = object() + stack = self.exit_stack() +- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): ++ expected_error, expected_text = support.cl2_cm_api_exc_info_async() ++ with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(cm) + stack.push_async_exit(cm) + self.assertIs(stack._exit_callbacks[-1][1], cm) diff --git a/dev/py3_12_test_to_contextlib2.patch b/dev/py3_12_test_to_contextlib2.patch new file mode 100644 index 0000000..275a1b4 --- /dev/null +++ b/dev/py3_12_test_to_contextlib2.patch @@ -0,0 +1,106 @@ +--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib.py 2024-05-23 11:57:09.276022441 +1000 ++++ /home/ncoghlan/devel/contextlib2/test/test_contextlib.py 2024-05-23 17:38:37.295232213 +1000 +@@ -1,4 +1,4 @@ +-"""Unit tests for contextlib.py, and other context managers.""" ++"""Unit tests for synchronous features of contextlib2.py""" + + import io + import os +@@ -7,7 +7,7 @@ + import threading + import traceback + import unittest +-from contextlib import * # Tests __all__ ++from contextlib2 import * # Tests __all__ + from test import support + from test.support import os_helper + from test.support.testcase import ExceptionIsLikeMixin +@@ -161,7 +161,8 @@ + ctx.__exit__(TypeError, TypeError("foo"), None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. +- self.assertFalse(ctx.gen.gi_suspended) ++ if support.cl2_gens_have_gi_suspended: ++ self.assertFalse(ctx.gen.gi_suspended) + + def test_contextmanager_trap_no_yield(self): + @contextmanager +@@ -183,7 +184,8 @@ + ctx.__exit__(None, None, None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. +- self.assertFalse(ctx.gen.gi_suspended) ++ if support.cl2_gens_have_gi_suspended: ++ self.assertFalse(ctx.gen.gi_suspended) + + def test_contextmanager_non_normalised(self): + @contextmanager +@@ -610,7 +612,8 @@ + def __exit__(self, *exc): + pass + +- with self.assertRaisesRegex(TypeError, 'the context manager'): ++ expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__enter__") ++ with self.assertRaisesRegex(expected_error, expected_text): + with mycontext(): + pass + +@@ -622,7 +625,8 @@ + def __uxit__(self, *exc): + pass + +- with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'): ++ expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__exit__") ++ with self.assertRaisesRegex(expected_error, expected_text): + with mycontext(): + pass + +@@ -790,12 +794,13 @@ + def __enter__(self): + pass + ++ expected_error, expected_text = support.cl2_cm_api_exc_info_sync() + with self.exit_stack() as stack: +- with self.assertRaisesRegex(TypeError, 'the context manager'): ++ with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(LacksEnterAndExit()) +- with self.assertRaisesRegex(TypeError, 'the context manager'): ++ with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(LacksEnter()) +- with self.assertRaisesRegex(TypeError, 'the context manager'): ++ with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(LacksExit()) + self.assertFalse(stack._exit_callbacks) + +@@ -858,8 +863,11 @@ + [('_exit_wrapper', 'callback(*args, **kwds)'), + ('raise_exc', 'raise exc')] + +- self.assertEqual( +- [(f.name, f.line) for f in ve_frames], expected) ++ # This check fails on PyPy 3.10 ++ # It also fails on CPython 3.9 and earlier versions ++ if support.check_impl_detail(cpython=True) and support.cl2_check_traceback_details: ++ self.assertEqual( ++ [(f.name, f.line) for f in ve_frames], expected) + + self.assertIsInstance(exc.__context__, ZeroDivisionError) + zde_frames = traceback.extract_tb(exc.__context__.__traceback__) +@@ -1093,7 +1101,8 @@ + cm.__enter__ = object() + cm.__exit__ = object() + stack = self.exit_stack() +- with self.assertRaisesRegex(TypeError, 'the context manager'): ++ expected_error, expected_text = support.cl2_cm_api_exc_info_sync() ++ with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(cm) + stack.push(cm) + self.assertIs(stack._exit_callbacks[-1][1], cm) +@@ -1264,6 +1273,7 @@ + 1/0 + self.assertTrue(outer_continued) + ++ @support.cl2_requires_exception_groups + def test_exception_groups(self): + eg_ve = lambda: ExceptionGroup( + "EG with ValueErrors only", diff --git a/dev/save_diff_snapshot.sh b/dev/save_diff_snapshot.sh new file mode 100755 index 0000000..67e734b --- /dev/null +++ b/dev/save_diff_snapshot.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +git_root="$(git rev-parse --show-toplevel)" + +cpython_dir="${1:-$git_root/../cpython}" + +diff_prefix="py3_12" # Update based on the version being synced + +function diff_file() +{ + diff -ud "$2" "$git_root/$3" > "$git_root/dev/${diff_prefix}_$1.patch" +} + +diff_file rst_to_contextlib2 \ + "$cpython_dir/Doc/library/contextlib.rst" "docs/contextlib2.rst" + +diff_file py_to_contextlib2 \ + "$cpython_dir/Lib/contextlib.py" "contextlib2/__init__.py" + +diff_file pyi_to_contextlib2 \ + "$git_root/dev/typeshed_contextlib.pyi" "contextlib2/__init__.pyi" + +diff_file test_to_contextlib2 \ + "$cpython_dir/Lib/test/test_contextlib.py" "test/test_contextlib.py" + +diff_file test_async_to_contextlib2 \ + "$cpython_dir/Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py" diff --git a/dev/sync_from_cpython.sh b/dev/sync_from_cpython.sh new file mode 100755 index 0000000..a6dade9 --- /dev/null +++ b/dev/sync_from_cpython.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +git_root="$(git rev-parse --show-toplevel)" + +cpython_dir="${1:-$git_root/../cpython}" # Folder with relevant CPython version + +function sync_file() +{ + cp -fv "$cpython_dir/$1" "$git_root/$2" +} + +sync_file "Doc/library/contextlib.rst" "docs/contextlib2.rst" +sync_file "Lib/contextlib.py" "contextlib2/__init__.py" +sync_file "Lib/test/test_contextlib.py" "test/test_contextlib.py" +sync_file "Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py" + +echo +echo "Note: Update the 'contextlib2/__init__.pyi' stub as described in the file" +echo diff --git a/dev/typeshed_contextlib.pyi b/dev/typeshed_contextlib.pyi new file mode 100644 index 0000000..29ac7cd --- /dev/null +++ b/dev/typeshed_contextlib.pyi @@ -0,0 +1,209 @@ +import abc +import sys +from _typeshed import FileDescriptorOrPath, Unused +from abc import abstractmethod +from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator +from types import TracebackType +from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable +from typing_extensions import ParamSpec, Self, TypeAlias + +__all__ = [ + "contextmanager", + "closing", + "AbstractContextManager", + "ContextDecorator", + "ExitStack", + "redirect_stdout", + "redirect_stderr", + "suppress", + "AbstractAsyncContextManager", + "AsyncExitStack", + "asynccontextmanager", + "nullcontext", +] + +if sys.version_info >= (3, 10): + __all__ += ["aclosing"] + +if sys.version_info >= (3, 11): + __all__ += ["chdir"] + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) +_T_io = TypeVar("_T_io", bound=IO[str] | None) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None) +_F = TypeVar("_F", bound=Callable[..., Any]) +_P = ParamSpec("_P") + +_ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None] +_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc) + +@runtime_checkable +class AbstractContextManager(Protocol[_T_co, _ExitT_co]): + def __enter__(self) -> _T_co: ... + @abstractmethod + def __exit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +@runtime_checkable +class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): + async def __aenter__(self) -> _T_co: ... + @abstractmethod + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +class ContextDecorator: + def __call__(self, func: _F) -> _F: ... + +class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator): + # __init__ and all instance attributes are actually inherited from _GeneratorContextManagerBase + # _GeneratorContextManagerBase is more trouble than it's worth to include in the stub; see #6676 + def __init__(self, func: Callable[..., Iterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... + gen: Generator[_T_co, Any, Any] + func: Callable[..., Generator[_T_co, Any, Any]] + args: tuple[Any, ...] + kwds: dict[str, Any] + if sys.version_info >= (3, 9): + def __exit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + else: + def __exit__( + self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + +def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... + +if sys.version_info >= (3, 10): + _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) + + class AsyncContextDecorator: + def __call__(self, func: _AF) -> _AF: ... + + class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator): + # __init__ and these attributes are actually defined in the base class _GeneratorContextManagerBase, + # which is more trouble than it's worth to include in the stub (see #6676) + def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... + gen: AsyncGenerator[_T_co, Any] + func: Callable[..., AsyncGenerator[_T_co, Any]] + args: tuple[Any, ...] + kwds: dict[str, Any] + async def __aexit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + +else: + class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]): + def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... + gen: AsyncGenerator[_T_co, Any] + func: Callable[..., AsyncGenerator[_T_co, Any]] + args: tuple[Any, ...] + kwds: dict[str, Any] + async def __aexit__( + self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool | None: ... + +def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... + +class _SupportsClose(Protocol): + def close(self) -> object: ... + +_SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) + +class closing(AbstractContextManager[_SupportsCloseT, None]): + def __init__(self, thing: _SupportsCloseT) -> None: ... + def __exit__(self, *exc_info: Unused) -> None: ... + +if sys.version_info >= (3, 10): + class _SupportsAclose(Protocol): + def aclose(self) -> Awaitable[object]: ... + + _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) + + class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): + def __init__(self, thing: _SupportsAcloseT) -> None: ... + async def __aexit__(self, *exc_info: Unused) -> None: ... + +class suppress(AbstractContextManager[None, bool]): + def __init__(self, *exceptions: type[BaseException]) -> None: ... + def __exit__( + self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None + ) -> bool: ... + +class _RedirectStream(AbstractContextManager[_T_io, None]): + def __init__(self, new_target: _T_io) -> None: ... + def __exit__( + self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None + ) -> None: ... + +class redirect_stdout(_RedirectStream[_T_io]): ... +class redirect_stderr(_RedirectStream[_T_io]): ... + +# In reality this is a subclass of `AbstractContextManager`; +# see #7961 for why we don't do that in the stub +class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): + def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... + def push(self, exit: _CM_EF) -> _CM_EF: ... + def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... + def pop_all(self) -> Self: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> _ExitT_co: ... + +_ExitCoroFunc: TypeAlias = Callable[ + [type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None] +] +_ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc) + +# In reality this is a subclass of `AbstractAsyncContextManager`; +# see #7961 for why we don't do that in the stub +class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): + def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... + async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... + def push(self, exit: _CM_EF) -> _CM_EF: ... + def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... + def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... + def push_async_callback( + self, callback: Callable[_P, Awaitable[_T]], /, *args: _P.args, **kwds: _P.kwargs + ) -> Callable[_P, Awaitable[_T]]: ... + def pop_all(self) -> Self: ... + async def aclose(self) -> None: ... + async def __aenter__(self) -> Self: ... + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / + ) -> bool: ... + +if sys.version_info >= (3, 10): + class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): + enter_result: _T + @overload + def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... + @overload + def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + def __enter__(self) -> _T: ... + def __exit__(self, *exctype: Unused) -> None: ... + async def __aenter__(self) -> _T: ... + async def __aexit__(self, *exctype: Unused) -> None: ... + +else: + class nullcontext(AbstractContextManager[_T, None]): + enter_result: _T + @overload + def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... + @overload + def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 + def __enter__(self) -> _T: ... + def __exit__(self, *exctype: Unused) -> None: ... + +if sys.version_info >= (3, 11): + _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) + + class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): + path: _T_fd_or_any_path + def __init__(self, path: _T_fd_or_any_path) -> None: ... + def __enter__(self) -> None: ... + def __exit__(self, *excinfo: Unused) -> None: ... diff --git a/docs/conf.py b/docs/conf.py index 600d17c..fdc7c26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,8 +11,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -25,7 +23,10 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.intersphinx'] +extensions = [ + 'sphinx.ext.intersphinx', + 'sphinx_rtd_theme', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -92,7 +93,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -121,7 +122,7 @@ html_theme = 'default' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -218,4 +219,4 @@ man_pages = [ # Configuration for intersphinx: refer to the Python 3 standard library. -intersphinx_mapping = {'http://docs.python.org/3': None} +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} diff --git a/docs/contextlib2.rst b/docs/contextlib2.rst index 3250678..2e6def0 100644 --- a/docs/contextlib2.rst +++ b/docs/contextlib2.rst @@ -30,16 +30,16 @@ Functions and classes provided: This function is a :term:`decorator` that can be used to define a factory function for :keyword:`with` statement context managers, without needing to - create a class or separate :meth:`__enter__` and :meth:`__exit__` methods. + create a class or separate :meth:`~object.__enter__` and :meth:`~object.__exit__` methods. While many objects natively support use in with statements, sometimes a resource needs to be managed that isn't a context manager in its own right, - and doesn't implement a ``close()`` method for use with ``contextlib.closing`` + and doesn't implement a ``close()`` method for use with ``contextlib2.closing`` An abstract example would be the following to ensure correct resource management:: - from contextlib import contextmanager + from contextlib2 import contextmanager @contextmanager def managed_resource(*args, **kwds): @@ -51,6 +51,8 @@ Functions and classes provided: # Code to release resource, e.g.: release_resource(resource) + The function can then be used like this:: + >>> with managed_resource(timeout=3600) as resource: ... # Resource is released at the end of this block, ... # even if code in the block raises an exception @@ -81,18 +83,18 @@ Functions and classes provided: .. decorator:: asynccontextmanager - Similar to :func:`~contextlib.contextmanager`, but creates an + Similar to :func:`~contextlib2.contextmanager`, but creates an :ref:`asynchronous context manager `. This function is a :term:`decorator` that can be used to define a factory function for :keyword:`async with` statement asynchronous context managers, - without needing to create a class or separate :meth:`__aenter__` and - :meth:`__aexit__` methods. It must be applied to an :term:`asynchronous + without needing to create a class or separate :meth:`~object.__aenter__` and + :meth:`~object.__aexit__` methods. It must be applied to an :term:`asynchronous generator` function. A simple example:: - from contextlib import asynccontextmanager + from contextlib2 import asynccontextmanager @asynccontextmanager async def get_connection(): @@ -115,7 +117,9 @@ Functions and classes provided: either as decorators or with :keyword:`async with` statements:: import time + from contextlib2 import asynccontextmanager + @asynccontextmanager async def timeit(): now = time.monotonic() try: @@ -123,9 +127,9 @@ Functions and classes provided: finally: print(f'it took {time.monotonic() - now}s to run') - @timeit() - async def main(): - # ... async code ... + @timeit() + async def main(): + # ... async code ... When used as a decorator, a new generator instance is implicitly created on each function call. This allows the otherwise "one-shot" context managers @@ -138,7 +142,7 @@ Functions and classes provided: Return a context manager that closes *thing* upon completion of the block. This is basically equivalent to:: - from contextlib import contextmanager + from contextlib2 import contextmanager @contextmanager def closing(thing): @@ -149,23 +153,31 @@ Functions and classes provided: And lets you write code like this:: - from contextlib import closing + from contextlib2 import closing from urllib.request import urlopen - with closing(urlopen('http://www.python.org')) as page: + with closing(urlopen('https://www.python.org')) as page: for line in page: print(line) without needing to explicitly close ``page``. Even if an error occurs, ``page.close()`` will be called when the :keyword:`with` block is exited. + .. note:: -.. class:: aclosing(thing) + Most types managing resources support the :term:`context manager` protocol, + which closes *thing* on leaving the :keyword:`with` statement. + As such, :func:`!closing` is most useful for third party types that don't + support context managers. + This example is purely for illustration purposes, + as :func:`~urllib.request.urlopen` would normally be used in a context manager. + +.. function:: aclosing(thing) Return an async context manager that calls the ``aclose()`` method of *thing* upon completion of the block. This is basically equivalent to:: - from contextlib import asynccontextmanager + from contextlib2 import asynccontextmanager @asynccontextmanager async def aclosing(thing): @@ -178,7 +190,7 @@ Functions and classes provided: generators when they happen to exit early by :keyword:`break` or an exception. For example:: - from contextlib import aclosing + from contextlib2 import aclosing async with aclosing(my_generator()) as values: async for value in values: @@ -205,10 +217,10 @@ Functions and classes provided: def myfunction(arg, ignore_exceptions=False): if ignore_exceptions: # Use suppress to ignore all exceptions. - cm = contextlib.suppress(Exception) + cm = contextlib2.suppress(Exception) else: # Do not ignore any exceptions, cm has no effect. - cm = contextlib.nullcontext() + cm = contextlib2.nullcontext() with cm: # Do something @@ -229,15 +241,15 @@ Functions and classes provided: :ref:`asynchronous context managers `:: async def send_http(session=None): - if not session: - # If no http session, create it with aiohttp - cm = aiohttp.ClientSession() - else: - # Caller is responsible for closing the session - cm = nullcontext(session) + if not session: + # If no http session, create it with aiohttp + cm = aiohttp.ClientSession() + else: + # Caller is responsible for closing the session + cm = nullcontext(session) - async with cm as session: - # Send http requests with session + async with cm as session: + # Send http requests with session .. versionadded:: 0.6.0 Part of the standard library in Python 3.7 and later @@ -260,7 +272,7 @@ Functions and classes provided: For example:: - from contextlib import suppress + from contextlib2 import suppress with suppress(FileNotFoundError): os.remove('somefile.tmp') @@ -282,9 +294,17 @@ Functions and classes provided: This context manager is :ref:`reentrant `. + If the code within the :keyword:`!with` block raises a + :exc:`BaseExceptionGroup`, suppressed exceptions are removed from the + group. If any exceptions in the group are not suppressed, a group containing + them is re-raised. + .. versionadded:: 0.5 Part of the standard library in Python 3.4 and later + .. versionchanged:: 24.6.0 + Updated to Python 3.12 version that supports suppressing exceptions raised + as part of a :exc:`BaseExceptionGroup`. .. function:: redirect_stdout(new_target) @@ -329,7 +349,7 @@ Functions and classes provided: .. function:: redirect_stderr(new_target) - Similar to :func:`~contextlib.redirect_stdout` but redirecting + Similar to :func:`~contextlib2.redirect_stdout` but redirecting :data:`sys.stderr` to another file or file-like object. This context manager is :ref:`reentrant `. @@ -338,6 +358,24 @@ Functions and classes provided: Part of the standard library in Python 3.5 and later +.. function:: chdir(path) + + Non parallel-safe context manager to change the current working directory. + As this changes a global state, the working directory, it is not suitable + for use in most threaded or async contexts. It is also not suitable for most + non-linear code execution, like generators, where the program execution is + temporarily relinquished -- unless explicitly desired, you should not yield + when this context manager is active. + + This is a simple wrapper around :func:`~os.chdir`, it changes the current + working directory upon entering and restores the old one on exit. + + This context manager is :ref:`reentrant `. + + .. versionadded:: 24.6.0 + Part of the standard library in Python 3.11 and later + + .. class:: ContextDecorator() A base class that enables a context manager to also be used as a decorator. @@ -351,7 +389,7 @@ Functions and classes provided: Example of ``ContextDecorator``:: - from contextlib import ContextDecorator + from contextlib2 import ContextDecorator class mycontext(ContextDecorator): def __enter__(self): @@ -362,6 +400,8 @@ Functions and classes provided: print('Finishing') return False + The class can then be used like this:: + >>> @mycontext() ... def function(): ... print('The bit in the middle') @@ -396,7 +436,7 @@ Functions and classes provided: Existing context managers that already have a base class can be extended by using ``ContextDecorator`` as a mixin class:: - from contextlib import ContextDecorator + from contextlib2 import ContextDecorator class mycontext(ContextBaseClass, ContextDecorator): def __enter__(self): @@ -419,7 +459,7 @@ Functions and classes provided: Example of ``AsyncContextDecorator``:: from asyncio import run - from contextlib import AsyncContextDecorator + from contextlib2 import AsyncContextDecorator class mycontext(AsyncContextDecorator): async def __aenter__(self): @@ -430,6 +470,8 @@ Functions and classes provided: print('Finishing') return False + The class can then be used like this:: + >>> @mycontext() ... async def function(): ... print('The bit in the middle') @@ -467,6 +509,9 @@ Functions and classes provided: # the with statement, even if attempts to open files later # in the list raise an exception + The :meth:`~object.__enter__` method returns the :class:`ExitStack` instance, and + performs no additional operations. + 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 :keyword:`with` statement). Note that callbacks are *not* @@ -493,27 +538,32 @@ Functions and classes provided: .. method:: enter_context(cm) - Enters a new context manager and adds its :meth:`__exit__` method to + Enters a new context manager and adds its :meth:`~object.__exit__` method to the callback stack. The return value is the result of the context - manager's own :meth:`__enter__` method. + manager's own :meth:`~object.__enter__` method. These context managers may suppress exceptions just as they normally would if used directly as part of a :keyword:`with` statement. + .. versionchanged:: 24.6.0 + When running on Python 3.11 or later, raises :exc:`TypeError` instead + of :exc:`AttributeError` if *cm* is not a context manager. This aligns + with the behaviour of :keyword:`with` statements in Python 3.11+. + .. method:: push(exit) - Adds a context manager's :meth:`__exit__` method to the callback stack. + Adds a context manager's :meth:`~object.__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. + part of an :meth:`~object.__enter__` implementation with a context manager's own + :meth:`~object.__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. + :meth:`~object.__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. + same way context manager :meth:`~object.__exit__` methods can. The passed in object is returned from the function, allowing this method to be used as a function decorator. @@ -562,26 +612,33 @@ Functions and classes provided: asynchronous context managers, as well as having coroutines for cleanup logic. - The :meth:`close` method is not implemented, :meth:`aclose` must be used + The :meth:`~ExitStack.close` method is not implemented; :meth:`aclose` must be used instead. .. method:: enter_async_context(cm) + :async: - Similar to :meth:`enter_context` but expects an asynchronous context + Similar to :meth:`ExitStack.enter_context` but expects an asynchronous context manager. + .. versionchanged:: 24.6.0 + When running on Python 3.11 or later, raises :exc:`TypeError` instead + of :exc:`AttributeError` if *cm* is not an asynchronous context manager. + This aligns with the behaviour of ``async with`` statements in Python 3.11+. + .. method:: push_async_exit(exit) - Similar to :meth:`push` but expects either an asynchronous context manager + Similar to :meth:`ExitStack.push` but expects either an asynchronous context manager or a coroutine function. .. method:: push_async_callback(callback, /, *args, **kwds) - Similar to :meth:`callback` but expects a coroutine function. + Similar to :meth:`ExitStack.callback` but expects a coroutine function. .. method:: aclose() + :async: - Similar to :meth:`close` but properly handles awaitables. + Similar to :meth:`ExitStack.close` but properly handles awaitables. Continuing the example for :func:`asynccontextmanager`:: @@ -600,7 +657,7 @@ Examples and Recipes -------------------- This section describes some examples and recipes for making effective use of -the tools provided by :mod:`contextlib`. +the tools provided by :mod:`contextlib2`. Supporting a variable number of context managers @@ -658,13 +715,13 @@ Cleaning up in an ``__enter__`` implementation As noted in the documentation of :meth:`ExitStack.push`, this method can be useful in cleaning up an already allocated resource if later -steps in the :meth:`__enter__` implementation fail. +steps in the :meth:`~object.__enter__` implementation fail. Here's an example of doing this for a context manager that accepts resource acquisition and release functions, along with an optional validation function, and maps them to the context management protocol:: - from contextlib import contextmanager, AbstractContextManager, ExitStack + from contextlib2 import contextmanager, AbstractContextManager, ExitStack class ResourceManager(AbstractContextManager): @@ -724,7 +781,7 @@ up being separated by arbitrarily long sections of code. execution at the end of a ``with`` statement, and then later decide to skip executing that callback:: - from contextlib import ExitStack + from contextlib2 import ExitStack with ExitStack() as stack: stack.callback(cleanup_resources) @@ -738,7 +795,7 @@ rather than requiring a separate flag variable. If a particular application uses this pattern a lot, it can be simplified even further by means of a small helper class:: - from contextlib import ExitStack + from contextlib2 import ExitStack class Callback(ExitStack): def __init__(self, callback, /, *args, **kwds): @@ -758,7 +815,7 @@ function, then it is still possible to use the decorator form of :meth:`ExitStack.callback` to declare the resource cleanup in advance:: - from contextlib import ExitStack + from contextlib2 import ExitStack with ExitStack() as stack: @stack.callback @@ -785,7 +842,7 @@ writing both a function decorator and a context manager for the task, inheriting from :class:`ContextDecorator` provides both capabilities in a single definition:: - from contextlib import ContextDecorator + from contextlib2 import ContextDecorator import logging logging.basicConfig(level=logging.INFO) @@ -815,7 +872,7 @@ And also as a function decorator:: 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 +:meth:`~object.__enter__`. If that value is needed, then it is still necessary to use an explicit ``with`` statement. .. seealso:: @@ -847,7 +904,7 @@ 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 + >>> from contextlib2 import contextmanager >>> @contextmanager ... def singleuse(): ... print("Before") @@ -879,10 +936,10 @@ 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 are -:func:`suppress` and :func:`redirect_stdout`. Here's a very simple example of -reentrant use:: +:func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very +simple example of reentrant use:: - >>> from contextlib import redirect_stdout + >>> from contextlib2 import redirect_stdout >>> from io import StringIO >>> stream = StringIO() >>> write_to_stream = redirect_stdout(stream) @@ -928,7 +985,7 @@ Another example of a reusable, but not reentrant, context manager is when leaving any with statement, regardless of where those callbacks were added:: - >>> from contextlib import ExitStack + >>> from contextlib2 import ExitStack >>> stack = ExitStack() >>> with stack: ... stack.callback(print, "Callback: from first context") @@ -962,7 +1019,7 @@ statement, which is unlikely to be desirable behaviour. Using separate :class:`ExitStack` instances instead of reusing a single instance avoids that problem:: - >>> from contextlib import ExitStack + >>> from contextlib2 import ExitStack >>> with ExitStack() as outer_stack: ... outer_stack.callback(print, "Callback: from outer context") ... with ExitStack() as inner_stack: diff --git a/docs/index.rst b/docs/index.rst index 2facd28..70a775a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,14 +21,15 @@ involving the ``with`` and ``async with`` statements. Additions Relative to the Standard Library ------------------------------------------ -This module is primarily a backport of the Python 3.10 version of -:mod:`contextlib` to earlier releases. The async context management features -require asynchronous generator support in the language runtime, so the oldest -supported version is now Python 3.6 (contextlib2 0.6.0 and earlier support -older Python versions by omitting all asynchronous features). +This module is primarily a backport of the Python 3.12.3 version of +:mod:`contextlib` to earlier releases. (Note: as of the start of the Python 3.13 +beta release cycle, there have been no subsequent changes to ``contextlib``) -This module is also a proving ground for new features not yet part of the -standard library. There are currently no such features in the module. +The module makes use of positional-only argument syntax in several call +signatures, so the oldest supported Python version is Python 3.8. + +This module may also be used as a proving ground for new features not yet part +of the standard library. There are currently no such features in the module. Finally, this module contains some deprecated APIs which never graduated to standard library inclusion. These interfaces are no longer documented, but may diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..6c5d5d4 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx-rtd-theme diff --git a/test/data/README.txt b/test/data/README.txt new file mode 100644 index 0000000..d020952 --- /dev/null +++ b/test/data/README.txt @@ -0,0 +1 @@ +test_contextlib uses this folder for chdir tests diff --git a/test/support/__init__.py b/test/support/__init__.py index 1e708d1..8bf7693 100644 --- a/test/support/__init__.py +++ b/test/support/__init__.py @@ -2,5 +2,100 @@ import sys import unittest +# Extra contextlib2 helpers checking CPython version-dependent details +_py_ver = sys.version_info + +cl2_gens_have_gi_suspended = (_py_ver >= (3, 11)) +cl2_async_gens_have_ag_suspended = (_py_ver >= (3, 12)) + +cl2_have_exception_groups = (_py_ver >= (3, 11)) +cl2_requires_exception_groups = unittest.skipIf(not cl2_have_exception_groups, + "Test requires exception groups") + +cl2_check_traceback_details = (_py_ver >= (3, 10)) + +# CM protocol checking switched to TypeError in Python 3.11 +cl2_cm_api_exc_type = TypeError if (_py_ver >= (3, 11)) else AttributeError +if cl2_cm_api_exc_type is AttributeError: + cl2_cm_api_exc_text_sync = { + "": "has no attribute", + "__enter__": "__enter__", + "__exit__": "__exit__", + } + cl2_cm_api_exc_text_async = cl2_cm_api_exc_text_sync +else: + cl2_cm_api_exc_text_sync = { + "": "the context manager", + "__enter__": "the context manager", + "__exit__": "the context manager.*__exit__", + } + cl2_cm_api_exc_text_async = { + "": "asynchronous context manager", + "__enter__": "asynchronous context manager", + "__exit__": "asynchronous context manager.*__exit__", + } + +def cl2_cm_api_exc_info_sync(check_context="", /): + return cl2_cm_api_exc_type, cl2_cm_api_exc_text_sync[check_context] + +def cl2_cm_api_exc_info_async(check_context="", /): + return cl2_cm_api_exc_type, cl2_cm_api_exc_text_async[check_context] + +# Some tests check docstring details requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, "Test requires docstrings") + +# Some tests check CPython implementation details +def _parse_guards(guards): + # Returns a tuple ({platform_name: run_me}, default_value) + if not guards: + return ({'cpython': True}, False) + is_true = list(guards.values())[0] + assert list(guards.values()) == [is_true] * len(guards) # all True or all False + return (guards, not is_true) + +# Use the following check to guard CPython's implementation-specific tests -- +# or to run them only on the implementation(s) guarded by the arguments. +def check_impl_detail(**guards): + """This function returns True or False depending on the host platform. + Examples: + if check_impl_detail(): # only on CPython (default) + if check_impl_detail(jython=True): # only on Jython + if check_impl_detail(cpython=False): # everywhere except on CPython + """ + guards, default = _parse_guards(guards) + return guards.get(sys.implementation.name, default) + +# Early reference release tests force gc collection +def gc_collect(): + """Force as many objects as possible to be collected. + + In non-CPython implementations of Python, this is needed because timely + deallocation is not guaranteed by the garbage collector. (Even in CPython + this can be the case in case of reference cycles.) This means that __del__ + methods may be called later than expected and weakrefs may remain alive for + longer than expected. This function tries its best to force all garbage + objects to disappear. + """ + import gc + gc.collect() + gc.collect() + gc.collect() + +# test_contextlib_async includes some socket-based tests +# Emscripten's socket emulation and WASI sockets have limitations. +is_emscripten = sys.platform == "emscripten" +is_wasi = sys.platform == "wasi" +has_socket_support = not is_emscripten and not is_wasi + +def requires_working_socket(*, module=False): + """Skip tests or modules that require working sockets + + Can be used as a function/class decorator or to skip an entire module. + """ + msg = "requires socket support" + if module: + if not has_socket_support: + raise unittest.SkipTest(msg) + else: + return unittest.skipUnless(has_socket_support, msg) diff --git a/test/support/testcase.py b/test/support/testcase.py new file mode 100644 index 0000000..ecba716 --- /dev/null +++ b/test/support/testcase.py @@ -0,0 +1,33 @@ +"""Enough of the test.support.testcase APIs to run the contextlib test suite""" +from . import cl2_have_exception_groups + +if not cl2_have_exception_groups: + # Placeholder to let the isinstance check below run on older versions + class ExceptionGroup(Exception): + pass + +class ExceptionIsLikeMixin: + def assertExceptionIsLike(self, exc, template): + """ + Passes when the provided `exc` matches the structure of `template`. + Individual exceptions don't have to be the same objects or even pass + an equality test: they only need to be the same type and contain equal + `exc_obj.args`. + """ + if exc is None and template is None: + return + + if template is None: + self.fail(f"unexpected exception: {exc}") + + if exc is None: + self.fail(f"expected an exception like {template!r}, got None") + + if not isinstance(exc, ExceptionGroup): + self.assertEqual(exc.__class__, template.__class__) + self.assertEqual(exc.args[0], template.args[0]) + else: + self.assertEqual(exc.message, template.message) + self.assertEqual(len(exc.exceptions), len(template.exceptions)) + for e, t in zip(exc.exceptions, template.exceptions): + self.assertExceptionIsLike(e, t) diff --git a/test/test_contextlib.py b/test/test_contextlib.py index b29478c..32e1550 100644 --- a/test/test_contextlib.py +++ b/test/test_contextlib.py @@ -1,15 +1,17 @@ -"""Unit tests for contextlib.py, and other context managers.""" +"""Unit tests for synchronous features of contextlib2.py""" import io +import os import sys import tempfile import threading +import traceback import unittest from contextlib2 import * # Tests __all__ from test import support from test.support import os_helper +from test.support.testcase import ExceptionIsLikeMixin import weakref -import gc class TestAbstractContextManager(unittest.TestCase): @@ -87,6 +89,56 @@ class ContextManagerTestCase(unittest.TestCase): raise ZeroDivisionError() self.assertEqual(state, [1, 42, 999]) + def test_contextmanager_traceback(self): + @contextmanager + def f(): + yield + + try: + with f(): + 1/0 + except ZeroDivisionError as e: + frames = traceback.extract_tb(e.__traceback__) + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].name, 'test_contextmanager_traceback') + self.assertEqual(frames[0].line, '1/0') + + # Repeat with RuntimeError (which goes through a different code path) + class RuntimeErrorSubclass(RuntimeError): + pass + + try: + with f(): + raise RuntimeErrorSubclass(42) + except RuntimeErrorSubclass as e: + frames = traceback.extract_tb(e.__traceback__) + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].name, 'test_contextmanager_traceback') + self.assertEqual(frames[0].line, 'raise RuntimeErrorSubclass(42)') + + class StopIterationSubclass(StopIteration): + pass + + for stop_exc in ( + StopIteration('spam'), + StopIterationSubclass('spam'), + ): + with self.subTest(type=type(stop_exc)): + try: + with f(): + raise stop_exc + except type(stop_exc) as e: + self.assertIs(e, stop_exc) + frames = traceback.extract_tb(e.__traceback__) + else: + self.fail(f'{stop_exc} was suppressed') + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].name, 'test_contextmanager_traceback') + self.assertEqual(frames[0].line, 'raise stop_exc') + def test_contextmanager_no_reraise(self): @contextmanager def whee(): @@ -105,9 +157,48 @@ class ContextManagerTestCase(unittest.TestCase): yield ctx = whoo() ctx.__enter__() - self.assertRaises( - RuntimeError, ctx.__exit__, TypeError, TypeError("foo"), None - ) + with self.assertRaises(RuntimeError): + ctx.__exit__(TypeError, TypeError("foo"), None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. + if support.cl2_gens_have_gi_suspended: + self.assertFalse(ctx.gen.gi_suspended) + + def test_contextmanager_trap_no_yield(self): + @contextmanager + def whoo(): + if False: + yield + ctx = whoo() + with self.assertRaises(RuntimeError): + ctx.__enter__() + + def test_contextmanager_trap_second_yield(self): + @contextmanager + def whoo(): + yield + yield + ctx = whoo() + ctx.__enter__() + with self.assertRaises(RuntimeError): + ctx.__exit__(None, None, None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. + if support.cl2_gens_have_gi_suspended: + self.assertFalse(ctx.gen.gi_suspended) + + def test_contextmanager_non_normalised(self): + @contextmanager + def whoo(): + try: + yield + except RuntimeError: + raise SyntaxError + + ctx = whoo() + ctx.__enter__() + with self.assertRaises(SyntaxError): + ctx.__exit__(RuntimeError, None, None) def test_contextmanager_except(self): state = [] @@ -127,19 +218,22 @@ class ContextManagerTestCase(unittest.TestCase): self.assertEqual(state, [1, 42, 999]) def test_contextmanager_except_stopiter(self): - stop_exc = StopIteration('spam') @contextmanager def woohoo(): yield - try: - with self.assertWarnsRegex(DeprecationWarning, - "StopIteration"): - with woohoo(): - raise stop_exc - except Exception as ex: - self.assertIs(ex, stop_exc) - else: - self.fail('StopIteration was suppressed') + + class StopIterationSubclass(StopIteration): + pass + + for stop_exc in (StopIteration('spam'), StopIterationSubclass('spam')): + with self.subTest(type=type(stop_exc)): + try: + with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail(f'{stop_exc} was suppressed') def test_contextmanager_except_pep479(self): code = """\ @@ -185,6 +279,25 @@ def woohoo(): self.assertEqual(ex.args[0], 'issue29692:Unchained') self.assertIsNone(ex.__cause__) + def test_contextmanager_wrap_runtimeerror(self): + @contextmanager + def woohoo(): + try: + yield + except Exception as exc: + raise RuntimeError(f'caught {exc}') from exc + + with self.assertRaises(RuntimeError): + with woohoo(): + 1 / 0 + + # If the context manager wrapped StopIteration 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(StopIteration): + with woohoo(): + raise StopIteration + def _create_contextmanager_attribs(self): def attribs(**kw): def decorate(func): @@ -196,6 +309,7 @@ def woohoo(): @attribs(foo='bar') def baz(spam): """Whee!""" + yield return baz def test_contextmanager_attribs(self): @@ -230,7 +344,7 @@ def woohoo(): a = weakref.ref(a) b = weakref.ref(b) # Allow test to work with a non-refcounted GC - gc.collect(); gc.collect(); gc.collect() + support.gc_collect() self.assertIsNone(a()) self.assertIsNone(b()) yield @@ -252,8 +366,11 @@ def woohoo(): def test_recursive(self): depth = 0 + ncols = 0 @contextmanager def woohoo(): + nonlocal ncols + ncols += 1 nonlocal depth before = depth depth += 1 @@ -267,6 +384,7 @@ def woohoo(): recursive() recursive() + self.assertEqual(ncols, 10) self.assertEqual(depth, 0) @@ -494,8 +612,8 @@ class TestContextDecorator(unittest.TestCase): def __exit__(self, *exc): pass - # 3.11+ raises TypeError, older versions raise AttributeError - with self.assertRaises((AttributeError, TypeError)): + expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__enter__") + with self.assertRaisesRegex(expected_error, expected_text): with mycontext(): pass @@ -507,8 +625,8 @@ class TestContextDecorator(unittest.TestCase): def __uxit__(self, *exc): pass - # 3.11+ raises TypeError, older versions raise AttributeError - with self.assertRaises((AttributeError, TypeError)): + expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__exit__") + with self.assertRaisesRegex(expected_error, expected_text): with mycontext(): pass @@ -666,6 +784,26 @@ class TestBaseExitStack: result.append(2) self.assertEqual(result, [1, 2, 3, 4]) + def test_enter_context_errors(self): + class LacksEnterAndExit: + pass + class LacksEnter: + def __exit__(self, *exc_info): + pass + class LacksExit: + def __enter__(self): + pass + + expected_error, expected_text = support.cl2_cm_api_exc_info_sync() + with self.exit_stack() as stack: + with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(LacksEnterAndExit()) + with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(LacksEnter()) + with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(LacksExit()) + self.assertFalse(stack._exit_callbacks) + def test_close(self): result = [] with self.exit_stack() as stack: @@ -701,6 +839,41 @@ class TestBaseExitStack: stack.push(lambda *exc: True) 1/0 + def test_exit_exception_traceback(self): + # This test captures the current behavior of ExitStack so that we know + # if we ever unintendedly change it. It is not a statement of what the + # desired behavior is (for instance, we may want to remove some of the + # internal contextlib frames). + + def raise_exc(exc): + raise exc + + try: + with self.exit_stack() as stack: + stack.callback(raise_exc, ValueError) + 1/0 + except ValueError as e: + exc = e + + self.assertIsInstance(exc, ValueError) + ve_frames = traceback.extract_tb(exc.__traceback__) + expected = \ + [('test_exit_exception_traceback', 'with self.exit_stack() as stack:')] + \ + self.callback_error_internal_frames + \ + [('_exit_wrapper', 'callback(*args, **kwds)'), + ('raise_exc', 'raise exc')] + + # This check fails on PyPy 3.10 + # It also fails on CPython 3.9 and earlier versions + if support.check_impl_detail(cpython=True) and support.cl2_check_traceback_details: + self.assertEqual( + [(f.name, f.line) for f in ve_frames], expected) + + self.assertIsInstance(exc.__context__, ZeroDivisionError) + zde_frames = traceback.extract_tb(exc.__context__.__traceback__) + self.assertEqual([(f.name, f.line) for f in zde_frames], + [('test_exit_exception_traceback', '1/0')]) + def test_exit_exception_chaining_reference(self): # Sanity check to make sure that ExitStack chaining matches # actual nested with statements @@ -780,6 +953,40 @@ class TestBaseExitStack: self.assertIsInstance(inner_exc, ValueError) self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + def test_exit_exception_explicit_none_context(self): + # Ensure ExitStack chaining matches actual nested `with` statements + # regarding explicit __context__ = None. + + class MyException(Exception): + pass + + @contextmanager + def my_cm(): + try: + yield + except BaseException: + exc = MyException() + try: + raise exc + finally: + exc.__context__ = None + + @contextmanager + def my_cm_with_exit_stack(): + with self.exit_stack() as stack: + stack.enter_context(my_cm()) + yield stack + + for cm in (my_cm, my_cm_with_exit_stack): + with self.subTest(): + try: + with cm(): + raise IndexError() + except MyException as exc: + self.assertIsNone(exc.__context__) + else: + self.fail("Expected IndexError, but no exception was raised") + def test_exit_exception_non_suppressing(self): # http://bugs.python.org/issue19092 def raise_exc(exc): @@ -891,9 +1098,12 @@ class TestBaseExitStack: def test_instance_bypass(self): class Example(object): pass cm = Example() + cm.__enter__ = object() cm.__exit__ = object() stack = self.exit_stack() - self.assertRaises(AttributeError, stack.enter_context, cm) + expected_error, expected_text = support.cl2_cm_api_exc_info_sync() + with self.assertRaisesRegex(expected_error, expected_text): + stack.enter_context(cm) stack.push(cm) self.assertIs(stack._exit_callbacks[-1][1], cm) @@ -934,6 +1144,10 @@ class TestBaseExitStack: class TestExitStack(TestBaseExitStack, unittest.TestCase): exit_stack = ExitStack + callback_error_internal_frames = [ + ('__exit__', 'raise exc_details[1]'), + ('__exit__', 'if cb(*exc_details):'), + ] class TestRedirectStream: @@ -1005,7 +1219,7 @@ class TestRedirectStderr(TestRedirectStream, unittest.TestCase): orig_stream = "stderr" -class TestSuppress(unittest.TestCase): +class TestSuppress(ExceptionIsLikeMixin, unittest.TestCase): @support.requires_docstrings def test_instance_docs(self): @@ -1059,5 +1273,96 @@ class TestSuppress(unittest.TestCase): 1/0 self.assertTrue(outer_continued) + @support.cl2_requires_exception_groups + def test_exception_groups(self): + eg_ve = lambda: ExceptionGroup( + "EG with ValueErrors only", + [ValueError("ve1"), ValueError("ve2"), ValueError("ve3")], + ) + eg_all = lambda: ExceptionGroup( + "EG with many types of exceptions", + [ValueError("ve1"), KeyError("ke1"), ValueError("ve2"), KeyError("ke2")], + ) + with suppress(ValueError): + raise eg_ve() + with suppress(ValueError, KeyError): + raise eg_all() + with self.assertRaises(ExceptionGroup) as eg1: + with suppress(ValueError): + raise eg_all() + self.assertExceptionIsLike( + eg1.exception, + ExceptionGroup( + "EG with many types of exceptions", + [KeyError("ke1"), KeyError("ke2")], + ), + ) + # Check handling of BaseExceptionGroup, using GeneratorExit so that + # we don't accidentally discard a ctrl-c with KeyboardInterrupt. + with suppress(GeneratorExit): + raise BaseExceptionGroup("message", [GeneratorExit()]) + # If we raise a BaseException group, we can still suppress parts + with self.assertRaises(BaseExceptionGroup) as eg1: + with suppress(KeyError): + raise BaseExceptionGroup("message", [GeneratorExit("g"), KeyError("k")]) + self.assertExceptionIsLike( + eg1.exception, BaseExceptionGroup("message", [GeneratorExit("g")]), + ) + # If we suppress all the leaf BaseExceptions, we get a non-base ExceptionGroup + with self.assertRaises(ExceptionGroup) as eg1: + with suppress(GeneratorExit): + raise BaseExceptionGroup("message", [GeneratorExit("g"), KeyError("k")]) + self.assertExceptionIsLike( + eg1.exception, ExceptionGroup("message", [KeyError("k")]), + ) + + +class TestChdir(unittest.TestCase): + def make_relative_path(self, *parts): + return os.path.join( + os.path.dirname(os.path.realpath(__file__)), + *parts, + ) + + def test_simple(self): + old_cwd = os.getcwd() + target = self.make_relative_path('data') + self.assertNotEqual(old_cwd, target) + + with chdir(target): + self.assertEqual(os.getcwd(), target) + self.assertEqual(os.getcwd(), old_cwd) + + def test_reentrant(self): + old_cwd = os.getcwd() + target1 = self.make_relative_path('data') + target2 = self.make_relative_path('ziptestdata') + self.assertNotIn(old_cwd, (target1, target2)) + chdir1, chdir2 = chdir(target1), chdir(target2) + + with chdir1: + self.assertEqual(os.getcwd(), target1) + with chdir2: + self.assertEqual(os.getcwd(), target2) + with chdir1: + self.assertEqual(os.getcwd(), target1) + self.assertEqual(os.getcwd(), target2) + self.assertEqual(os.getcwd(), target1) + self.assertEqual(os.getcwd(), old_cwd) + + def test_exception(self): + old_cwd = os.getcwd() + target = self.make_relative_path('data') + self.assertNotEqual(old_cwd, target) + + try: + with chdir(target): + self.assertEqual(os.getcwd(), target) + raise RuntimeError("boom") + except RuntimeError as re: + self.assertEqual(str(re), "boom") + self.assertEqual(os.getcwd(), old_cwd) + + if __name__ == "__main__": unittest.main() diff --git a/test/test_contextlib_async.py b/test/test_contextlib_async.py index eb2ed72..42b5502 100644 --- a/test/test_contextlib_async.py +++ b/test/test_contextlib_async.py @@ -1,3 +1,5 @@ +"""Unit tests for asynchronous features of contextlib2.py""" + import asyncio from contextlib2 import ( asynccontextmanager, AbstractAsyncContextManager, @@ -5,24 +7,23 @@ from contextlib2 import ( import functools from test import support import unittest +import traceback -from test.test_contextlib import TestBaseExitStack +from .test_contextlib import TestBaseExitStack +support.requires_working_socket(module=True) 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) + asyncio.run(coro) return wrapper +def tearDownModule(): + asyncio.set_event_loop_policy(None) + class TestAbstractAsyncContextManager(unittest.TestCase): @@ -50,15 +51,11 @@ class TestAbstractAsyncContextManager(unittest.TestCase): 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]) + g = gen() + async for val in g: + self.assertEqual(val, 11) + break + await g.aclose() def test_exit_is_abstract(self): class MissingAexit(AbstractAsyncContextManager): @@ -127,6 +124,62 @@ class AsyncContextManagerTestCase(unittest.TestCase): raise ZeroDivisionError() self.assertEqual(state, [1, 42, 999]) + @_async_test + async def test_contextmanager_traceback(self): + @asynccontextmanager + async def f(): + yield + + try: + async with f(): + 1/0 + except ZeroDivisionError as e: + frames = traceback.extract_tb(e.__traceback__) + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].name, 'test_contextmanager_traceback') + self.assertEqual(frames[0].line, '1/0') + + # Repeat with RuntimeError (which goes through a different code path) + class RuntimeErrorSubclass(RuntimeError): + pass + + try: + async with f(): + raise RuntimeErrorSubclass(42) + except RuntimeErrorSubclass as e: + frames = traceback.extract_tb(e.__traceback__) + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].name, 'test_contextmanager_traceback') + self.assertEqual(frames[0].line, 'raise RuntimeErrorSubclass(42)') + + class StopIterationSubclass(StopIteration): + pass + + class StopAsyncIterationSubclass(StopAsyncIteration): + pass + + for stop_exc in ( + StopIteration('spam'), + StopAsyncIteration('ham'), + StopIterationSubclass('spam'), + StopAsyncIterationSubclass('spam') + ): + with self.subTest(type=type(stop_exc)): + try: + async with f(): + raise stop_exc + except type(stop_exc) as e: + self.assertIs(e, stop_exc) + frames = traceback.extract_tb(e.__traceback__) + else: + self.fail(f'{stop_exc} was suppressed') + + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].name, 'test_contextmanager_traceback') + self.assertEqual(frames[0].line, 'raise stop_exc') + @_async_test async def test_contextmanager_no_reraise(self): @asynccontextmanager @@ -149,6 +202,10 @@ class AsyncContextManagerTestCase(unittest.TestCase): await ctx.__aenter__() with self.assertRaises(RuntimeError): await ctx.__aexit__(TypeError, TypeError('foo'), None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. + if support.cl2_async_gens_have_ag_suspended: + self.assertFalse(ctx.gen.ag_suspended) @_async_test async def test_contextmanager_trap_no_yield(self): @@ -170,6 +227,10 @@ class AsyncContextManagerTestCase(unittest.TestCase): await ctx.__aenter__() with self.assertRaises(RuntimeError): await ctx.__aexit__(None, None, None) + if support.check_impl_detail(cpython=True): + # The "gen" attribute is an implementation detail. + if support.cl2_async_gens_have_ag_suspended: + self.assertFalse(ctx.gen.ag_suspended) @_async_test async def test_contextmanager_non_normalised(self): @@ -209,7 +270,18 @@ class AsyncContextManagerTestCase(unittest.TestCase): async def woohoo(): yield - for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')): + class StopIterationSubclass(StopIteration): + pass + + class StopAsyncIterationSubclass(StopAsyncIteration): + pass + + for stop_exc in ( + StopIteration('spam'), + StopAsyncIteration('ham'), + StopIterationSubclass('spam'), + StopAsyncIterationSubclass('spam') + ): with self.subTest(type=type(stop_exc)): try: async with woohoo(): @@ -307,6 +379,82 @@ class AsyncContextManagerTestCase(unittest.TestCase): self.assertEqual(ncols, 10) self.assertEqual(depth, 0) + @_async_test + async def test_decorator(self): + entered = False + + @asynccontextmanager + async def context(): + nonlocal entered + entered = True + yield + entered = False + + @context() + async def test(): + self.assertTrue(entered) + + self.assertFalse(entered) + await test() + self.assertFalse(entered) + + @_async_test + async def test_decorator_with_exception(self): + entered = False + + @asynccontextmanager + async def context(): + nonlocal entered + try: + entered = True + yield + finally: + entered = False + + @context() + async def test(): + self.assertTrue(entered) + raise NameError('foo') + + self.assertFalse(entered) + with self.assertRaisesRegex(NameError, 'foo'): + await test() + self.assertFalse(entered) + + @_async_test + async def test_decorating_method(self): + + @asynccontextmanager + async def context(): + yield + + + class Test(object): + + @context() + async def method(self, a, b, c=None): + self.a = a + self.b = b + self.c = c + + # these tests are for argument passing when used as a decorator + test = Test() + await test.method(1, 2) + self.assertEqual(test.a, 1) + self.assertEqual(test.b, 2) + self.assertEqual(test.c, None) + + test = Test() + await test.method('a', 'b', 'c') + self.assertEqual(test.a, 'a') + self.assertEqual(test.b, 'b') + self.assertEqual(test.c, 'c') + + test = Test() + await test.method(a=1, b=2) + self.assertEqual(test.a, 1) + self.assertEqual(test.b, 2) + class AclosingTestCase(unittest.TestCase): @@ -399,6 +547,13 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): return self.run_coroutine(self.__aexit__(*exc_details)) exit_stack = SyncAsyncExitStack + callback_error_internal_frames = [ + ('__exit__', 'return self.run_coroutine(self.__aexit__(*exc_details))'), + ('run_coroutine', 'raise exc'), + ('run_coroutine', 'raise exc'), + ('__aexit__', 'raise exc_details[1]'), + ('__aexit__', 'cb_suppress = cb(*exc_details)'), + ] def setUp(self): self.loop = asyncio.new_event_loop() @@ -486,7 +641,7 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): 1/0 @_async_test - async def test_async_enter_context(self): + async def test_enter_async_context(self): class TestCM(object): async def __aenter__(self): result.append(1) @@ -507,6 +662,27 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): self.assertEqual(result, [1, 2, 3, 4]) + @_async_test + async def test_enter_async_context_errors(self): + class LacksEnterAndExit: + pass + class LacksEnter: + async def __aexit__(self, *exc_info): + pass + class LacksExit: + async def __aenter__(self): + pass + + expected_error, expected_text = support.cl2_cm_api_exc_info_async() + async with self.exit_stack() as stack: + with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(LacksEnterAndExit()) + with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(LacksEnter()) + with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(LacksExit()) + self.assertFalse(stack._exit_callbacks) + @_async_test async def test_async_exit_exception_chaining(self): # Ensure exception chaining matches the reference behaviour @@ -539,6 +715,54 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): self.assertIsInstance(inner_exc, ValueError) self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) + @_async_test + async def test_async_exit_exception_explicit_none_context(self): + # Ensure AsyncExitStack chaining matches actual nested `with` statements + # regarding explicit __context__ = None. + + class MyException(Exception): + pass + + @asynccontextmanager + async def my_cm(): + try: + yield + except BaseException: + exc = MyException() + try: + raise exc + finally: + exc.__context__ = None + + @asynccontextmanager + async def my_cm_with_exit_stack(): + async with self.exit_stack() as stack: + await stack.enter_async_context(my_cm()) + yield stack + + for cm in (my_cm, my_cm_with_exit_stack): + with self.subTest(): + try: + async with cm(): + raise IndexError() + except MyException as exc: + self.assertIsNone(exc.__context__) + else: + self.fail("Expected IndexError, but no exception was raised") + + @_async_test + async def test_instance_bypass_async(self): + class Example(object): pass + cm = Example() + cm.__aenter__ = object() + cm.__aexit__ = object() + stack = self.exit_stack() + expected_error, expected_text = support.cl2_cm_api_exc_info_async() + with self.assertRaisesRegex(expected_error, expected_text): + await stack.enter_async_context(cm) + stack.push_async_exit(cm) + self.assertIs(stack._exit_callbacks[-1][1], cm) + class TestAsyncNullcontext(unittest.TestCase): @_async_test diff --git a/test/ziptestdata/README.txt b/test/ziptestdata/README.txt new file mode 100644 index 0000000..d020952 --- /dev/null +++ b/test/ziptestdata/README.txt @@ -0,0 +1 @@ +test_contextlib uses this folder for chdir tests