mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-03-17 14:10:25 +00:00
Compare commits
34 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a547a3247 | ||
|
|
e713c7cd64 | ||
|
|
0d2a7d056e | ||
|
|
15d711ad16 | ||
|
|
892462960d | ||
|
|
3fbca593ec | ||
|
|
f64cf04df8 | ||
|
|
8fe4d73971 | ||
|
|
7b862aaa80 | ||
|
|
defc103aae | ||
|
|
7e2a0a900a | ||
|
|
7c6b72683c | ||
|
|
4be95fed29 | ||
|
|
690922cfde | ||
|
|
c127f51df1 | ||
|
|
241de07097 | ||
|
|
ff30b3a68f | ||
|
|
9ef8594837 | ||
|
|
89893bba60 | ||
|
|
26ad9aef80 | ||
|
|
c1d9e6a4a7 | ||
|
|
d63f68b104 | ||
|
|
0828b5a332 | ||
|
|
1ea4e4dfe0 | ||
|
|
3feaef8f91 | ||
|
|
89c5189648 | ||
|
|
5373afc4ba | ||
|
|
3d2540dda1 | ||
|
|
d78420a808 | ||
|
|
a35252eefd | ||
|
|
16db3dcf86 | ||
|
|
2dc4c04492 | ||
|
|
908e2da622 | ||
|
|
c907a9975e |
36 changed files with 2495 additions and 748 deletions
19
.github/workflows/test.yml
vendored
19
.github/workflows/test.yml
vendored
|
|
@ -1,6 +1,12 @@
|
|||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -8,13 +14,16 @@ jobs:
|
|||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9, '3.10.0-beta - 3.10', 'pypy3']
|
||||
python-version: [3.8, 3.9, '3.10', 3.11, 3.12, 'pypy-3.10']
|
||||
|
||||
# Check https://github.com/actions/action-versions/tree/main/config/actions
|
||||
# for latest versions if the standard actions start emitting warnings
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
|
@ -24,7 +33,7 @@ jobs:
|
|||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key:
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -11,3 +11,7 @@ MANIFEST
|
|||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
|
||||
# Patching output files
|
||||
*.orig
|
||||
*.rej
|
||||
|
|
|
|||
14
.pre-commit-config.yaml
Normal file
14
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
35
.readthedocs.yaml
Normal file
35
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the OS, Python version and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "20"
|
||||
# rust: "1.70"
|
||||
# golang: "1.20"
|
||||
|
||||
# Build documentation in the "docs/" directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
||||
# builder: "dirhtml"
|
||||
# Fail on all warnings to avoid broken references
|
||||
# fail_on_warning: true
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
# formats:
|
||||
# - pdf
|
||||
# - epub
|
||||
|
||||
# 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
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Code of Conduct
|
||||
|
||||
As contributors and maintainers of the Jazzband projects, and in the interest of
|
||||
fostering an open and welcoming community, we pledge to respect all people who
|
||||
contribute through reporting issues, posting feature requests, updating documentation,
|
||||
submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in the Jazzband a harassment-free experience
|
||||
for everyone, regardless of the level of experience, gender, gender identity and
|
||||
expression, sexual orientation, disability, personal appearance, body size, race,
|
||||
ethnicity, age, religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery
|
||||
- Personal attacks
|
||||
- Trolling or insulting/derogatory comments
|
||||
- Public or private harassment
|
||||
- Publishing other's private information, such as physical or electronic addresses,
|
||||
without explicit permission
|
||||
- Other unethical or unprofessional conduct
|
||||
|
||||
The Jazzband roadies have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are not
|
||||
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
|
||||
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
By adopting this Code of Conduct, the roadies commit themselves to fairly and
|
||||
consistently applying these principles to every aspect of managing the jazzband
|
||||
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
|
||||
removed from the Jazzband roadies.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces when an
|
||||
individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
|
||||
investigated and will result in a response that is deemed necessary and appropriate to
|
||||
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
|
||||
reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
|
||||
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
|
||||
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/3/0/
|
||||
|
|
@ -2,4 +2,4 @@ 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
|
||||
|
|
|
|||
36
NEWS.rst
36
NEWS.rst
|
|
@ -1,6 +1,40 @@
|
|||
Release History
|
||||
---------------
|
||||
|
||||
24.6.0 (2024-06-??)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* 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 <https://github.com/jazzband/contextlib2/issues/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 <https://github.com/jazzband/contextlib2/issues/54>`__)
|
||||
* The ``dev/mypy.allowlist`` file needed for the ``mypy stubtest`` step in the
|
||||
``tox`` test configuration is now included in the published sdist
|
||||
(`#53 <https://github.com/jazzband/contextlib2/issues/53>`__)
|
||||
* Type hints have been updated to include ``nullcontext`` (3.10 API added in
|
||||
21.6.0) (`#41 <https://github.com/jazzband/contextlib2/issues/41>`__)
|
||||
* Test suite updated to pass on Python 3.11 and 3.12 (21.6.0 works on these
|
||||
versions, the test suite just failed due to no longer valid assumptions)
|
||||
(`#51 <https://github.com/jazzband/contextlib2/issues/51>`__)
|
||||
* Updates to the default compatibility testing matrix:
|
||||
|
||||
* Added: CPython 3.11, CPython 3.12
|
||||
* Dropped: CPython 3.6, CPython 3.7
|
||||
|
||||
21.6.0 (2021-06-27)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
@ -64,7 +98,7 @@ Release History
|
|||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Thanks to the welcome efforts of Jannis Leidel, contextlib2 is now a
|
||||
[Jazzband](https://jazzband.co/) project! This means that I (Nick Coghlan)
|
||||
[Jazzband](https://jazzband.co/) project! This means that I (Alyssa Coghlan)
|
||||
am no longer a single point of failure for backports of future contextlib
|
||||
updates to earlier Python versions.
|
||||
|
||||
|
|
|
|||
40
README.rst
40
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.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
21.6.0
|
||||
24.6.0rc1
|
||||
|
|
|
|||
|
|
@ -1,32 +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.6/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"]
|
||||
|
||||
# Backwards compatibility
|
||||
__all__ += ["ContextStack"]
|
||||
|
||||
class AbstractContextManager(abc.ABC):
|
||||
|
||||
"""An abstract base class for context managers."""
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
|
@ -86,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()
|
||||
|
|
@ -143,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
|
||||
|
|
@ -164,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
|
||||
|
|
@ -187,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
|
||||
|
|
@ -256,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):
|
||||
|
|
@ -449,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:
|
||||
|
|
@ -460,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
|
||||
|
|
@ -505,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
|
||||
|
|
@ -565,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
|
||||
|
|
@ -628,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
|
||||
|
|
@ -641,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
|
||||
|
||||
|
|
@ -665,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
|
||||
|
|
@ -708,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
|
||||
|
|
@ -779,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__()
|
||||
|
|
|
|||
|
|
@ -1,132 +1,201 @@
|
|||
# Type hints copied from the typeshed project under the Apache License 2.0
|
||||
# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE
|
||||
|
||||
import sys
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
IO,
|
||||
Any,
|
||||
AsyncContextManager,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Iterator,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
overload,
|
||||
)
|
||||
from typing_extensions import ParamSpec, Protocol
|
||||
# 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)
|
||||
|
||||
AbstractContextManager = ContextManager
|
||||
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 True:
|
||||
AbstractAsyncContextManager = AsyncContextManager
|
||||
__all__ += ["aclosing"]
|
||||
|
||||
if True:
|
||||
__all__ += ["chdir"]
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
_T_io = TypeVar("_T_io", bound=Optional[IO[str]])
|
||||
_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 = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], bool]
|
||||
_CM_EF = TypeVar("_CM_EF", ContextManager[Any], _ExitFunc)
|
||||
_ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None]
|
||||
_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc)
|
||||
|
||||
class _GeneratorContextManager(ContextManager[_T_co]):
|
||||
@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: ...
|
||||
|
||||
# type ignore to deal with incomplete ParamSpec support in mypy
|
||||
def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, _GeneratorContextManager[_T]]: ... # type: ignore
|
||||
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 True:
|
||||
def __exit__(
|
||||
self, typ: 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 True:
|
||||
def asynccontextmanager(func: Callable[_P, AsyncIterator[_T]]) -> Callable[_P, AsyncContextManager[_T]]: ... # type: ignore
|
||||
_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: ...
|
||||
|
||||
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(ContextManager[_SupportsCloseT]):
|
||||
class closing(AbstractContextManager[_SupportsCloseT, None]):
|
||||
def __init__(self, thing: _SupportsCloseT) -> None: ...
|
||||
def __exit__(self, *exc_info: Unused) -> None: ...
|
||||
|
||||
if True:
|
||||
class _SupportsAclose(Protocol):
|
||||
async def aclose(self) -> object: ...
|
||||
def aclose(self) -> Awaitable[object]: ...
|
||||
|
||||
_SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose)
|
||||
class aclosing(AsyncContextManager[_SupportsAcloseT]):
|
||||
|
||||
class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]):
|
||||
def __init__(self, thing: _SupportsAcloseT) -> None: ...
|
||||
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
|
||||
class AsyncContextDecorator:
|
||||
def __call__(self, func: _AF) -> _AF: ...
|
||||
async def __aexit__(self, *exc_info: Unused) -> None: ...
|
||||
|
||||
class suppress(ContextManager[None]):
|
||||
def __init__(self, *exceptions: Type[BaseException]) -> None: ...
|
||||
class suppress(AbstractContextManager[None, bool]):
|
||||
def __init__(self, *exceptions: type[BaseException]) -> None: ...
|
||||
def __exit__(
|
||||
self, exctype: Optional[Type[BaseException]], excinst: Optional[BaseException], exctb: Optional[TracebackType]
|
||||
self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None
|
||||
) -> bool: ...
|
||||
|
||||
class redirect_stdout(ContextManager[_T_io]):
|
||||
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_stderr(ContextManager[_T_io]):
|
||||
def __init__(self, new_target: _T_io) -> None: ...
|
||||
class redirect_stdout(_RedirectStream[_T_io]): ...
|
||||
class redirect_stderr(_RedirectStream[_T_io]): ...
|
||||
|
||||
class ContextDecorator:
|
||||
def __call__(self, func: _F) -> _F: ...
|
||||
|
||||
_U = TypeVar("_U", bound=ExitStack)
|
||||
|
||||
class ExitStack(ContextManager[ExitStack]):
|
||||
def __init__(self) -> None: ...
|
||||
def enter_context(self, cm: ContextManager[_T]) -> _T: ...
|
||||
# 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[..., Any], *args: Any, **kwds: Any) -> Callable[..., Any]: ...
|
||||
def pop_all(self: _U) -> _U: ...
|
||||
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: _U) -> _U: ...
|
||||
def __enter__(self) -> Self: ...
|
||||
def __exit__(
|
||||
self,
|
||||
__exc_type: Optional[Type[BaseException]],
|
||||
__exc_value: Optional[BaseException],
|
||||
__traceback: Optional[TracebackType],
|
||||
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 True:
|
||||
_S = TypeVar("_S", bound=AsyncExitStack)
|
||||
|
||||
_ExitCoroFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], Awaitable[bool]]
|
||||
_CallbackCoroFunc = Callable[..., Awaitable[Any]]
|
||||
_ACM_EF = TypeVar("_ACM_EF", AsyncContextManager[Any], _ExitCoroFunc)
|
||||
class AsyncExitStack(AsyncContextManager[AsyncExitStack]):
|
||||
def __init__(self) -> None: ...
|
||||
def enter_context(self, cm: ContextManager[_T]) -> _T: ...
|
||||
def enter_async_context(self, cm: AsyncContextManager[_T]) -> Awaitable[_T]: ...
|
||||
def push(self, exit: _CM_EF) -> _CM_EF: ...
|
||||
def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ...
|
||||
def callback(self, callback: Callable[..., Any], *args: Any, **kwds: Any) -> Callable[..., Any]: ...
|
||||
def push_async_callback(self, callback: _CallbackCoroFunc, *args: Any, **kwds: Any) -> _CallbackCoroFunc: ...
|
||||
def pop_all(self: _S) -> _S: ...
|
||||
def aclose(self) -> Awaitable[None]: ...
|
||||
def __aenter__(self: _S) -> Awaitable[_S]: ...
|
||||
def __aexit__(
|
||||
self,
|
||||
__exc_type: Optional[Type[BaseException]],
|
||||
__exc_value: Optional[BaseException],
|
||||
__traceback: Optional[TracebackType],
|
||||
) -> Awaitable[bool]: ...
|
||||
|
||||
if True:
|
||||
class nullcontext(AbstractContextManager[_T]):
|
||||
class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]):
|
||||
enter_result: _T
|
||||
@overload
|
||||
def __init__(self: nullcontext[None], enter_result: None = ...) -> None: ...
|
||||
def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
|
||||
@overload
|
||||
def __init__(self: nullcontext[_T], enter_result: _T) -> None: ...
|
||||
def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
|
||||
def __enter__(self) -> _T: ...
|
||||
def __exit__(self, *exctype: Any) -> bool: ...
|
||||
def __exit__(self, *exctype: Unused) -> None: ...
|
||||
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)
|
||||
|
||||
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: ...
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
# Deprecated APIs that never graduated to the standard library
|
||||
contextlib2.ContextDecorator.refresh_cm
|
||||
contextlib2.ContextStack
|
||||
|
||||
# stubcheck no longer complains about this one for some reason
|
||||
# (but it does complain about the unused allowlist entry)
|
||||
# contextlib2.ContextStack
|
||||
|
||||
# mypy seems to be confused by the GenericAlias compatibility hack
|
||||
contextlib2.AbstractAsyncContextManager.__class_getitem__
|
||||
contextlib2.AbstractContextManager.__class_getitem__
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
--- ../contextlib.pyi 2021-06-27 16:02:28.004872421 +1000
|
||||
+++ contextlib2/__init__.pyi 2021-06-27 16:00:25.431733524 +1000
|
||||
@@ -1,3 +1,6 @@
|
||||
+# Type hints copied from the typeshed project under the Apache License 2.0
|
||||
+# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE
|
||||
+
|
||||
import sys
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
@@ -16,8 +19,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")
|
||||
@@ -35,7 +44,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):
|
||||
@@ -46,7 +55,7 @@
|
||||
class closing(ContextManager[_SupportsCloseT]):
|
||||
def __init__(self, thing: _SupportsCloseT) -> None: ...
|
||||
|
||||
-if sys.version_info >= (3, 10):
|
||||
+if True:
|
||||
class _SupportsAclose(Protocol):
|
||||
async def aclose(self) -> object: ...
|
||||
_SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose)
|
||||
@@ -88,7 +97,7 @@
|
||||
__traceback: Optional[TracebackType],
|
||||
) -> bool: ...
|
||||
|
||||
-if sys.version_info >= (3, 7):
|
||||
+if True:
|
||||
_S = TypeVar("_S", bound=AsyncExitStack)
|
||||
|
||||
_ExitCoroFunc = Callable[[Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]], Awaitable[bool]]
|
||||
@@ -112,7 +121,7 @@
|
||||
__traceback: Optional[TracebackType],
|
||||
) -> Awaitable[bool]: ...
|
||||
|
||||
-if sys.version_info >= (3, 7):
|
||||
+if True:
|
||||
class nullcontext(AbstractContextManager[_T]):
|
||||
enter_result: _T
|
||||
@overload
|
||||
|
|
@ -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 <reentrant-cms>`.
|
||||
|
||||
- .. 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 <reentrant-cms>`.
|
||||
|
||||
- .. 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 <reentrant-cms>`.
|
||||
|
||||
- .. 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
|
||||
--------------------
|
||||
|
|
@ -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()
|
||||
116
dev/py3_12_py_to_contextlib2.patch
Normal file
116
dev/py3_12_py_to_contextlib2.patch
Normal file
|
|
@ -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()
|
||||
112
dev/py3_12_pyi_to_contextlib2.patch
Normal file
112
dev/py3_12_pyi_to_contextlib2.patch
Normal file
|
|
@ -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]):
|
||||
453
dev/py3_12_rst_to_contextlib2.patch
Normal file
453
dev/py3_12_rst_to_contextlib2.patch
Normal file
|
|
@ -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 <async-context-managers>`.
|
||||
|
||||
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 <reentrant-cms>`.
|
||||
|
||||
- .. 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 <reentrant-cms>`.
|
||||
|
||||
- .. 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 <reentrant-cms>`.
|
||||
|
||||
- .. 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:
|
||||
67
dev/py3_12_test_async_to_contextlib2.patch
Normal file
67
dev/py3_12_test_async_to_contextlib2.patch
Normal file
|
|
@ -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)
|
||||
106
dev/py3_12_test_to_contextlib2.patch
Normal file
106
dev/py3_12_test_to_contextlib2.patch
Normal file
|
|
@ -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",
|
||||
27
dev/save_diff_snapshot.sh
Executable file
27
dev/save_diff_snapshot.sh
Executable file
|
|
@ -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"
|
||||
19
dev/sync_from_cpython.sh
Executable file
19
dev/sync_from_cpython.sh
Executable file
|
|
@ -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
|
||||
209
dev/typeshed_contextlib.pyi
Normal file
209
dev/typeshed_contextlib.pyi
Normal file
|
|
@ -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: ...
|
||||
19
docs/conf.py
19
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']
|
||||
|
|
@ -41,7 +42,7 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'contextlib2'
|
||||
copyright = u'2021, Nick Coghlan'
|
||||
copyright = u'2024, Alyssa Coghlan'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
|
@ -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.
|
||||
|
|
@ -180,7 +181,7 @@ htmlhelp_basename = 'contextlib2doc'
|
|||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'contextlib2.tex', u'contextlib2 Documentation',
|
||||
u'Nick Coghlan', 'manual'),
|
||||
u'Alyssa Coghlan', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
|
@ -213,9 +214,9 @@ latex_documents = [
|
|||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'contextlib2', u'contextlib2 Documentation',
|
||||
[u'Nick Coghlan'], 1)
|
||||
[u'Alyssa Coghlan'], 1)
|
||||
]
|
||||
|
||||
|
||||
# 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)}
|
||||
|
|
|
|||
|
|
@ -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 <async-context-managers>`.
|
||||
|
||||
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-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 <reentrant-cms>`.
|
||||
|
||||
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 <reentrant-cms>`.
|
||||
|
|
@ -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 <reentrant-cms>`.
|
||||
|
||||
.. 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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -55,7 +56,7 @@ PyPI page`_.
|
|||
There are no operating system or distribution specific versions of this
|
||||
module - it is a pure Python module that should work on all platforms.
|
||||
|
||||
Supported Python versions are currently 3.6+.
|
||||
Supported Python versions are currently 3.8+.
|
||||
|
||||
.. _Python Package Index: http://pypi.python.org
|
||||
.. _pip: http://www.pip-installer.org
|
||||
|
|
|
|||
1
docs/requirements.txt
Normal file
1
docs/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
sphinx-rtd-theme
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[bdist_wheel]
|
||||
universal=1
|
||||
20
setup.py
20
setup.py
|
|
@ -4,18 +4,29 @@ try:
|
|||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
# Note: The minimum Python version requirement is set on the basis of
|
||||
# "if it's not tested, it's broken".
|
||||
# Specifically, if a Python version is no longer available for testing
|
||||
# in CI, then the minimum supported Python version will be increased.
|
||||
# That way there's no risk of a release that breaks older Python versions.
|
||||
|
||||
setup(
|
||||
name='contextlib2',
|
||||
version=open('VERSION.txt').read().strip(),
|
||||
python_requires='>=3.6',
|
||||
python_requires='>=3.7',
|
||||
packages=['contextlib2'],
|
||||
include_package_data=True,
|
||||
license='PSF License',
|
||||
description='Backports and enhancements for the contextlib module',
|
||||
long_description=open('README.rst').read(),
|
||||
author='Nick Coghlan',
|
||||
author='Alyssa Coghlan',
|
||||
author_email='ncoghlan@gmail.com',
|
||||
url='http://contextlib2.readthedocs.org',
|
||||
url='https://github.com/jazzband/contextlib2',
|
||||
project_urls= {
|
||||
'Documentation': 'https://contextlib2.readthedocs.org',
|
||||
'Source': 'https://github.com/jazzband/contextlib2.git',
|
||||
'Issue Tracker': 'https://github.com/jazzband/contextlib2.git',
|
||||
}
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
|
|
@ -23,11 +34,12 @@ setup(
|
|||
# These are the Python versions tested, it may work on others
|
||||
# It definitely won't work on versions without native async support
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
],
|
||||
|
||||
)
|
||||
|
|
|
|||
1
test/data/README.txt
Normal file
1
test/data/README.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
test_contextlib uses this folder for chdir tests
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
33
test/support/testcase.py
Normal file
33
test/support/testcase.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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,7 +612,8 @@ class TestContextDecorator(unittest.TestCase):
|
|||
def __exit__(self, *exc):
|
||||
pass
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__enter__")
|
||||
with self.assertRaisesRegex(expected_error, expected_text):
|
||||
with mycontext():
|
||||
pass
|
||||
|
||||
|
|
@ -506,7 +625,8 @@ class TestContextDecorator(unittest.TestCase):
|
|||
def __uxit__(self, *exc):
|
||||
pass
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__exit__")
|
||||
with self.assertRaisesRegex(expected_error, expected_text):
|
||||
with mycontext():
|
||||
pass
|
||||
|
||||
|
|
@ -664,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:
|
||||
|
|
@ -699,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
|
||||
|
|
@ -778,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):
|
||||
|
|
@ -889,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)
|
||||
|
||||
|
|
@ -932,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:
|
||||
|
|
@ -1003,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):
|
||||
|
|
@ -1057,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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
test/ziptestdata/README.txt
Normal file
1
test/ziptestdata/README.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
test_contextlib uses this folder for chdir tests
|
||||
11
tox.ini
11
tox.ini
|
|
@ -1,5 +1,6 @@
|
|||
[tox]
|
||||
envlist = py{36,37,38,39,3_10,py3}
|
||||
# Python 3.8 is the first version with positional-only argument syntax support
|
||||
envlist = py{38,39,3.10,3.11,3.12,py3}
|
||||
skip_missing_interpreters = True
|
||||
|
||||
[testenv]
|
||||
|
|
@ -15,9 +16,9 @@ deps =
|
|||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.6: py36
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py3_10
|
||||
pypy3: pypy3
|
||||
3.10: py3.10
|
||||
3.11: py3.11
|
||||
3.12: py3.12
|
||||
pypy-3.10: pypy3
|
||||
|
|
|
|||
Loading…
Reference in a new issue