Compare commits

...

34 commits

Author SHA1 Message Date
Ülgen Sarıkavak
9a547a3247
Merge pull request #61 from jazzband/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2025-10-06 20:48:14 +03:00
pre-commit-ci[bot]
e713c7cd64
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v6.0.0)
2025-10-06 17:43:36 +00:00
Alyssa Coghlan
0d2a7d056e
Avoid duplicate testing in PRs 2024-07-25 12:57:40 +10:00
Alyssa Coghlan
15d711ad16
Add more project URL links 2024-06-19 22:13:48 +10:00
Nick Coghlan
892462960d Remove random paste error from NEWS.rst 2024-05-23 18:06:18 +10:00
Nick Coghlan
3fbca593ec Set an rc version 2024-05-23 18:03:41 +10:00
Alyssa Coghlan
f64cf04df8
Sync with CPython 3.12.3 (#60)
* generated new diff files covering this sync
* added some helper scripts for the sync process
* fixed RTD theme regression (due to RTD changes since the last release)

Closes #43
2024-05-23 17:55:20 +10:00
Alyssa Coghlan
8fe4d73971
Fix typechecking with recent mypy releases (#59)
* sync with latest typeshed stub file (closes #54)
* publish `dev/mypy.allowlist` in sdist (closes #53)
* drop Python 3.7 support due to positional-only arg
  syntax in the updated stub file
2024-05-23 00:26:48 +10:00
Alyssa Coghlan
7b862aaa80
Update supported versions (#58)
* Add Python 3.11 and 3.12 to CI (adjusting affected test cases)
* Add Python 3.11 and 3.12 to package metadata
* Drop Python 3.6 from package metadata

Note: typechecking is still disabled in CI for now

Closes #56
2024-05-22 21:59:53 +10:00
Alyssa Coghlan
defc103aae
CI config cleanup, fix author name
* Update pre-commit hooks
* Drop 3.6 from CI matrix (no longer available on GH actions)
* Ensure 3.10 pass actually runs on Python 3.10
* Disable typechecking CI until it works again
* Update old references to my birth name
2024-05-22 21:18:09 +10:00
Nick Coghlan
7e2a0a900a Fix references to me 2024-05-22 21:13:35 +10:00
Nick Coghlan
7c6b72683c Try to make the RTD build happy 2024-05-22 21:13:18 +10:00
Nick Coghlan
4be95fed29 Temporarily turn off typechecking test 2024-05-22 21:09:07 +10:00
Nick Coghlan
690922cfde Fix 3.10 testing 2024-05-22 20:54:41 +10:00
Nick Coghlan
c127f51df1 Also fix GitHub config 2024-05-22 20:23:08 +10:00
Nick Coghlan
241de07097 Can't test on 3.6 anymore 2024-05-22 20:20:46 +10:00
Nick Coghlan
ff30b3a68f Update pre-commit hooks 2024-05-22 20:08:13 +10:00
Hugo van Kemenade
9ef8594837
Merge pull request #49 from jazzband/add-pre-commit 2022-03-24 17:54:18 +02:00
Hugo van Kemenade
89893bba60
Merge pull request #48 from jazzband/all-repos_autofix_all-repos-sed 2022-03-24 17:53:37 +02:00
Hugo van Kemenade
26ad9aef80
Test Python 3.10 final 2021-11-10 22:59:22 +02:00
Hugo van Kemenade
c1d9e6a4a7 Add basic pre-commit config 2021-11-10 22:37:03 +02:00
Hugo van Kemenade
d63f68b104 CI: Replace deprecated pypy3 with pypy-3.8
pypy3 is deprecated and is not available in newer images:
https://github.com/actions/setup-python/issues/244#issuecomment-925966022

Instead explicitly specify the version:
https://github.com/actions/setup-python#specifying-a-pypy-version

Committed via https://github.com/asottile/all-repos
2021-11-10 22:14:24 +02:00
Jannis Leidel
0828b5a332
Merge pull request #47 from jazzband/jazzband/sync/default
Jazzband: Synced file(s) with jazzband/.github
2021-10-22 17:46:45 +02:00
jazzband-bot
1ea4e4dfe0 Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' 2021-10-21 14:33:39 +00:00
Thomas Grainger
3feaef8f91
Merge pull request #45 from jazzband/typeshed-resync 2021-08-13 11:00:17 +01:00
Sviatoslav Sydorenko
89c5189648
Merge pull request #46 from ppentchev/roam-universal-wheels
Do not build universal wheels.
2021-07-26 23:50:56 +03:00
Peter Pentchev
5373afc4ba Do not build universal wheels.
Only Python 3.x is supported, so universal wheels will only confuse
tools that examine the PyPI metadata about supported Python versions.
2021-07-24 17:08:22 +03:00
Thomas Grainger
3d2540dda1
Update contextlib2/_typeshed.py 2021-07-20 17:38:40 +01:00
Thomas Grainger
d78420a808
Update contextlib2/_typeshed.py 2021-07-20 17:38:32 +01:00
Thomas Grainger
a35252eefd
backport _typeshed.Self 2021-07-20 14:11:42 +01:00
Thomas Grainger
16db3dcf86
resync from typeshed 2021-07-16 08:33:19 +01:00
Nick Coghlan
2dc4c04492
Merge pull request #40 from graingert/widen-supports-aclose
sync pyi from typeshed
2021-06-30 22:24:55 +10:00
Thomas Grainger
908e2da622
add py3.10 async contextlib.nullcontext (pyi)
Fixes #41
2021-06-29 19:15:55 +01:00
Thomas Grainger
c907a9975e
widen _SupportsAclose
Fixes #39
2021-06-29 09:55:17 +01:00
36 changed files with 2495 additions and 748 deletions

View file

@ -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
View file

@ -11,3 +11,7 @@ MANIFEST
.coverage
coverage.xml
htmlcov/
# Patching output files
*.orig
*.rej

14
.pre-commit-config.yaml Normal file
View 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
View 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
View 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/

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -1 +1 @@
21.6.0
24.6.0rc1

View file

@ -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__()

View file

@ -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: ...

View file

@ -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__

View file

@ -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

View file

@ -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
--------------------

View file

@ -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()

View 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()

View 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]):

View 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:

View 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)

View 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
View 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
View 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
View 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: ...

View file

@ -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)}

View file

@ -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:

View file

@ -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
View file

@ -0,0 +1 @@
sphinx-rtd-theme

View file

@ -1,2 +0,0 @@
[bdist_wheel]
universal=1

View file

@ -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
View file

@ -0,0 +1 @@
test_contextlib uses this folder for chdir tests

View file

@ -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
View 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)

View file

@ -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()

View file

@ -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

View file

@ -0,0 +1 @@
test_contextlib uses this folder for chdir tests

11
tox.ini
View file

@ -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