mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-03-20 15:40:27 +00:00
Compare commits
No commits in common. "master" and "0.5.0" have entirely different histories.
43 changed files with 2008 additions and 6099 deletions
|
|
@ -1,6 +0,0 @@
|
||||||
[run]
|
|
||||||
source = contextlib2
|
|
||||||
branch = 1
|
|
||||||
|
|
||||||
[report]
|
|
||||||
omit = *test*
|
|
||||||
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
|
|
@ -1,53 +0,0 @@
|
||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: github.repository == 'jazzband/contextlib2'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
|
|
||||||
- name: Get pip cache dir
|
|
||||||
id: pip-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(pip cache dir)"
|
|
||||||
|
|
||||||
- name: Cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
|
||||||
key: release-${{ hashFiles('**/tox.ini') }}
|
|
||||||
restore-keys: |
|
|
||||||
release-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install -U pip
|
|
||||||
python -m pip install -U setuptools twine wheel
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: |
|
|
||||||
python setup.py --version
|
|
||||||
python setup.py sdist --format=gztar bdist_wheel
|
|
||||||
twine check dist/*
|
|
||||||
|
|
||||||
- name: Upload packages to Jazzband
|
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
|
||||||
with:
|
|
||||||
user: jazzband
|
|
||||||
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
|
|
||||||
repository_url: https://jazzband.co/projects/contextlib2/upload
|
|
||||||
56
.github/workflows/test.yml
vendored
56
.github/workflows/test.yml
vendored
|
|
@ -1,56 +0,0 @@
|
||||||
name: Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
max-parallel: 5
|
|
||||||
matrix:
|
|
||||||
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@v4
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Get pip cache dir
|
|
||||||
id: pip-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(pip cache dir)"
|
|
||||||
|
|
||||||
- name: Cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
|
||||||
key:
|
|
||||||
${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ matrix.os }}-${{ matrix.python-version }}-v1-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install --upgrade tox tox-gh-actions
|
|
||||||
|
|
||||||
- name: Tox tests
|
|
||||||
run: |
|
|
||||||
tox -v
|
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v1
|
|
||||||
with:
|
|
||||||
name: Python ${{ matrix.python-version }}
|
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
|
|
@ -1,17 +0,0 @@
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
docs/_build/
|
|
||||||
pip.egg-info/
|
|
||||||
MANIFEST
|
|
||||||
.tox
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
*.py[cod]
|
|
||||||
*~
|
|
||||||
.coverage
|
|
||||||
coverage.xml
|
|
||||||
htmlcov/
|
|
||||||
|
|
||||||
# Patching output files
|
|
||||||
*.orig
|
|
||||||
*.rej
|
|
||||||
18
.hgignore
Normal file
18
.hgignore
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
syntax: glob
|
||||||
|
*.swp
|
||||||
|
*.kate-swp
|
||||||
|
*.o
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.cover
|
||||||
|
*~
|
||||||
|
.tox/
|
||||||
|
__pycache__
|
||||||
|
.coverage
|
||||||
|
coverage/
|
||||||
|
htmlcov/
|
||||||
|
_build/
|
||||||
|
dist/
|
||||||
|
MANIFEST
|
||||||
|
.tox
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
# 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/
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
[](https://jazzband.co/)
|
|
||||||
|
|
||||||
This is a [Jazzband](https://jazzband.co/) project. By contributing you agree
|
|
||||||
to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct)
|
|
||||||
and follow the [guidelines](https://jazzband.co/about/guidelines).
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
Note: The type hints included in this package come from the typeshed project,
|
|
||||||
and are hence distributed under the Apache License 2.0 rather than under the
|
|
||||||
Python Software License that covers the module implementation and test suite.
|
|
||||||
|
|
||||||
A. HISTORY OF THE SOFTWARE
|
A. HISTORY OF THE SOFTWARE
|
||||||
==========================
|
==========================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in
|
include *.py *.txt *.rst *.md
|
||||||
recursive-include contextlib2 *.py *.pyi py.typed
|
|
||||||
recursive-include docs *.rst *.py make.bat Makefile
|
recursive-include docs *.rst *.py make.bat Makefile
|
||||||
recursive-include test *.py
|
|
||||||
recursive-include dev *.patch *.allowlist *.sh
|
|
||||||
|
|
|
||||||
177
NEWS.rst
177
NEWS.rst
|
|
@ -1,148 +1,8 @@
|
||||||
Release History
|
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)
|
|
||||||
^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* License update: due to the inclusion of type hints from the ``typeshed``
|
|
||||||
project, the ``contextlib2`` project is now under a combination of the
|
|
||||||
Python Software License (existing license) and the Apache License 2.0
|
|
||||||
(``typeshed`` license)
|
|
||||||
* Switched to calendar based versioning using a "year"-"month"-"serial" scheme,
|
|
||||||
rather than continuing with pre-1.0 semantic versioning
|
|
||||||
* Due to the inclusion of asynchronous features from Python 3.7+, the
|
|
||||||
minimum supported Python version is now Python 3.6
|
|
||||||
(`#29 <https://github.com/jazzband/contextlib2/issues/29>`__)
|
|
||||||
* Synchronised with the Python 3.10 version of contextlib
|
|
||||||
(`#12 <https://github.com/jazzband/contextlib2/issues/12>`__), making the
|
|
||||||
following new features available on Python 3.6+:
|
|
||||||
|
|
||||||
* ``asyncontextmanager`` (added in Python 3.7, enhanced in Python 3.10)
|
|
||||||
* ``aclosing`` (added in Python 3.10)
|
|
||||||
* ``AbstractAsyncContextManager`` (added in Python 3.7)
|
|
||||||
* ``AsyncContextDecorator`` (added in Python 3.10)
|
|
||||||
* ``AsyncExitStack`` (added in Python 3.7)
|
|
||||||
* async support in ``nullcontext`` (Python 3.10)
|
|
||||||
|
|
||||||
* ``contextlib2`` now includes an adapted copy of the ``contextlib``
|
|
||||||
type hints from ``typeshed`` (the adaptation removes the Python version
|
|
||||||
dependencies from the API definition)
|
|
||||||
(`#33 <https://github.com/jazzband/contextlib2/issues/33>`__)
|
|
||||||
* to incorporate the type hints stub file and the ``py.typed`` marker file,
|
|
||||||
``contextlib2`` is now installed as a package rather than as a module
|
|
||||||
* Updates to the default compatibility testing matrix:
|
|
||||||
|
|
||||||
* Added: CPython 3.9, CPython 3.10
|
|
||||||
* Dropped: CPython 2.7, CPython 3.5, PyPy2
|
|
||||||
|
|
||||||
0.6.0.post1 (2019-10-10)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* Issue `#24 <https://github.com/jazzband/contextlib2/issues/24>`__:
|
|
||||||
Correctly update NEWS.rst for the 0.6.0 release.
|
|
||||||
|
|
||||||
0.6.0 (2019-09-21)
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* Issue `#16 <https://github.com/jazzband/contextlib2/issues/16>`__:
|
|
||||||
Backport `AbstractContextManager` from Python 3.6 and `nullcontext`
|
|
||||||
from Python 3.7 (patch by John Vandenberg)
|
|
||||||
|
|
||||||
0.5.5 (2017-04-25)
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* Issue `#13 <https://github.com/jazzband/contextlib2/issues/13>`__:
|
|
||||||
``setup.py`` now falls back to plain ``distutils`` if ``setuptools`` is not
|
|
||||||
available (patch by Allan Harwood)
|
|
||||||
|
|
||||||
* Updates to the default compatibility testing matrix:
|
|
||||||
|
|
||||||
* Added: PyPy3, CPython 3.6 (maintenance), CPython 3.7 (development)
|
|
||||||
* Dropped: CPython 3.3
|
|
||||||
|
|
||||||
0.5.4 (2016-07-31)
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* Thanks to the welcome efforts of Jannis Leidel, contextlib2 is now a
|
|
||||||
[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.
|
|
||||||
|
|
||||||
* Issue `#7 <https://github.com/jazzband/contextlib2/issues/7>`__: Backported
|
|
||||||
fix for CPython issue `#27122 <http://bugs.python.org/issue27122>`__,
|
|
||||||
preventing a potential infinite loop on Python 3.5 when handling
|
|
||||||
``RuntimeError`` (CPython updates by Gregory P. Smith & Serhiy Storchaka)
|
|
||||||
|
|
||||||
|
|
||||||
0.5.3 (2016-05-02)
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* ``ExitStack`` now correctly handles context managers implemented as old-style
|
|
||||||
classes in Python 2.x (such as ``codecs.StreamReader`` and
|
|
||||||
``codecs.StreamWriter``)
|
|
||||||
|
|
||||||
* ``setup.py`` has been migrated to setuptools and configured to emit a
|
|
||||||
universal wheel file by default
|
|
||||||
|
|
||||||
0.5.2 (2016-05-02)
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* development migrated from BitBucket to GitHub
|
|
||||||
|
|
||||||
* ``redirect_stream``, ``redirect_stdout``, ``redirect_stderr`` and ``suppress``
|
|
||||||
now explicitly inherit from ``object``, ensuring compatibility with
|
|
||||||
``ExitStack`` when run under Python 2.x (patch contributed by Devin
|
|
||||||
Jeanpierre).
|
|
||||||
|
|
||||||
* ``MANIFEST.in`` is now included in the published sdist, ensuring the archive
|
|
||||||
can be precisely recreated even without access to the original source repo
|
|
||||||
(patch contributed by Guy Rozendorn)
|
|
||||||
|
|
||||||
|
|
||||||
0.5.1 (2016-01-13)
|
|
||||||
^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
* Python 2.6 compatilibity restored (patch contributed by Armin Ronacher)
|
|
||||||
|
|
||||||
* README converted back to reStructured Text formatting
|
|
||||||
|
|
||||||
|
|
||||||
0.5.0 (2016-01-12)
|
0.5.0 (2016-01-12)
|
||||||
^^^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Updated to include all features from the Python 3.4 and 3.5 releases of
|
* Updated to include all features from the Python 3.4 and 3.5 releases of
|
||||||
contextlib (also includes some ``ExitStack`` enhancements made following
|
contextlib (also includes some ``ExitStack`` enhancements made following
|
||||||
|
|
@ -153,36 +13,31 @@ Release History
|
||||||
|
|
||||||
* Python 2.6, 3.2 and 3.3 have been dropped from compatibility testing
|
* Python 2.6, 3.2 and 3.3 have been dropped from compatibility testing
|
||||||
|
|
||||||
* tox is now supported for local version compatibility testing (patch by
|
|
||||||
Marc Abramowitz)
|
|
||||||
|
|
||||||
|
|
||||||
0.4.0 (2012-05-05)
|
0.4.0 (2012-05-05)
|
||||||
^^^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* (BitBucket) Issue #8: Replace ContextStack with ExitStack (old ContextStack
|
|
||||||
API retained for backwards compatibility)
|
|
||||||
|
|
||||||
|
* Issue #8: Replace ContextStack with ExitStack (old ContextStack API
|
||||||
|
retained for backwards compatibility)
|
||||||
* Fall back to unittest2 if unittest is missing required functionality
|
* Fall back to unittest2 if unittest is missing required functionality
|
||||||
|
|
||||||
|
|
||||||
0.3.1 (2012-01-17)
|
0.3.1 (2012-01-17)
|
||||||
^^^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* (BitBucket) Issue #7: Add MANIFEST.in so PyPI package contains all relevant
|
* Issue #7: Add MANIFEST.in so PyPI package contains all relevant files
|
||||||
files (patch contributed by Doug Latornell)
|
|
||||||
|
|
||||||
|
|
||||||
0.3 (2012-01-04)
|
0.3 (2012-01-04)
|
||||||
^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* (BitBucket) Issue #5: ContextStack.register no longer pointlessly returns the
|
* Issue #5: ContextStack.register no longer pointlessly returns the wrapped
|
||||||
wrapped function
|
function
|
||||||
* (BitBucket) Issue #2: Add examples and recipes section to docs
|
* Issue #2: Add examples and recipes section to docs
|
||||||
* (BitBucket) Issue #3: ContextStack.register_exit() now accepts objects with
|
* Issue #3: ContextStack.register_exit() now accepts objects with __exit__
|
||||||
__exit__ attributes in addition to accepting exit callbacks directly
|
attributes in addition to accepting exit callbacks directly
|
||||||
* (BitBucket) Issue #1: Add ContextStack.preserve() to move all registered
|
* Issue #1: Add ContextStack.preserve() to move all registered callbacks to
|
||||||
callbacks to a new ContextStack object
|
a new ContextStack object
|
||||||
* Wrapped callbacks now expose __wrapped__ (for direct callbacks) or __self__
|
* Wrapped callbacks now expose __wrapped__ (for direct callbacks) or __self__
|
||||||
(for context manager methods) attributes to aid in introspection
|
(for context manager methods) attributes to aid in introspection
|
||||||
* Moved version number to a VERSION.txt file (read by both docs and setup.py)
|
* Moved version number to a VERSION.txt file (read by both docs and setup.py)
|
||||||
|
|
@ -190,14 +45,14 @@ Release History
|
||||||
|
|
||||||
|
|
||||||
0.2 (2011-12-15)
|
0.2 (2011-12-15)
|
||||||
^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Renamed CleanupManager to ContextStack (hopefully before anyone started
|
* Renamed CleanupManager to ContextStack (hopefully before anyone started
|
||||||
using the module for anything, since I didn't alias the old name at all)
|
using the module for anything, since I didn't alias the old name at all)
|
||||||
|
|
||||||
|
|
||||||
0.1 (2011-12-13)
|
0.1 (2011-12-13)
|
||||||
^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Initial release as a backport module
|
* Initial release as a backport module
|
||||||
* Added CleanupManager (based on a `Python feature request`_)
|
* Added CleanupManager (based on a `Python feature request`_)
|
||||||
|
|
|
||||||
40
README.md
Normal file
40
README.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
contextlib2 is a backport of the [standard library's contextlib
|
||||||
|
module](https://docs.python.org/3.5/library/contextlib.html) to
|
||||||
|
earlier Python versions.
|
||||||
|
|
||||||
|
It also serves as a real world proving ground for possible future
|
||||||
|
enhancements to the standard library version.
|
||||||
|
|
||||||
|
Development
|
||||||
|
-----------
|
||||||
|
|
||||||
|
contextlib2 currently has no dependencies.
|
||||||
|
|
||||||
|
Local testing is currently just a matter of running `python test_contextlib2.py`.
|
||||||
|
|
||||||
|
You can test against multiple versions of Python with [tox](http://tox.testrun.org/)):
|
||||||
|
|
||||||
|
pip install tox
|
||||||
|
tox
|
||||||
|
|
||||||
|
Versions currently tested in tox are:
|
||||||
|
|
||||||
|
* CPython 2.7 (also tested in Codeship)
|
||||||
|
* CPython 3.4 (also tested in Codeship)
|
||||||
|
* CPython 3.5
|
||||||
|
* PyPy
|
||||||
|
* PyPy3
|
||||||
|
|
||||||
|
To install all the relevant runtimes on Fedora 23:
|
||||||
|
|
||||||
|
sudo dnf install python python3 pypy pypy3
|
||||||
|
sudo dnf copr enable -y mstuchli/Python3.5
|
||||||
|
sudo dnf install python35-python3
|
||||||
|
|
||||||
|
Continuous integration
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
CI is set up in Codeship to run against PRs and commits.
|
||||||
|
|
||||||
|
[](https://codeship.com/projects/102388)
|
||||||
|
[](https://codecov.io/bitbucket/ncoghlan/contextlib2?branch=default)
|
||||||
102
README.rst
102
README.rst
|
|
@ -1,102 +0,0 @@
|
||||||
.. image:: https://jazzband.co/static/img/badge.svg
|
|
||||||
:target: https://jazzband.co/
|
|
||||||
:alt: Jazzband
|
|
||||||
|
|
||||||
.. image:: https://github.com/jazzband/contextlib2/workflows/Test/badge.svg
|
|
||||||
:target: https://github.com/jazzband/contextlib2/actions
|
|
||||||
:alt: Tests
|
|
||||||
|
|
||||||
.. image:: https://codecov.io/gh/jazzband/contextlib2/branch/master/graph/badge.svg
|
|
||||||
:target: https://codecov.io/gh/jazzband/contextlib2
|
|
||||||
:alt: Coverage
|
|
||||||
|
|
||||||
.. image:: https://readthedocs.org/projects/contextlib2/badge/?version=latest
|
|
||||||
:target: https://contextlib2.readthedocs.org/
|
|
||||||
:alt: Latest Docs
|
|
||||||
|
|
||||||
contextlib2 is a backport of the `standard library's contextlib
|
|
||||||
module <https://docs.python.org/3/library/contextlib.html>`_ to
|
|
||||||
earlier Python versions.
|
|
||||||
|
|
||||||
It also sometimes serves as a real world proving ground for possible future
|
|
||||||
enhancements to the standard library version.
|
|
||||||
|
|
||||||
Licensing
|
|
||||||
---------
|
|
||||||
|
|
||||||
As a backport of Python standard library software, the implementation, test
|
|
||||||
suite and other supporting files for this project are distributed under the
|
|
||||||
Python Software License used for the CPython reference implementation.
|
|
||||||
|
|
||||||
The one exception is the included type hints file, which comes from the
|
|
||||||
``typeshed`` project, and is hence distributed under the Apache License 2.0.
|
|
||||||
|
|
||||||
Development
|
|
||||||
-----------
|
|
||||||
|
|
||||||
``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::
|
|
||||||
|
|
||||||
python3 -m unittest discover -t . -s test
|
|
||||||
|
|
||||||
You can test against multiple versions of Python with
|
|
||||||
`tox <https://tox.testrun.org/>`_::
|
|
||||||
|
|
||||||
pip install tox
|
|
||||||
tox
|
|
||||||
|
|
||||||
Versions currently tested in both tox and GitHub Actions are:
|
|
||||||
|
|
||||||
* CPython 3.8
|
|
||||||
* CPython 3.9
|
|
||||||
* CPython 3.10
|
|
||||||
* CPython 3.11
|
|
||||||
* CPython 3.12
|
|
||||||
* PyPy3 (specifically 3.10 in GitHub Actions)
|
|
||||||
|
|
||||||
Updating to a new stdlib reference version
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
As of Python 3.12.3, 4 files needed to be copied from the CPython reference
|
|
||||||
implementation to contextlib2:
|
|
||||||
|
|
||||||
* ``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``
|
|
||||||
|
|
||||||
The corresponding version of ``contextlib2/__init__.pyi`` also needs to be
|
|
||||||
retrieved from the ``typeshed`` project::
|
|
||||||
|
|
||||||
wget https://raw.githubusercontent.com/python/typeshed/master/stdlib/contextlib.pyi
|
|
||||||
|
|
||||||
The following patch files are saved in the ``dev`` directory:
|
|
||||||
|
|
||||||
* 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 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 to ``docs/contextlib2.rst`` to use ``contextlib2`` version
|
|
||||||
numbers in the version added/changed notes and to integrate the module
|
|
||||||
documentation with the rest of the project documentation
|
|
||||||
|
|
||||||
When the upstream changes between releases are minor, these patch files may be
|
|
||||||
used directly to reapply the ``contextlib2`` specific changes after syncing a
|
|
||||||
new version. Even when the patches do not apply cleanly, they're still a useful
|
|
||||||
guide as to the changes that are needed to restore compatibility with older
|
|
||||||
Python versions and make any other ``contextlib2`` specific updates.
|
|
||||||
|
|
||||||
The test directory is laid out so that the test suite's imports from
|
|
||||||
``test.support`` work the same way as they do in the main CPython test suite.
|
|
||||||
These files are selective copies rather than complete ones as the ``contextlib``
|
|
||||||
tests only need a tiny fraction of the features available in the real
|
|
||||||
``test.support`` module.
|
|
||||||
|
|
||||||
The ``dev/sync_from_cpython.sh`` and ``dev/save_diff_snapshot.sh`` scripts
|
|
||||||
automate some of the steps in the sync process.
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
24.6.0rc1
|
0.5.0
|
||||||
|
|
|
||||||
419
contextlib2.py
Normal file
419
contextlib2.py
Normal file
|
|
@ -0,0 +1,419 @@
|
||||||
|
"""contextlib2 - backports and enhancements to the contextlib module"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from collections import deque
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
|
||||||
|
"redirect_stdout", "redirect_stderr", "suppress"]
|
||||||
|
|
||||||
|
# Backwards compatibility
|
||||||
|
__all__ += ["ContextStack"]
|
||||||
|
|
||||||
|
class ContextDecorator(object):
|
||||||
|
"A base class or mixin that enables context managers to work as decorators."
|
||||||
|
|
||||||
|
def refresh_cm(self):
|
||||||
|
"""Returns the context manager used to actually wrap the call to the
|
||||||
|
decorated function.
|
||||||
|
|
||||||
|
The default implementation just returns *self*.
|
||||||
|
|
||||||
|
Overriding this method allows otherwise one-shot context managers
|
||||||
|
like _GeneratorContextManager to support use as decorators via
|
||||||
|
implicit recreation.
|
||||||
|
|
||||||
|
DEPRECATED: refresh_cm was never added to the standard library's
|
||||||
|
ContextDecorator API
|
||||||
|
"""
|
||||||
|
warnings.warn("refresh_cm was never added to the standard library",
|
||||||
|
DeprecationWarning)
|
||||||
|
return self._recreate_cm()
|
||||||
|
|
||||||
|
def _recreate_cm(self):
|
||||||
|
"""Return a recreated instance of self.
|
||||||
|
|
||||||
|
Allows an otherwise one-shot context manager like
|
||||||
|
_GeneratorContextManager to support use as
|
||||||
|
a decorator via implicit recreation.
|
||||||
|
|
||||||
|
This is a private interface just for _GeneratorContextManager.
|
||||||
|
See issue #11647 for details.
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __call__(self, func):
|
||||||
|
@wraps(func)
|
||||||
|
def inner(*args, **kwds):
|
||||||
|
with self._recreate_cm():
|
||||||
|
return func(*args, **kwds)
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
class _GeneratorContextManager(ContextDecorator):
|
||||||
|
"""Helper for @contextmanager decorator."""
|
||||||
|
|
||||||
|
def __init__(self, func, args, kwds):
|
||||||
|
self.gen = func(*args, **kwds)
|
||||||
|
self.func, self.args, self.kwds = func, args, kwds
|
||||||
|
# Issue 19330: ensure context manager instances have good docstrings
|
||||||
|
doc = getattr(func, "__doc__", None)
|
||||||
|
if doc is None:
|
||||||
|
doc = type(self).__doc__
|
||||||
|
self.__doc__ = doc
|
||||||
|
# Unfortunately, this still doesn't provide good help output when
|
||||||
|
# inspecting the created context manager instances, since pydoc
|
||||||
|
# currently bypasses the instance docstring and shows the docstring
|
||||||
|
# for the class instead.
|
||||||
|
# See http://bugs.python.org/issue19404 for more details.
|
||||||
|
|
||||||
|
def _recreate_cm(self):
|
||||||
|
# _GCM instances are one-shot context managers, so the
|
||||||
|
# CM must be recreated each time a decorated function is
|
||||||
|
# called
|
||||||
|
return self.__class__(self.func, self.args, self.kwds)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
return next(self.gen)
|
||||||
|
except StopIteration:
|
||||||
|
raise RuntimeError("generator didn't yield")
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
if type is None:
|
||||||
|
try:
|
||||||
|
next(self.gen)
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
raise RuntimeError("generator didn't stop")
|
||||||
|
else:
|
||||||
|
if value is None:
|
||||||
|
# Need to force instantiation so we can reliably
|
||||||
|
# tell if we get the same exception back
|
||||||
|
value = type()
|
||||||
|
try:
|
||||||
|
self.gen.throw(type, value, traceback)
|
||||||
|
raise RuntimeError("generator didn't stop after throw()")
|
||||||
|
except StopIteration as exc:
|
||||||
|
# Suppress StopIteration *unless* it's the same exception that
|
||||||
|
# was passed to throw(). This prevents a StopIteration
|
||||||
|
# raised inside the "with" statement from being suppressed.
|
||||||
|
return exc is not value
|
||||||
|
except RuntimeError as exc:
|
||||||
|
# Likewise, avoid suppressing if a StopIteration exception
|
||||||
|
# was passed to throw() and later wrapped into a RuntimeError
|
||||||
|
# (see PEP 479).
|
||||||
|
if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value:
|
||||||
|
return False
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
# only re-raise if it's *not* the exception that was
|
||||||
|
# passed to throw(), because __exit__() must not raise
|
||||||
|
# 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 sys.exc_info()[1] is not value:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def contextmanager(func):
|
||||||
|
"""@contextmanager decorator.
|
||||||
|
|
||||||
|
Typical usage:
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def some_generator(<arguments>):
|
||||||
|
<setup>
|
||||||
|
try:
|
||||||
|
yield <value>
|
||||||
|
finally:
|
||||||
|
<cleanup>
|
||||||
|
|
||||||
|
This makes this:
|
||||||
|
|
||||||
|
with some_generator(<arguments>) as <variable>:
|
||||||
|
<body>
|
||||||
|
|
||||||
|
equivalent to this:
|
||||||
|
|
||||||
|
<setup>
|
||||||
|
try:
|
||||||
|
<variable> = <value>
|
||||||
|
<body>
|
||||||
|
finally:
|
||||||
|
<cleanup>
|
||||||
|
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def helper(*args, **kwds):
|
||||||
|
return _GeneratorContextManager(func, args, kwds)
|
||||||
|
return helper
|
||||||
|
|
||||||
|
|
||||||
|
class closing(object):
|
||||||
|
"""Context to automatically close something at the end of a block.
|
||||||
|
|
||||||
|
Code like this:
|
||||||
|
|
||||||
|
with closing(<module>.open(<arguments>)) as f:
|
||||||
|
<block>
|
||||||
|
|
||||||
|
is equivalent to this:
|
||||||
|
|
||||||
|
f = <module>.open(<arguments>)
|
||||||
|
try:
|
||||||
|
<block>
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, thing):
|
||||||
|
self.thing = thing
|
||||||
|
def __enter__(self):
|
||||||
|
return self.thing
|
||||||
|
def __exit__(self, *exc_info):
|
||||||
|
self.thing.close()
|
||||||
|
|
||||||
|
|
||||||
|
class _RedirectStream:
|
||||||
|
|
||||||
|
_stream = None
|
||||||
|
|
||||||
|
def __init__(self, new_target):
|
||||||
|
self._new_target = new_target
|
||||||
|
# We use a list of old targets to make this CM re-entrant
|
||||||
|
self._old_targets = []
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._old_targets.append(getattr(sys, self._stream))
|
||||||
|
setattr(sys, self._stream, self._new_target)
|
||||||
|
return self._new_target
|
||||||
|
|
||||||
|
def __exit__(self, exctype, excinst, exctb):
|
||||||
|
setattr(sys, self._stream, self._old_targets.pop())
|
||||||
|
|
||||||
|
|
||||||
|
class redirect_stdout(_RedirectStream):
|
||||||
|
"""Context manager for temporarily redirecting stdout to another file.
|
||||||
|
|
||||||
|
# How to send help() to stderr
|
||||||
|
with redirect_stdout(sys.stderr):
|
||||||
|
help(dir)
|
||||||
|
|
||||||
|
# How to write help() to a file
|
||||||
|
with open('help.txt', 'w') as f:
|
||||||
|
with redirect_stdout(f):
|
||||||
|
help(pow)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_stream = "stdout"
|
||||||
|
|
||||||
|
|
||||||
|
class redirect_stderr(_RedirectStream):
|
||||||
|
"""Context manager for temporarily redirecting stderr to another file."""
|
||||||
|
|
||||||
|
_stream = "stderr"
|
||||||
|
|
||||||
|
|
||||||
|
class suppress:
|
||||||
|
"""Context manager to suppress specified exceptions
|
||||||
|
|
||||||
|
After the exception is suppressed, execution proceeds with the next
|
||||||
|
statement following the with statement.
|
||||||
|
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.remove(somefile)
|
||||||
|
# Execution still resumes here if the file was already removed
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *exceptions):
|
||||||
|
self._exceptions = exceptions
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, exctype, excinst, exctb):
|
||||||
|
# Unlike isinstance and issubclass, CPython exception handling
|
||||||
|
# currently only looks at the concrete type hierarchy (ignoring
|
||||||
|
# the instance and subclass checking hooks). While Guido considers
|
||||||
|
# that a bug rather than a feature, it's a fairly hard one to fix
|
||||||
|
# due to various internal implementation details. suppress provides
|
||||||
|
# the simpler issubclass based semantics, rather than trying to
|
||||||
|
# exactly reproduce the limitations of the CPython interpreter.
|
||||||
|
#
|
||||||
|
# See http://bugs.python.org/issue12029 for more details
|
||||||
|
return exctype is not None and issubclass(exctype, self._exceptions)
|
||||||
|
|
||||||
|
|
||||||
|
# Context manipulation is Python 3 only
|
||||||
|
_HAVE_EXCEPTION_CHAINING = sys.version_info.major >= 3
|
||||||
|
if _HAVE_EXCEPTION_CHAINING:
|
||||||
|
def _make_context_fixer(frame_exc):
|
||||||
|
def _fix_exception_context(new_exc, old_exc):
|
||||||
|
# Context may not be correct, so find the end of the chain
|
||||||
|
while 1:
|
||||||
|
exc_context = new_exc.__context__
|
||||||
|
if exc_context is old_exc:
|
||||||
|
# Context is already set correctly (see issue 20317)
|
||||||
|
return
|
||||||
|
if exc_context is None or exc_context is frame_exc:
|
||||||
|
break
|
||||||
|
new_exc = exc_context
|
||||||
|
# Change the end of the chain to point to the exception
|
||||||
|
# we expect it to reference
|
||||||
|
new_exc.__context__ = old_exc
|
||||||
|
return _fix_exception_context
|
||||||
|
|
||||||
|
def _reraise_with_existing_context(exc_details):
|
||||||
|
try:
|
||||||
|
# bare "raise exc_details[1]" replaces our carefully
|
||||||
|
# set-up context
|
||||||
|
fixed_ctx = exc_details[1].__context__
|
||||||
|
raise exc_details[1]
|
||||||
|
except BaseException:
|
||||||
|
exc_details[1].__context__ = fixed_ctx
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# No exception context in Python 2
|
||||||
|
def _make_context_fixer(frame_exc):
|
||||||
|
return lambda new_exc, old_exc: None
|
||||||
|
|
||||||
|
# Use 3 argument raise in Python 2,
|
||||||
|
# but use exec to avoid SyntaxError in Python 3
|
||||||
|
def _reraise_with_existing_context(exc_details):
|
||||||
|
exc_type, exc_value, exc_tb = exc_details
|
||||||
|
exec ("raise exc_type, exc_value, exc_tb")
|
||||||
|
|
||||||
|
# Inspired by discussions on http://bugs.python.org/issue13585
|
||||||
|
class ExitStack(object):
|
||||||
|
"""Context manager for dynamic management of a stack of exit callbacks
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
files = [stack.enter_context(open(fname)) for fname in filenames]
|
||||||
|
# All opened files will automatically be closed at the end of
|
||||||
|
# the with statement, even if attempts to open files later
|
||||||
|
# in the list raise an exception
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._exit_callbacks = deque()
|
||||||
|
|
||||||
|
def pop_all(self):
|
||||||
|
"""Preserve the context stack by transferring it to a new instance"""
|
||||||
|
new_stack = type(self)()
|
||||||
|
new_stack._exit_callbacks = self._exit_callbacks
|
||||||
|
self._exit_callbacks = deque()
|
||||||
|
return new_stack
|
||||||
|
|
||||||
|
def _push_cm_exit(self, cm, cm_exit):
|
||||||
|
"""Helper to correctly register callbacks to __exit__ methods"""
|
||||||
|
def _exit_wrapper(*exc_details):
|
||||||
|
return cm_exit(cm, *exc_details)
|
||||||
|
_exit_wrapper.__self__ = cm
|
||||||
|
self.push(_exit_wrapper)
|
||||||
|
|
||||||
|
def push(self, exit):
|
||||||
|
"""Registers a callback with the standard __exit__ method signature
|
||||||
|
|
||||||
|
Can suppress exceptions the same way __exit__ methods can.
|
||||||
|
|
||||||
|
Also accepts any object with an __exit__ method (registering a call
|
||||||
|
to the method instead of the object itself)
|
||||||
|
"""
|
||||||
|
# We use an unbound method rather than a bound method to follow
|
||||||
|
# the standard lookup behaviour for special methods
|
||||||
|
_cb_type = type(exit)
|
||||||
|
try:
|
||||||
|
exit_method = _cb_type.__exit__
|
||||||
|
except AttributeError:
|
||||||
|
# Not a context manager, so assume its a callable
|
||||||
|
self._exit_callbacks.append(exit)
|
||||||
|
else:
|
||||||
|
self._push_cm_exit(exit, exit_method)
|
||||||
|
return exit # Allow use as a decorator
|
||||||
|
|
||||||
|
def callback(self, callback, *args, **kwds):
|
||||||
|
"""Registers an arbitrary callback and arguments.
|
||||||
|
|
||||||
|
Cannot suppress exceptions.
|
||||||
|
"""
|
||||||
|
def _exit_wrapper(exc_type, exc, tb):
|
||||||
|
callback(*args, **kwds)
|
||||||
|
# We changed the signature, so using @wraps is not appropriate, but
|
||||||
|
# setting __wrapped__ may still help with introspection
|
||||||
|
_exit_wrapper.__wrapped__ = callback
|
||||||
|
self.push(_exit_wrapper)
|
||||||
|
return callback # Allow use as a decorator
|
||||||
|
|
||||||
|
def enter_context(self, cm):
|
||||||
|
"""Enters the supplied context manager
|
||||||
|
|
||||||
|
If successful, also pushes its __exit__ method as a callback and
|
||||||
|
returns the result of the __enter__ method.
|
||||||
|
"""
|
||||||
|
# We look up the special methods on the type to match the with statement
|
||||||
|
_cm_type = type(cm)
|
||||||
|
_exit = _cm_type.__exit__
|
||||||
|
result = _cm_type.__enter__(cm)
|
||||||
|
self._push_cm_exit(cm, _exit)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Immediately unwind the context stack"""
|
||||||
|
self.__exit__(None, None, None)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
received_exc = exc_details[0] is not None
|
||||||
|
|
||||||
|
# We manipulate the exception state so it behaves as though
|
||||||
|
# we were actually nesting multiple with statements
|
||||||
|
frame_exc = sys.exc_info()[1]
|
||||||
|
_fix_exception_context = _make_context_fixer(frame_exc)
|
||||||
|
|
||||||
|
# Callbacks are invoked in LIFO order to match the behaviour of
|
||||||
|
# nested context managers
|
||||||
|
suppressed_exc = False
|
||||||
|
pending_raise = False
|
||||||
|
while self._exit_callbacks:
|
||||||
|
cb = self._exit_callbacks.pop()
|
||||||
|
try:
|
||||||
|
if cb(*exc_details):
|
||||||
|
suppressed_exc = True
|
||||||
|
pending_raise = False
|
||||||
|
exc_details = (None, None, None)
|
||||||
|
except:
|
||||||
|
new_exc_details = sys.exc_info()
|
||||||
|
# simulate the stack of exceptions by setting the context
|
||||||
|
_fix_exception_context(new_exc_details[1], exc_details[1])
|
||||||
|
pending_raise = True
|
||||||
|
exc_details = new_exc_details
|
||||||
|
if pending_raise:
|
||||||
|
_reraise_with_existing_context(exc_details)
|
||||||
|
return received_exc and suppressed_exc
|
||||||
|
|
||||||
|
# Preserve backwards compatibility
|
||||||
|
class ContextStack(ExitStack):
|
||||||
|
"""Backwards compatibility alias for ExitStack"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
warnings.warn("ContextStack has been renamed to ExitStack",
|
||||||
|
DeprecationWarning)
|
||||||
|
super(ContextStack, self).__init__()
|
||||||
|
|
||||||
|
def register_exit(self, callback):
|
||||||
|
return self.push(callback)
|
||||||
|
|
||||||
|
def register(self, callback, *args, **kwds):
|
||||||
|
return self.callback(callback, *args, **kwds)
|
||||||
|
|
||||||
|
def preserve(self):
|
||||||
|
return self.pop_all()
|
||||||
|
|
@ -1,876 +0,0 @@
|
||||||
"""Utilities for with-statement contexts. See PEP 343."""
|
|
||||||
import abc
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import _collections_abc
|
|
||||||
from collections import deque
|
|
||||||
from functools import wraps
|
|
||||||
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",
|
|
||||||
"AsyncExitStack", "ContextDecorator", "ExitStack",
|
|
||||||
"redirect_stdout", "redirect_stderr", "suppress", "aclosing",
|
|
||||||
"chdir"]
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractContextManager(abc.ABC):
|
|
||||||
|
|
||||||
"""An abstract base class for context managers."""
|
|
||||||
|
|
||||||
__class_getitem__ = classmethod(GenericAlias)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
"""Return `self` upon entering the runtime context."""
|
|
||||||
return self
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
|
||||||
"""Raise any exception triggered within the runtime context."""
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __subclasshook__(cls, C):
|
|
||||||
if cls is AbstractContextManager:
|
|
||||||
return _collections_abc._check_methods(C, "__enter__", "__exit__")
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractAsyncContextManager(abc.ABC):
|
|
||||||
|
|
||||||
"""An abstract base class for asynchronous context managers."""
|
|
||||||
|
|
||||||
__class_getitem__ = classmethod(GenericAlias)
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
"""Return `self` upon entering the runtime context."""
|
|
||||||
return self
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
||||||
"""Raise any exception triggered within the runtime context."""
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __subclasshook__(cls, C):
|
|
||||||
if cls is AbstractAsyncContextManager:
|
|
||||||
return _collections_abc._check_methods(C, "__aenter__",
|
|
||||||
"__aexit__")
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
|
|
||||||
class ContextDecorator(object):
|
|
||||||
"A base class or mixin that enables context managers to work as decorators."
|
|
||||||
|
|
||||||
def refresh_cm(self):
|
|
||||||
"""Returns the context manager used to actually wrap the call to the
|
|
||||||
decorated function.
|
|
||||||
|
|
||||||
The default implementation just returns *self*.
|
|
||||||
|
|
||||||
Overriding this method allows otherwise one-shot context managers
|
|
||||||
like _GeneratorContextManager to support use as decorators via
|
|
||||||
implicit recreation.
|
|
||||||
|
|
||||||
DEPRECATED: refresh_cm was never added to the standard library's
|
|
||||||
ContextDecorator API
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
|
|
||||||
Allows an otherwise one-shot context manager like
|
|
||||||
_GeneratorContextManager to support use as
|
|
||||||
a decorator via implicit recreation.
|
|
||||||
|
|
||||||
This is a private interface just for _GeneratorContextManager.
|
|
||||||
See issue #11647 for details.
|
|
||||||
"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
@wraps(func)
|
|
||||||
def inner(*args, **kwds):
|
|
||||||
with self._recreate_cm():
|
|
||||||
return func(*args, **kwds)
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncContextDecorator(object):
|
|
||||||
"A base class or mixin that enables async context managers to work as decorators."
|
|
||||||
|
|
||||||
def _recreate_cm(self):
|
|
||||||
"""Return a recreated instance of self.
|
|
||||||
"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
@wraps(func)
|
|
||||||
async def inner(*args, **kwds):
|
|
||||||
async with self._recreate_cm():
|
|
||||||
return await func(*args, **kwds)
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
class _GeneratorContextManagerBase:
|
|
||||||
"""Shared functionality for @contextmanager and @asynccontextmanager."""
|
|
||||||
|
|
||||||
def __init__(self, func, args, kwds):
|
|
||||||
self.gen = func(*args, **kwds)
|
|
||||||
self.func, self.args, self.kwds = func, args, kwds
|
|
||||||
# Issue 19330: ensure context manager instances have good docstrings
|
|
||||||
doc = getattr(func, "__doc__", None)
|
|
||||||
if doc is None:
|
|
||||||
doc = type(self).__doc__
|
|
||||||
self.__doc__ = doc
|
|
||||||
# Unfortunately, this still doesn't provide good help output when
|
|
||||||
# inspecting the created context manager instances, since pydoc
|
|
||||||
# currently bypasses the instance docstring and shows the docstring
|
|
||||||
# for the class instead.
|
|
||||||
# See http://bugs.python.org/issue19404 for more details.
|
|
||||||
|
|
||||||
def _recreate_cm(self):
|
|
||||||
# _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
|
|
||||||
del self.args, self.kwds, self.func
|
|
||||||
try:
|
|
||||||
return next(self.gen)
|
|
||||||
except StopIteration:
|
|
||||||
raise RuntimeError("generator didn't yield") from None
|
|
||||||
|
|
||||||
def __exit__(self, typ, value, traceback):
|
|
||||||
if typ is None:
|
|
||||||
try:
|
|
||||||
next(self.gen)
|
|
||||||
except StopIteration:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
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 = typ()
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
# 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 StopIteration exception
|
|
||||||
# was passed to throw() and later wrapped into a RuntimeError
|
|
||||||
# (see PEP 479 for sync generators; async generators also
|
|
||||||
# have this behavior). But do this only if the exception wrapped
|
|
||||||
# by the RuntimeError is actually Stop(Async)Iteration (see
|
|
||||||
# issue29692).
|
|
||||||
if (
|
|
||||||
isinstance(value, StopIteration)
|
|
||||||
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):
|
|
||||||
"""@contextmanager decorator.
|
|
||||||
|
|
||||||
Typical usage:
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def some_generator(<arguments>):
|
|
||||||
<setup>
|
|
||||||
try:
|
|
||||||
yield <value>
|
|
||||||
finally:
|
|
||||||
<cleanup>
|
|
||||||
|
|
||||||
This makes this:
|
|
||||||
|
|
||||||
with some_generator(<arguments>) as <variable>:
|
|
||||||
<body>
|
|
||||||
|
|
||||||
equivalent to this:
|
|
||||||
|
|
||||||
<setup>
|
|
||||||
try:
|
|
||||||
<variable> = <value>
|
|
||||||
<body>
|
|
||||||
finally:
|
|
||||||
<cleanup>
|
|
||||||
"""
|
|
||||||
@wraps(func)
|
|
||||||
def helper(*args, **kwds):
|
|
||||||
return _GeneratorContextManager(func, args, kwds)
|
|
||||||
return helper
|
|
||||||
|
|
||||||
|
|
||||||
def asynccontextmanager(func):
|
|
||||||
"""@asynccontextmanager decorator.
|
|
||||||
|
|
||||||
Typical usage:
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def some_async_generator(<arguments>):
|
|
||||||
<setup>
|
|
||||||
try:
|
|
||||||
yield <value>
|
|
||||||
finally:
|
|
||||||
<cleanup>
|
|
||||||
|
|
||||||
This makes this:
|
|
||||||
|
|
||||||
async with some_async_generator(<arguments>) as <variable>:
|
|
||||||
<body>
|
|
||||||
|
|
||||||
equivalent to this:
|
|
||||||
|
|
||||||
<setup>
|
|
||||||
try:
|
|
||||||
<variable> = <value>
|
|
||||||
<body>
|
|
||||||
finally:
|
|
||||||
<cleanup>
|
|
||||||
"""
|
|
||||||
@wraps(func)
|
|
||||||
def helper(*args, **kwds):
|
|
||||||
return _AsyncGeneratorContextManager(func, args, kwds)
|
|
||||||
return helper
|
|
||||||
|
|
||||||
|
|
||||||
class closing(AbstractContextManager):
|
|
||||||
"""Context to automatically close something at the end of a block.
|
|
||||||
|
|
||||||
Code like this:
|
|
||||||
|
|
||||||
with closing(<module>.open(<arguments>)) as f:
|
|
||||||
<block>
|
|
||||||
|
|
||||||
is equivalent to this:
|
|
||||||
|
|
||||||
f = <module>.open(<arguments>)
|
|
||||||
try:
|
|
||||||
<block>
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, thing):
|
|
||||||
self.thing = thing
|
|
||||||
def __enter__(self):
|
|
||||||
return self.thing
|
|
||||||
def __exit__(self, *exc_info):
|
|
||||||
self.thing.close()
|
|
||||||
|
|
||||||
|
|
||||||
class aclosing(AbstractAsyncContextManager):
|
|
||||||
"""Async context manager for safely finalizing an asynchronously cleaned-up
|
|
||||||
resource such as an async generator, calling its ``aclose()`` method.
|
|
||||||
|
|
||||||
Code like this:
|
|
||||||
|
|
||||||
async with aclosing(<module>.fetch(<arguments>)) as agen:
|
|
||||||
<block>
|
|
||||||
|
|
||||||
is equivalent to this:
|
|
||||||
|
|
||||||
agen = <module>.fetch(<arguments>)
|
|
||||||
try:
|
|
||||||
<block>
|
|
||||||
finally:
|
|
||||||
await agen.aclose()
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, thing):
|
|
||||||
self.thing = thing
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self.thing
|
|
||||||
async def __aexit__(self, *exc_info):
|
|
||||||
await self.thing.aclose()
|
|
||||||
|
|
||||||
|
|
||||||
class _RedirectStream(AbstractContextManager):
|
|
||||||
|
|
||||||
_stream = None
|
|
||||||
|
|
||||||
def __init__(self, new_target):
|
|
||||||
self._new_target = new_target
|
|
||||||
# We use a list of old targets to make this CM re-entrant
|
|
||||||
self._old_targets = []
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self._old_targets.append(getattr(sys, self._stream))
|
|
||||||
setattr(sys, self._stream, self._new_target)
|
|
||||||
return self._new_target
|
|
||||||
|
|
||||||
def __exit__(self, exctype, excinst, exctb):
|
|
||||||
setattr(sys, self._stream, self._old_targets.pop())
|
|
||||||
|
|
||||||
|
|
||||||
class redirect_stdout(_RedirectStream):
|
|
||||||
"""Context manager for temporarily redirecting stdout to another file.
|
|
||||||
|
|
||||||
# How to send help() to stderr
|
|
||||||
with redirect_stdout(sys.stderr):
|
|
||||||
help(dir)
|
|
||||||
|
|
||||||
# How to write help() to a file
|
|
||||||
with open('help.txt', 'w') as f:
|
|
||||||
with redirect_stdout(f):
|
|
||||||
help(pow)
|
|
||||||
"""
|
|
||||||
|
|
||||||
_stream = "stdout"
|
|
||||||
|
|
||||||
|
|
||||||
class redirect_stderr(_RedirectStream):
|
|
||||||
"""Context manager for temporarily redirecting stderr to another file."""
|
|
||||||
|
|
||||||
_stream = "stderr"
|
|
||||||
|
|
||||||
|
|
||||||
class suppress(AbstractContextManager):
|
|
||||||
"""Context manager to suppress specified exceptions
|
|
||||||
|
|
||||||
After the exception is suppressed, execution proceeds with the next
|
|
||||||
statement following the with statement.
|
|
||||||
|
|
||||||
with suppress(FileNotFoundError):
|
|
||||||
os.remove(somefile)
|
|
||||||
# Execution still resumes here if the file was already removed
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *exceptions):
|
|
||||||
self._exceptions = exceptions
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __exit__(self, exctype, excinst, exctb):
|
|
||||||
# Unlike isinstance and issubclass, CPython exception handling
|
|
||||||
# currently only looks at the concrete type hierarchy (ignoring
|
|
||||||
# the instance and subclass checking hooks). While Guido considers
|
|
||||||
# that a bug rather than a feature, it's a fairly hard one to fix
|
|
||||||
# due to various internal implementation details. suppress provides
|
|
||||||
# the simpler issubclass based semantics, rather than trying to
|
|
||||||
# exactly reproduce the limitations of the CPython interpreter.
|
|
||||||
#
|
|
||||||
# See http://bugs.python.org/issue12029 for more details
|
|
||||||
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:
|
|
||||||
"""A base class for ExitStack and AsyncExitStack."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_exit_wrapper(cm, cm_exit):
|
|
||||||
return MethodType(cm_exit, cm)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_cb_wrapper(callback, /, *args, **kwds):
|
|
||||||
def _exit_wrapper(exc_type, exc, tb):
|
|
||||||
callback(*args, **kwds)
|
|
||||||
return _exit_wrapper
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._exit_callbacks = deque()
|
|
||||||
|
|
||||||
def pop_all(self):
|
|
||||||
"""Preserve the context stack by transferring it to a new instance."""
|
|
||||||
new_stack = type(self)()
|
|
||||||
new_stack._exit_callbacks = self._exit_callbacks
|
|
||||||
self._exit_callbacks = deque()
|
|
||||||
return new_stack
|
|
||||||
|
|
||||||
def push(self, exit):
|
|
||||||
"""Registers a callback with the standard __exit__ method signature.
|
|
||||||
|
|
||||||
Can suppress exceptions the same way __exit__ method can.
|
|
||||||
Also accepts any object with an __exit__ method (registering a call
|
|
||||||
to the method instead of the object itself).
|
|
||||||
"""
|
|
||||||
# We use an unbound method rather than a bound method to follow
|
|
||||||
# the standard lookup behaviour for special methods.
|
|
||||||
_cb_type = type(exit)
|
|
||||||
|
|
||||||
try:
|
|
||||||
exit_method = _cb_type.__exit__
|
|
||||||
except AttributeError:
|
|
||||||
# Not a context manager, so assume it's a callable.
|
|
||||||
self._push_exit_callback(exit)
|
|
||||||
else:
|
|
||||||
self._push_cm_exit(exit, exit_method)
|
|
||||||
return exit # Allow use as a decorator.
|
|
||||||
|
|
||||||
def enter_context(self, cm):
|
|
||||||
"""Enters the supplied context manager.
|
|
||||||
|
|
||||||
If successful, also pushes its __exit__ method as a callback and
|
|
||||||
returns the result of the __enter__ method.
|
|
||||||
"""
|
|
||||||
# We look up the special methods on the type to match the with
|
|
||||||
# statement.
|
|
||||||
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(self, callback, /, *args, **kwds):
|
|
||||||
"""Registers an arbitrary callback and arguments.
|
|
||||||
|
|
||||||
Cannot suppress exceptions.
|
|
||||||
"""
|
|
||||||
_exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds)
|
|
||||||
|
|
||||||
# We changed the signature, so using @wraps is not appropriate, but
|
|
||||||
# setting __wrapped__ may still help with introspection.
|
|
||||||
_exit_wrapper.__wrapped__ = callback
|
|
||||||
self._push_exit_callback(_exit_wrapper)
|
|
||||||
return callback # Allow use as a decorator
|
|
||||||
|
|
||||||
def _push_cm_exit(self, cm, cm_exit):
|
|
||||||
"""Helper to correctly register callbacks to __exit__ methods."""
|
|
||||||
_exit_wrapper = self._create_exit_wrapper(cm, cm_exit)
|
|
||||||
self._push_exit_callback(_exit_wrapper, True)
|
|
||||||
|
|
||||||
def _push_exit_callback(self, callback, is_sync=True):
|
|
||||||
self._exit_callbacks.append((is_sync, callback))
|
|
||||||
|
|
||||||
|
|
||||||
# Inspired by discussions on http://bugs.python.org/issue13585
|
|
||||||
class ExitStack(_BaseExitStack, AbstractContextManager):
|
|
||||||
"""Context manager for dynamic management of a stack of exit callbacks.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
with ExitStack() as stack:
|
|
||||||
files = [stack.enter_context(open(fname)) for fname in filenames]
|
|
||||||
# All opened files will automatically be closed at the end of
|
|
||||||
# the with statement, even if attempts to open files later
|
|
||||||
# in the list raise an exception.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *exc_details):
|
|
||||||
received_exc = exc_details[0] is not None
|
|
||||||
|
|
||||||
# We manipulate the exception state so it behaves as though
|
|
||||||
# we were actually nesting multiple with statements
|
|
||||||
frame_exc = sys.exc_info()[1]
|
|
||||||
def _fix_exception_context(new_exc, old_exc):
|
|
||||||
# Context may not be correct, so find the end of the chain
|
|
||||||
while 1:
|
|
||||||
exc_context = new_exc.__context__
|
|
||||||
if exc_context is None or exc_context is old_exc:
|
|
||||||
# Context is already set correctly (see issue 20317)
|
|
||||||
return
|
|
||||||
if exc_context is frame_exc:
|
|
||||||
break
|
|
||||||
new_exc = exc_context
|
|
||||||
# Change the end of the chain to point to the exception
|
|
||||||
# we expect it to reference
|
|
||||||
new_exc.__context__ = old_exc
|
|
||||||
|
|
||||||
# Callbacks are invoked in LIFO order to match the behaviour of
|
|
||||||
# nested context managers
|
|
||||||
suppressed_exc = False
|
|
||||||
pending_raise = False
|
|
||||||
while self._exit_callbacks:
|
|
||||||
is_sync, cb = self._exit_callbacks.pop()
|
|
||||||
assert is_sync
|
|
||||||
try:
|
|
||||||
if cb(*exc_details):
|
|
||||||
suppressed_exc = True
|
|
||||||
pending_raise = False
|
|
||||||
exc_details = (None, None, None)
|
|
||||||
except:
|
|
||||||
new_exc_details = sys.exc_info()
|
|
||||||
# simulate the stack of exceptions by setting the context
|
|
||||||
_fix_exception_context(new_exc_details[1], exc_details[1])
|
|
||||||
pending_raise = True
|
|
||||||
exc_details = new_exc_details
|
|
||||||
if pending_raise:
|
|
||||||
try:
|
|
||||||
# bare "raise exc_details[1]" replaces our carefully
|
|
||||||
# set-up context
|
|
||||||
fixed_ctx = exc_details[1].__context__
|
|
||||||
raise exc_details[1]
|
|
||||||
except BaseException:
|
|
||||||
exc_details[1].__context__ = fixed_ctx
|
|
||||||
raise
|
|
||||||
return received_exc and suppressed_exc
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Immediately unwind the context stack."""
|
|
||||||
self.__exit__(None, None, None)
|
|
||||||
|
|
||||||
|
|
||||||
# Inspired by discussions on https://bugs.python.org/issue29302
|
|
||||||
class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager):
|
|
||||||
"""Async context manager for dynamic management of a stack of exit
|
|
||||||
callbacks.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
async with AsyncExitStack() as stack:
|
|
||||||
connections = [await stack.enter_async_context(get_connection())
|
|
||||||
for i in range(5)]
|
|
||||||
# All opened connections will automatically be released at the
|
|
||||||
# end of the async with statement, even if attempts to open a
|
|
||||||
# connection later in the list raise an exception.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_async_exit_wrapper(cm, cm_exit):
|
|
||||||
return MethodType(cm_exit, cm)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _create_async_cb_wrapper(callback, /, *args, **kwds):
|
|
||||||
async def _exit_wrapper(exc_type, exc, tb):
|
|
||||||
await callback(*args, **kwds)
|
|
||||||
return _exit_wrapper
|
|
||||||
|
|
||||||
async def enter_async_context(self, cm):
|
|
||||||
"""Enters the supplied async context manager.
|
|
||||||
|
|
||||||
If successful, also pushes its __aexit__ method as a callback and
|
|
||||||
returns the result of the __aenter__ method.
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
def push_async_exit(self, exit):
|
|
||||||
"""Registers a coroutine function with the standard __aexit__ method
|
|
||||||
signature.
|
|
||||||
|
|
||||||
Can suppress exceptions the same way __aexit__ method can.
|
|
||||||
Also accepts any object with an __aexit__ method (registering a call
|
|
||||||
to the method instead of the object itself).
|
|
||||||
"""
|
|
||||||
_cb_type = type(exit)
|
|
||||||
try:
|
|
||||||
exit_method = _cb_type.__aexit__
|
|
||||||
except AttributeError:
|
|
||||||
# Not an async context manager, so assume it's a coroutine function
|
|
||||||
self._push_exit_callback(exit, False)
|
|
||||||
else:
|
|
||||||
self._push_async_cm_exit(exit, exit_method)
|
|
||||||
return exit # Allow use as a decorator
|
|
||||||
|
|
||||||
def push_async_callback(self, callback, /, *args, **kwds):
|
|
||||||
"""Registers an arbitrary coroutine function and arguments.
|
|
||||||
|
|
||||||
Cannot suppress exceptions.
|
|
||||||
"""
|
|
||||||
_exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds)
|
|
||||||
|
|
||||||
# We changed the signature, so using @wraps is not appropriate, but
|
|
||||||
# setting __wrapped__ may still help with introspection.
|
|
||||||
_exit_wrapper.__wrapped__ = callback
|
|
||||||
self._push_exit_callback(_exit_wrapper, False)
|
|
||||||
return callback # Allow use as a decorator
|
|
||||||
|
|
||||||
async def aclose(self):
|
|
||||||
"""Immediately unwind the context stack."""
|
|
||||||
await self.__aexit__(None, None, None)
|
|
||||||
|
|
||||||
def _push_async_cm_exit(self, cm, cm_exit):
|
|
||||||
"""Helper to correctly register coroutine function to __aexit__
|
|
||||||
method."""
|
|
||||||
_exit_wrapper = self._create_async_exit_wrapper(cm, cm_exit)
|
|
||||||
self._push_exit_callback(_exit_wrapper, False)
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, *exc_details):
|
|
||||||
received_exc = exc_details[0] is not None
|
|
||||||
|
|
||||||
# We manipulate the exception state so it behaves as though
|
|
||||||
# we were actually nesting multiple with statements
|
|
||||||
frame_exc = sys.exc_info()[1]
|
|
||||||
def _fix_exception_context(new_exc, old_exc):
|
|
||||||
# Context may not be correct, so find the end of the chain
|
|
||||||
while 1:
|
|
||||||
exc_context = new_exc.__context__
|
|
||||||
if exc_context is None or exc_context is old_exc:
|
|
||||||
# Context is already set correctly (see issue 20317)
|
|
||||||
return
|
|
||||||
if exc_context is frame_exc:
|
|
||||||
break
|
|
||||||
new_exc = exc_context
|
|
||||||
# Change the end of the chain to point to the exception
|
|
||||||
# we expect it to reference
|
|
||||||
new_exc.__context__ = old_exc
|
|
||||||
|
|
||||||
# Callbacks are invoked in LIFO order to match the behaviour of
|
|
||||||
# nested context managers
|
|
||||||
suppressed_exc = False
|
|
||||||
pending_raise = False
|
|
||||||
while self._exit_callbacks:
|
|
||||||
is_sync, cb = self._exit_callbacks.pop()
|
|
||||||
try:
|
|
||||||
if is_sync:
|
|
||||||
cb_suppress = cb(*exc_details)
|
|
||||||
else:
|
|
||||||
cb_suppress = await cb(*exc_details)
|
|
||||||
|
|
||||||
if cb_suppress:
|
|
||||||
suppressed_exc = True
|
|
||||||
pending_raise = False
|
|
||||||
exc_details = (None, None, None)
|
|
||||||
except:
|
|
||||||
new_exc_details = sys.exc_info()
|
|
||||||
# simulate the stack of exceptions by setting the context
|
|
||||||
_fix_exception_context(new_exc_details[1], exc_details[1])
|
|
||||||
pending_raise = True
|
|
||||||
exc_details = new_exc_details
|
|
||||||
if pending_raise:
|
|
||||||
try:
|
|
||||||
# bare "raise exc_details[1]" replaces our carefully
|
|
||||||
# set-up context
|
|
||||||
fixed_ctx = exc_details[1].__context__
|
|
||||||
raise exc_details[1]
|
|
||||||
except BaseException:
|
|
||||||
exc_details[1].__context__ = fixed_ctx
|
|
||||||
raise
|
|
||||||
return received_exc and suppressed_exc
|
|
||||||
|
|
||||||
|
|
||||||
class nullcontext(AbstractContextManager, AbstractAsyncContextManager):
|
|
||||||
"""Context manager that does no additional processing.
|
|
||||||
|
|
||||||
Used as a stand-in for a normal context manager, when a particular
|
|
||||||
block of code is only sometimes used with a normal context manager:
|
|
||||||
|
|
||||||
cm = optional_cm if condition else nullcontext()
|
|
||||||
with cm:
|
|
||||||
# Perform operation, using optional_cm if condition is True
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, enter_result=None):
|
|
||||||
self.enter_result = enter_result
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self.enter_result
|
|
||||||
|
|
||||||
def __exit__(self, *excinfo):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self.enter_result
|
|
||||||
|
|
||||||
async def __aexit__(self, *excinfo):
|
|
||||||
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):
|
|
||||||
"""(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()
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
# 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
|
|
||||||
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:
|
|
||||||
__all__ += ["aclosing"]
|
|
||||||
|
|
||||||
if True:
|
|
||||||
__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 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:
|
|
||||||
_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(AbstractContextManager[_SupportsCloseT, None]):
|
|
||||||
def __init__(self, thing: _SupportsCloseT) -> None: ...
|
|
||||||
def __exit__(self, *exc_info: Unused) -> None: ...
|
|
||||||
|
|
||||||
if True:
|
|
||||||
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 True:
|
|
||||||
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: ...
|
|
||||||
|
|
||||||
if True:
|
|
||||||
_T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath)
|
|
||||||
|
|
||||||
class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]):
|
|
||||||
path: _T_fd_or_any_path
|
|
||||||
def __init__(self, path: _T_fd_or_any_path) -> None: ...
|
|
||||||
def __enter__(self) -> None: ...
|
|
||||||
def __exit__(self, *excinfo: Unused) -> None: ...
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# Deprecated APIs that never graduated to the standard library
|
|
||||||
contextlib2.ContextDecorator.refresh_cm
|
|
||||||
|
|
||||||
# stubcheck no longer complains about this one for some reason
|
|
||||||
# (but it does complain about the unused allowlist entry)
|
|
||||||
# contextlib2.ContextStack
|
|
||||||
|
|
||||||
# mypy seems to be confused by the GenericAlias compatibility hack
|
|
||||||
contextlib2.AbstractAsyncContextManager.__class_getitem__
|
|
||||||
contextlib2.AbstractContextManager.__class_getitem__
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
--- /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()
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
--- /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]):
|
|
||||||
|
|
@ -1,453 +0,0 @@
|
||||||
--- /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:
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
--- /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)
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
--- /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",
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
#!/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"
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
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: ...
|
|
||||||
21
docs/conf.py
21
docs/conf.py
|
|
@ -11,6 +11,8 @@
|
||||||
# All configuration values have a default; values that are commented out
|
# All configuration values have a default; values that are commented out
|
||||||
# serve to show the default.
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# 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
|
# 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.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
|
@ -23,10 +25,7 @@
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
extensions = [
|
extensions = ['sphinx.ext.intersphinx']
|
||||||
'sphinx.ext.intersphinx',
|
|
||||||
'sphinx_rtd_theme',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
@ -42,7 +41,7 @@ master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'contextlib2'
|
project = u'contextlib2'
|
||||||
copyright = u'2024, Alyssa Coghlan'
|
copyright = u'2011, Nick Coghlan'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
|
@ -93,7 +92,7 @@ pygments_style = 'sphinx'
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = 'default'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a 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
|
# further. For a list of options available for each theme, see the
|
||||||
|
|
@ -122,7 +121,7 @@ html_theme = 'sphinx_rtd_theme'
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = []
|
html_static_path = ['_static']
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
# using the given strftime format.
|
# using the given strftime format.
|
||||||
|
|
@ -181,7 +180,7 @@ htmlhelp_basename = 'contextlib2doc'
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('index', 'contextlib2.tex', u'contextlib2 Documentation',
|
('index', 'contextlib2.tex', u'contextlib2 Documentation',
|
||||||
u'Alyssa Coghlan', 'manual'),
|
u'Nick Coghlan', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
|
@ -214,9 +213,9 @@ latex_documents = [
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
('index', 'contextlib2', u'contextlib2 Documentation',
|
('index', 'contextlib2', u'contextlib2 Documentation',
|
||||||
[u'Alyssa Coghlan'], 1)
|
[u'Nick Coghlan'], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Configuration for intersphinx: refer to the Python 3 standard library.
|
# Example configuration for intersphinx: refer to the Python 3 standard library.
|
||||||
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
intersphinx_mapping = {'http://docs.python.org/py3k': None}
|
||||||
|
|
|
||||||
1033
docs/contextlib2.rst
1033
docs/contextlib2.rst
File diff suppressed because it is too large
Load diff
665
docs/index.rst
665
docs/index.rst
|
|
@ -15,32 +15,659 @@ also serves as a real world proving ground for potential future enhancements
|
||||||
to that module.
|
to that module.
|
||||||
|
|
||||||
Like :mod:`contextlib`, this module provides utilities for common tasks
|
Like :mod:`contextlib`, this module provides utilities for common tasks
|
||||||
involving the ``with`` and ``async with`` statements.
|
involving the ``with`` statement.
|
||||||
|
|
||||||
|
|
||||||
Additions Relative to the Standard Library
|
Additions Relative to the Standard Library
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
This module is primarily a backport of the Python 3.12.3 version of
|
This module is primarily a backport of the Python 3.5 version of
|
||||||
:mod:`contextlib` to earlier releases. (Note: as of the start of the Python 3.13
|
:mod:`contextlib` to earlier releases. However, it is also a proving ground
|
||||||
beta release cycle, there have been no subsequent changes to ``contextlib``)
|
for new features not yet part of the standard library.
|
||||||
|
|
||||||
The module makes use of positional-only argument syntax in several call
|
There are currently no such features in the module.
|
||||||
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
|
Refer to the :mod:`contextlib` documentation for details of which
|
||||||
of the standard library. There are currently no such features in the module.
|
versions of Python 3 introduce the various APIs provided in this module.
|
||||||
|
|
||||||
Finally, this module contains some deprecated APIs which never graduated to
|
|
||||||
standard library inclusion. These interfaces are no longer documented, but may
|
|
||||||
still be present in the code (emitting ``DeprecationWarning`` if used).
|
|
||||||
|
|
||||||
|
|
||||||
Using the Module
|
API Reference
|
||||||
|
=============
|
||||||
|
|
||||||
|
Functions and classes provided:
|
||||||
|
|
||||||
|
.. decorator:: contextmanager
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
A simple example (this is not recommended as a real way of generating HTML!)::
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def tag(name):
|
||||||
|
print("<%s>" % name)
|
||||||
|
yield
|
||||||
|
print("</%s>" % name)
|
||||||
|
|
||||||
|
>>> with tag("h1"):
|
||||||
|
... print("foo")
|
||||||
|
...
|
||||||
|
<h1>
|
||||||
|
foo
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
The function being decorated must return a :term:`generator`-iterator when
|
||||||
|
called. This iterator must yield exactly one value, which will be bound to
|
||||||
|
the targets in the :keyword:`with` statement's :keyword:`as` clause, if any.
|
||||||
|
|
||||||
|
At the point where the generator yields, the block nested in the :keyword:`with`
|
||||||
|
statement is executed. The generator is then resumed after the block is exited.
|
||||||
|
If an unhandled exception occurs in the block, it is reraised inside the
|
||||||
|
generator at the point where the yield occurred. Thus, you can use a
|
||||||
|
:keyword:`try`...\ :keyword:`except`...\ :keyword:`finally` statement to trap
|
||||||
|
the error (if any), or ensure that some cleanup takes place. If an exception is
|
||||||
|
trapped merely in order to log it or to perform some action (rather than to
|
||||||
|
suppress it entirely), the generator must reraise that exception. Otherwise the
|
||||||
|
generator context manager will indicate to the :keyword:`with` statement that
|
||||||
|
the exception has been handled, and execution will resume with the statement
|
||||||
|
immediately following the :keyword:`with` statement.
|
||||||
|
|
||||||
|
:func:`contextmanager` uses :class:`ContextDecorator` so the context managers
|
||||||
|
it creates can be used as decorators as well as in :keyword:`with` statements.
|
||||||
|
When used as a decorator, a new generator instance is implicitly created on
|
||||||
|
each function call (this allows the otherwise "one-shot" context managers
|
||||||
|
created by :func:`contextmanager` to meet the requirement that context
|
||||||
|
managers support multiple invocations in order to 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
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def closing(thing):
|
||||||
|
try:
|
||||||
|
yield thing
|
||||||
|
finally:
|
||||||
|
thing.close()
|
||||||
|
|
||||||
|
And lets you write code like this::
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
with closing(urlopen('http://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.
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: suppress(*exceptions)
|
||||||
|
|
||||||
|
Return a context manager that suppresses any of the specified exceptions
|
||||||
|
if they occur in the body of a with statement and then resumes execution
|
||||||
|
with the first statement following the end of the with statement.
|
||||||
|
|
||||||
|
As with any other mechanism that completely suppresses exceptions, this
|
||||||
|
context manager should be used only to cover very specific errors where
|
||||||
|
silently continuing with program execution is known to be the right
|
||||||
|
thing to do.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.remove('somefile.tmp')
|
||||||
|
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.remove('someotherfile.tmp')
|
||||||
|
|
||||||
|
This code is equivalent to::
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove('somefile.tmp')
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove('someotherfile.tmp')
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
Part of the standard library in Python 3.4 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: redirect_stdout(new_target)
|
||||||
|
|
||||||
|
Context manager for temporarily redirecting :data:`sys.stdout` to
|
||||||
|
another file or file-like object.
|
||||||
|
|
||||||
|
This tool adds flexibility to existing functions or classes whose output
|
||||||
|
is hardwired to stdout.
|
||||||
|
|
||||||
|
For example, the output of :func:`help` normally is sent to *sys.stdout*.
|
||||||
|
You can capture that output in a string by redirecting the output to a
|
||||||
|
:class:`io.StringIO` object::
|
||||||
|
|
||||||
|
f = io.StringIO()
|
||||||
|
with redirect_stdout(f):
|
||||||
|
help(pow)
|
||||||
|
s = f.getvalue()
|
||||||
|
|
||||||
|
To send the output of :func:`help` to a file on disk, redirect the output
|
||||||
|
to a regular file::
|
||||||
|
|
||||||
|
with open('help.txt', 'w') as f:
|
||||||
|
with redirect_stdout(f):
|
||||||
|
help(pow)
|
||||||
|
|
||||||
|
To send the output of :func:`help` to *sys.stderr*::
|
||||||
|
|
||||||
|
with redirect_stdout(sys.stderr):
|
||||||
|
help(pow)
|
||||||
|
|
||||||
|
Note that the global side effect on :data:`sys.stdout` means that this
|
||||||
|
context manager is not suitable for use in library code and most threaded
|
||||||
|
applications. It also has no effect on the output of subprocesses.
|
||||||
|
However, it is still a useful approach for many utility scripts.
|
||||||
|
|
||||||
|
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
Part of the standard library in Python 3.4 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: redirect_stderr(new_target)
|
||||||
|
|
||||||
|
Similar to :func:`redirect_stdout`, but redirecting :data:`sys.stderr` to
|
||||||
|
another file or file-like object.
|
||||||
|
|
||||||
|
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.5
|
||||||
|
Part of the standard library in Python 3.5 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: ContextDecorator()
|
||||||
|
|
||||||
|
A base class that enables a context manager to also be used as a decorator.
|
||||||
|
|
||||||
|
Context managers inheriting from ``ContextDecorator`` have to implement
|
||||||
|
``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional
|
||||||
|
exception handling even when used as a decorator.
|
||||||
|
|
||||||
|
``ContextDecorator`` is used by :func:`contextmanager`, so you get this
|
||||||
|
functionality automatically.
|
||||||
|
|
||||||
|
Example of ``ContextDecorator``::
|
||||||
|
|
||||||
|
from contextlib import ContextDecorator
|
||||||
|
|
||||||
|
class mycontext(ContextDecorator):
|
||||||
|
def __enter__(self):
|
||||||
|
print('Starting')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
print('Finishing')
|
||||||
|
return False
|
||||||
|
|
||||||
|
>>> @mycontext()
|
||||||
|
... def function():
|
||||||
|
... print('The bit in the middle')
|
||||||
|
...
|
||||||
|
>>> function()
|
||||||
|
Starting
|
||||||
|
The bit in the middle
|
||||||
|
Finishing
|
||||||
|
|
||||||
|
>>> with mycontext():
|
||||||
|
... print('The bit in the middle')
|
||||||
|
...
|
||||||
|
Starting
|
||||||
|
The bit in the middle
|
||||||
|
Finishing
|
||||||
|
|
||||||
|
This change is just syntactic sugar for any construct of the following form::
|
||||||
|
|
||||||
|
def f():
|
||||||
|
with cm():
|
||||||
|
# Do stuff
|
||||||
|
|
||||||
|
``ContextDecorator`` lets you instead write::
|
||||||
|
|
||||||
|
@cm()
|
||||||
|
def f():
|
||||||
|
# Do stuff
|
||||||
|
|
||||||
|
It makes it clear that the ``cm`` applies to the whole function, rather than
|
||||||
|
just a piece of it (and saving an indentation level is nice, too).
|
||||||
|
|
||||||
|
Existing context managers that already have a base class can be extended by
|
||||||
|
using ``ContextDecorator`` as a mixin class::
|
||||||
|
|
||||||
|
from contextlib import ContextDecorator
|
||||||
|
|
||||||
|
class mycontext(ContextBaseClass, ContextDecorator):
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
return False
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
As the decorated function must be able to be called multiple times, the
|
||||||
|
underlying context manager must support use in multiple :keyword:`with`
|
||||||
|
statements. If this is not the case, then the original construct with the
|
||||||
|
explicit :keyword:`with` statement inside the function should be used.
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: ExitStack()
|
||||||
|
|
||||||
|
A context manager that is designed to make it easy to programmatically
|
||||||
|
combine other context managers and cleanup functions, especially those
|
||||||
|
that are optional or otherwise driven by input data.
|
||||||
|
|
||||||
|
For example, a set of files may easily be handled in a single with
|
||||||
|
statement as follows::
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
files = [stack.enter_context(open(fname)) for fname in filenames]
|
||||||
|
# All opened files will automatically be closed at the end of
|
||||||
|
# the with statement, even if attempts to open files later
|
||||||
|
# in the list raise an exception
|
||||||
|
|
||||||
|
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*
|
||||||
|
invoked implicitly when the context stack instance is garbage collected.
|
||||||
|
|
||||||
|
This stack model is used so that context managers that acquire their
|
||||||
|
resources in their ``__init__`` method (such as file objects) can be
|
||||||
|
handled correctly.
|
||||||
|
|
||||||
|
Since registered callbacks are invoked in the reverse order of
|
||||||
|
registration, this ends up behaving as if multiple nested :keyword:`with`
|
||||||
|
statements had been used with the registered set of callbacks. This even
|
||||||
|
extends to exception handling - if an inner callback suppresses or replaces
|
||||||
|
an exception, then outer callbacks will be passed arguments based on that
|
||||||
|
updated state.
|
||||||
|
|
||||||
|
This is a relatively low level API that takes care of the details of
|
||||||
|
correctly unwinding the stack of exit callbacks. It provides a suitable
|
||||||
|
foundation for higher level context managers that manipulate the exit
|
||||||
|
stack in application specific ways.
|
||||||
|
|
||||||
|
.. versionadded:: 0.4
|
||||||
|
Part of the standard library in Python 3.3 and later
|
||||||
|
|
||||||
|
.. method:: enter_context(cm)
|
||||||
|
|
||||||
|
Enters a new context manager and adds its :meth:`__exit__` method to
|
||||||
|
the callback stack. The return value is the result of the context
|
||||||
|
manager's own :meth:`__enter__` method.
|
||||||
|
|
||||||
|
These context managers may suppress exceptions just as they normally
|
||||||
|
would if used directly as part of a :keyword:`with` statement.
|
||||||
|
|
||||||
|
.. method:: push(exit)
|
||||||
|
|
||||||
|
Adds a context manager's :meth:`__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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
By returning true values, these callbacks can suppress exceptions the
|
||||||
|
same way context manager :meth:`__exit__` methods can.
|
||||||
|
|
||||||
|
The passed in object is returned from the function, allowing this
|
||||||
|
method to be used as a function decorator.
|
||||||
|
|
||||||
|
.. method:: callback(callback, *args, **kwds)
|
||||||
|
|
||||||
|
Accepts an arbitrary callback function and arguments and adds it to
|
||||||
|
the callback stack.
|
||||||
|
|
||||||
|
Unlike the other methods, callbacks added this way cannot suppress
|
||||||
|
exceptions (as they are never passed the exception details).
|
||||||
|
|
||||||
|
The passed in callback is returned from the function, allowing this
|
||||||
|
method to be used as a function decorator.
|
||||||
|
|
||||||
|
.. method:: pop_all()
|
||||||
|
|
||||||
|
Transfers the callback stack to a fresh :class:`ExitStack` instance
|
||||||
|
and returns it. No callbacks are invoked by this operation - instead,
|
||||||
|
they will now be invoked when the new stack is closed (either
|
||||||
|
explicitly or implicitly at the end of a :keyword:`with` statement).
|
||||||
|
|
||||||
|
For example, a group of files can be opened as an "all or nothing"
|
||||||
|
operation as follows::
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
files = [stack.enter_context(open(fname)) for fname in filenames]
|
||||||
|
# Hold onto the close method, but don't call it yet.
|
||||||
|
close_files = stack.pop_all().close
|
||||||
|
# If opening any file fails, all previously opened files will be
|
||||||
|
# closed automatically. If all files are opened successfully,
|
||||||
|
# they will remain open even after the with statement ends.
|
||||||
|
# close_files() can then be invoked explicitly to close them all.
|
||||||
|
|
||||||
|
.. method:: close()
|
||||||
|
|
||||||
|
Immediately unwinds the callback stack, invoking callbacks in the
|
||||||
|
reverse order of registration. For any context managers and exit
|
||||||
|
callbacks registered, the arguments passed in will indicate that no
|
||||||
|
exception occurred.
|
||||||
|
|
||||||
|
|
||||||
|
Examples and Recipes
|
||||||
====================
|
====================
|
||||||
|
|
||||||
.. toctree::
|
This section describes some examples and recipes for making effective use of
|
||||||
contextlib2.rst
|
the tools provided by :mod:`contextlib2`. Some of them may also work with
|
||||||
|
:mod:`contextlib` in sufficiently recent versions of Python. When this is the
|
||||||
|
case, it is noted at the end of the example.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 contextlib2 import ExitStack
|
||||||
|
|
||||||
|
class ResourceManager(object):
|
||||||
|
|
||||||
|
def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
|
||||||
|
self.acquire_resource = acquire_resource
|
||||||
|
self.release_resource = release_resource
|
||||||
|
self.check_resource_ok = check_resource_ok
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
resource = self.acquire_resource()
|
||||||
|
if self.check_resource_ok is not None:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.push(self)
|
||||||
|
if not self.check_resource_ok(resource):
|
||||||
|
msg = "Failed validation for {!r}"
|
||||||
|
raise RuntimeError(msg.format(resource))
|
||||||
|
# The validation check passed and didn't raise an exception
|
||||||
|
# Accordingly, we want to keep the resource, and pass it
|
||||||
|
# back to our caller
|
||||||
|
stack.pop_all()
|
||||||
|
return resource
|
||||||
|
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
# We don't need to duplicate any of our resource release logic
|
||||||
|
self.release_resource()
|
||||||
|
|
||||||
|
This example will also work with :mod:`contextlib` in Python 3.3 or later.
|
||||||
|
|
||||||
|
|
||||||
|
Replacing any use of ``try-finally`` and flag variables
|
||||||
|
-------------------------------------------------------
|
||||||
|
|
||||||
|
A pattern you will sometimes see is a ``try-finally`` statement with a flag
|
||||||
|
variable to indicate whether or not the body of the ``finally`` clause should
|
||||||
|
be executed. In its simplest form (that can't already be handled just by
|
||||||
|
using an ``except`` clause instead), it looks something like this::
|
||||||
|
|
||||||
|
cleanup_needed = True
|
||||||
|
try:
|
||||||
|
result = perform_operation()
|
||||||
|
if result:
|
||||||
|
cleanup_needed = False
|
||||||
|
finally:
|
||||||
|
if cleanup_needed:
|
||||||
|
cleanup_resources()
|
||||||
|
|
||||||
|
As with any ``try`` statement based code, this can cause problems for
|
||||||
|
development and review, because the setup code and the cleanup code can end
|
||||||
|
up being separated by arbitrarily long sections of code.
|
||||||
|
|
||||||
|
:class:`ExitStack` makes it possible to instead register a callback for
|
||||||
|
execution at the end of a ``with`` statement, and then later decide to skip
|
||||||
|
executing that callback::
|
||||||
|
|
||||||
|
from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.callback(cleanup_resources)
|
||||||
|
result = perform_operation()
|
||||||
|
if result:
|
||||||
|
stack.pop_all()
|
||||||
|
|
||||||
|
This allows the intended cleanup up behaviour to be made explicit up front,
|
||||||
|
rather than requiring a separate flag variable.
|
||||||
|
|
||||||
|
If you find yourself using this pattern a lot, it can be simplified even
|
||||||
|
further by means of a small helper class::
|
||||||
|
|
||||||
|
from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
class Callback(ExitStack):
|
||||||
|
def __init__(self, callback, *args, **kwds):
|
||||||
|
super(Callback, self).__init__()
|
||||||
|
self.callback(callback, *args, **kwds)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.pop_all()
|
||||||
|
|
||||||
|
with Callback(cleanup_resources) as cb:
|
||||||
|
result = perform_operation()
|
||||||
|
if result:
|
||||||
|
cb.cancel()
|
||||||
|
|
||||||
|
If the resource cleanup isn't already neatly bundled into a standalone
|
||||||
|
function, then it is still possible to use the decorator form of
|
||||||
|
:meth:`ExitStack.callback` to declare the resource cleanup in
|
||||||
|
advance::
|
||||||
|
|
||||||
|
from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
@stack.callback
|
||||||
|
def cleanup_resources():
|
||||||
|
...
|
||||||
|
result = perform_operation()
|
||||||
|
if result:
|
||||||
|
stack.pop_all()
|
||||||
|
|
||||||
|
Due to the way the decorator protocol works, a callback function
|
||||||
|
declared this way cannot take any parameters. Instead, any resources to
|
||||||
|
be released must be accessed as closure variables.
|
||||||
|
|
||||||
|
This example will also work with :mod:`contextlib` in Python 3.3 or later.
|
||||||
|
|
||||||
|
|
||||||
|
Using a context manager as a function decorator
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
:class:`ContextDecorator` makes it possible to use a context manager in
|
||||||
|
both an ordinary ``with`` statement and also as a function decorator. The
|
||||||
|
:meth:`ContextDecorator.refresh_cm` method even makes it possible to use
|
||||||
|
otherwise single use context managers (such as those created by
|
||||||
|
:func:`contextmanager`) that way.
|
||||||
|
|
||||||
|
For example, it is sometimes useful to wrap functions or groups of statements
|
||||||
|
with a logger that can track the time of entry and time of exit. Rather than
|
||||||
|
writing both a function decorator and a context manager for the task,
|
||||||
|
:func:`contextmanager` provides both capabilities in a single
|
||||||
|
definition::
|
||||||
|
|
||||||
|
from contextlib2 import contextmanager
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def track_entry_and_exit(name):
|
||||||
|
logging.info('Entering: {}'.format(name))
|
||||||
|
yield
|
||||||
|
logging.info('Exiting: {}'.format(name))
|
||||||
|
|
||||||
|
This can be used as both a context manager::
|
||||||
|
|
||||||
|
with track_entry_and_exit('widget loader'):
|
||||||
|
print('Some time consuming activity goes here')
|
||||||
|
load_widget()
|
||||||
|
|
||||||
|
And also as a function decorator::
|
||||||
|
|
||||||
|
@track_entry_and_exit('widget loader')
|
||||||
|
def activity():
|
||||||
|
print('Some time consuming activity goes here')
|
||||||
|
load_widget()
|
||||||
|
|
||||||
|
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
|
||||||
|
an explicit ``with`` statement.
|
||||||
|
|
||||||
|
This example will also work with :mod:`contextlib` in Python 3.2.1 or later.
|
||||||
|
|
||||||
|
|
||||||
|
Context Management Concepts
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. _single-use-reusable-and-reentrant-cms:
|
||||||
|
|
||||||
|
Single use, reusable and reentrant context managers
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
Most context managers are written in a way that means they can only be
|
||||||
|
used effectively in a :keyword:`with` statement once. These single use
|
||||||
|
context managers must be created afresh each time they're used -
|
||||||
|
attempting to use them a second time will trigger an exception or
|
||||||
|
otherwise not work correctly.
|
||||||
|
|
||||||
|
This common limitation means that it is generally advisable to create
|
||||||
|
context managers directly in the header of the :keyword:`with` statement
|
||||||
|
where they are used (as shown in all of the usage examples above).
|
||||||
|
|
||||||
|
Files are an example of effectively single use context managers, since
|
||||||
|
the first :keyword:`with` statement will close the file, preventing any
|
||||||
|
further IO operations using that file object.
|
||||||
|
|
||||||
|
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
|
||||||
|
>>> @contextmanager
|
||||||
|
... def singleuse():
|
||||||
|
... print("Before")
|
||||||
|
... yield
|
||||||
|
... print("After")
|
||||||
|
...
|
||||||
|
>>> cm = singleuse()
|
||||||
|
>>> with cm:
|
||||||
|
... pass
|
||||||
|
...
|
||||||
|
Before
|
||||||
|
After
|
||||||
|
>>> with cm:
|
||||||
|
... pass
|
||||||
|
...
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
RuntimeError: generator didn't yield
|
||||||
|
|
||||||
|
|
||||||
|
.. _reentrant-cms:
|
||||||
|
|
||||||
|
Reentrant context managers
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
More sophisticated context managers may be "reentrant". These context
|
||||||
|
managers can not only be used in multiple :keyword:`with` statements,
|
||||||
|
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 is
|
||||||
|
:func:`suppress`. Here's a toy example of reentrant use (real world
|
||||||
|
examples of reentrancy are more likely to occur with objects like recursive
|
||||||
|
locks and are likely to be far more complicated than this example)::
|
||||||
|
|
||||||
|
>>> from contextlib import suppress
|
||||||
|
>>> ignore_raised_exception = suppress(ZeroDivisionError)
|
||||||
|
>>> with ignore_raised_exception:
|
||||||
|
... with ignore_raised_exception:
|
||||||
|
... 1/0
|
||||||
|
... print("This line runs")
|
||||||
|
... 1/0
|
||||||
|
... print("This is skipped")
|
||||||
|
...
|
||||||
|
This line runs
|
||||||
|
>>> # The second exception is also suppressed
|
||||||
|
|
||||||
|
|
||||||
|
.. _reusable-cms:
|
||||||
|
|
||||||
|
Reusable context managers
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Distinct from both single use and reentrant context managers are "reusable"
|
||||||
|
context managers (or, to be completely explicit, "reusable, but not
|
||||||
|
reentrant" context managers, since reentrant context managers are also
|
||||||
|
reusable). These context managers support being used multiple times, but
|
||||||
|
will fail (or otherwise not work correctly) if the specific context manager
|
||||||
|
instance has already been used in a containing with statement.
|
||||||
|
|
||||||
|
An example of a reusable context manager is :func:`redirect_stdout`::
|
||||||
|
|
||||||
|
>>> from contextlib import redirect_stdout
|
||||||
|
>>> from io import StringIO
|
||||||
|
>>> f = StringIO()
|
||||||
|
>>> collect_output = redirect_stdout(f)
|
||||||
|
>>> with collect_output:
|
||||||
|
... print("Collected")
|
||||||
|
...
|
||||||
|
>>> print("Not collected")
|
||||||
|
Not collected
|
||||||
|
>>> with collect_output:
|
||||||
|
... print("Also collected")
|
||||||
|
...
|
||||||
|
>>> print(f.getvalue())
|
||||||
|
Collected
|
||||||
|
Also collected
|
||||||
|
|
||||||
|
However, this context manager is not reentrant, so attempting to reuse it
|
||||||
|
within a containing with statement fails:
|
||||||
|
|
||||||
|
>>> with collect_output:
|
||||||
|
... # Nested reuse is not permitted
|
||||||
|
... with collect_output:
|
||||||
|
... pass
|
||||||
|
...
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
RuntimeError: Cannot reenter <...>
|
||||||
|
|
||||||
|
|
||||||
Obtaining the Module
|
Obtaining the Module
|
||||||
====================
|
====================
|
||||||
|
|
@ -56,7 +683,7 @@ PyPI page`_.
|
||||||
There are no operating system or distribution specific versions of this
|
There are no operating system or distribution specific versions of this
|
||||||
module - it is a pure Python module that should work on all platforms.
|
module - it is a pure Python module that should work on all platforms.
|
||||||
|
|
||||||
Supported Python versions are currently 3.8+.
|
Supported Python versions are currently 2.7 and 3.2+.
|
||||||
|
|
||||||
.. _Python Package Index: http://pypi.python.org
|
.. _Python Package Index: http://pypi.python.org
|
||||||
.. _pip: http://www.pip-installer.org
|
.. _pip: http://www.pip-installer.org
|
||||||
|
|
@ -66,11 +693,11 @@ Supported Python versions are currently 3.8+.
|
||||||
Development and Support
|
Development and Support
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
contextlib2 is developed and maintained on GitHub_. Problems and suggested
|
contextlib2 is developed and maintained on BitBucket_. Problems and suggested
|
||||||
improvements can be posted to the `issue tracker`_.
|
improvements can be posted to the `issue tracker`_.
|
||||||
|
|
||||||
.. _GitHub: https://github.com/jazzband/contextlib2
|
.. _BitBucket: https://bitbucket.org/ncoghlan/contextlib2/overview
|
||||||
.. _issue tracker: https://github.com/jazzband/contextlib2/issues
|
.. _issue tracker: https://bitbucket.org/ncoghlan/contextlib2/issues?status=new&status=open
|
||||||
|
|
||||||
|
|
||||||
.. include:: ../NEWS.rst
|
.. include:: ../NEWS.rst
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
sphinx-rtd-theme
|
|
||||||
40
setup.py
40
setup.py
|
|
@ -1,45 +1,29 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
try:
|
from distutils.core import setup
|
||||||
from setuptools import setup
|
|
||||||
except ImportError:
|
|
||||||
from distutils.core import setup
|
|
||||||
|
|
||||||
# Note: The minimum Python version requirement is set on the basis of
|
# Technically, unittest2 is a dependency to run the tests on 2.7
|
||||||
# "if it's not tested, it's broken".
|
# This file ignores that, since I don't want to depend on
|
||||||
# Specifically, if a Python version is no longer available for testing
|
# setuptools just to get "tests_require" support
|
||||||
# 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(
|
setup(
|
||||||
name='contextlib2',
|
name='contextlib2',
|
||||||
version=open('VERSION.txt').read().strip(),
|
version=open('VERSION.txt').read().strip(),
|
||||||
python_requires='>=3.7',
|
py_modules=['contextlib2'],
|
||||||
packages=['contextlib2'],
|
|
||||||
include_package_data=True,
|
|
||||||
license='PSF License',
|
license='PSF License',
|
||||||
description='Backports and enhancements for the contextlib module',
|
description='Backports and enhancements for the contextlib module',
|
||||||
long_description=open('README.rst').read(),
|
long_description=open('README.md').read(),
|
||||||
author='Alyssa Coghlan',
|
author='Nick Coghlan',
|
||||||
author_email='ncoghlan@gmail.com',
|
author_email='ncoghlan@gmail.com',
|
||||||
url='https://github.com/jazzband/contextlib2',
|
url='http://contextlib2.readthedocs.org',
|
||||||
project_urls= {
|
|
||||||
'Documentation': 'https://contextlib2.readthedocs.org',
|
|
||||||
'Source': 'https://github.com/jazzband/contextlib2.git',
|
|
||||||
'Issue Tracker': 'https://github.com/jazzband/contextlib2.git',
|
|
||||||
}
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'License :: OSI Approved :: Apache Software License',
|
|
||||||
'License :: OSI Approved :: Python Software Foundation License',
|
'License :: OSI Approved :: Python Software Foundation License',
|
||||||
# These are the Python versions tested, it may work on others
|
# 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 :: 2',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.9',
|
|
||||||
'Programming Language :: Python :: 3.10',
|
|
||||||
'Programming Language :: Python :: 3.11',
|
|
||||||
'Programming Language :: Python :: 3.12',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
# unittest test discovery requires an __init__.py file in the test directory
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
test_contextlib uses this folder for chdir tests
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
"""Enough of the test.support APIs to run the contextlib test suite"""
|
|
||||||
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)
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
"""Enough of the test.support.os_helper APIs to run the contextlib test suite"""
|
|
||||||
import os
|
|
||||||
|
|
||||||
unlink = os.unlink
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
"""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)
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,778 +0,0 @@
|
||||||
"""Unit tests for asynchronous features of contextlib2.py"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from contextlib2 import (
|
|
||||||
asynccontextmanager, AbstractAsyncContextManager,
|
|
||||||
AsyncExitStack, nullcontext, aclosing, contextmanager)
|
|
||||||
import functools
|
|
||||||
from test import support
|
|
||||||
import unittest
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
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)
|
|
||||||
asyncio.run(coro)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def tearDownModule():
|
|
||||||
asyncio.set_event_loop_policy(None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAbstractAsyncContextManager(unittest.TestCase):
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_enter(self):
|
|
||||||
class DefaultEnter(AbstractAsyncContextManager):
|
|
||||||
async def __aexit__(self, *args):
|
|
||||||
await super().__aexit__(*args)
|
|
||||||
|
|
||||||
manager = DefaultEnter()
|
|
||||||
self.assertIs(await manager.__aenter__(), manager)
|
|
||||||
|
|
||||||
async with manager as context:
|
|
||||||
self.assertIs(manager, context)
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_async_gen_propagates_generator_exit(self):
|
|
||||||
# A regression test for https://bugs.python.org/issue33786.
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def ctx():
|
|
||||||
yield
|
|
||||||
|
|
||||||
async def gen():
|
|
||||||
async with ctx():
|
|
||||||
yield 11
|
|
||||||
|
|
||||||
g = gen()
|
|
||||||
async for val in g:
|
|
||||||
self.assertEqual(val, 11)
|
|
||||||
break
|
|
||||||
await g.aclose()
|
|
||||||
|
|
||||||
def test_exit_is_abstract(self):
|
|
||||||
class MissingAexit(AbstractAsyncContextManager):
|
|
||||||
pass
|
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
MissingAexit()
|
|
||||||
|
|
||||||
def test_structural_subclassing(self):
|
|
||||||
class ManagerFromScratch:
|
|
||||||
async def __aenter__(self):
|
|
||||||
return self
|
|
||||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
||||||
return None
|
|
||||||
|
|
||||||
self.assertTrue(issubclass(ManagerFromScratch, AbstractAsyncContextManager))
|
|
||||||
|
|
||||||
class DefaultEnter(AbstractAsyncContextManager):
|
|
||||||
async def __aexit__(self, *args):
|
|
||||||
await super().__aexit__(*args)
|
|
||||||
|
|
||||||
self.assertTrue(issubclass(DefaultEnter, AbstractAsyncContextManager))
|
|
||||||
|
|
||||||
class NoneAenter(ManagerFromScratch):
|
|
||||||
__aenter__ = None
|
|
||||||
|
|
||||||
self.assertFalse(issubclass(NoneAenter, AbstractAsyncContextManager))
|
|
||||||
|
|
||||||
class NoneAexit(ManagerFromScratch):
|
|
||||||
__aexit__ = None
|
|
||||||
|
|
||||||
self.assertFalse(issubclass(NoneAexit, AbstractAsyncContextManager))
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncContextManagerTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_plain(self):
|
|
||||||
state = []
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo():
|
|
||||||
state.append(1)
|
|
||||||
yield 42
|
|
||||||
state.append(999)
|
|
||||||
async with woohoo() as x:
|
|
||||||
self.assertEqual(state, [1])
|
|
||||||
self.assertEqual(x, 42)
|
|
||||||
state.append(x)
|
|
||||||
self.assertEqual(state, [1, 42, 999])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_finally(self):
|
|
||||||
state = []
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo():
|
|
||||||
state.append(1)
|
|
||||||
try:
|
|
||||||
yield 42
|
|
||||||
finally:
|
|
||||||
state.append(999)
|
|
||||||
with self.assertRaises(ZeroDivisionError):
|
|
||||||
async with woohoo() as x:
|
|
||||||
self.assertEqual(state, [1])
|
|
||||||
self.assertEqual(x, 42)
|
|
||||||
state.append(x)
|
|
||||||
raise ZeroDivisionError()
|
|
||||||
self.assertEqual(state, [1, 42, 999])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_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
|
|
||||||
async def whee():
|
|
||||||
yield
|
|
||||||
ctx = whee()
|
|
||||||
await ctx.__aenter__()
|
|
||||||
# Calling __aexit__ should not result in an exception
|
|
||||||
self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None))
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_trap_yield_after_throw(self):
|
|
||||||
@asynccontextmanager
|
|
||||||
async def whoo():
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
except:
|
|
||||||
yield
|
|
||||||
ctx = whoo()
|
|
||||||
await ctx.__aenter__()
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
await ctx.__aexit__(TypeError, TypeError('foo'), None)
|
|
||||||
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):
|
|
||||||
@asynccontextmanager
|
|
||||||
async def whoo():
|
|
||||||
if False:
|
|
||||||
yield
|
|
||||||
ctx = whoo()
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
await ctx.__aenter__()
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_trap_second_yield(self):
|
|
||||||
@asynccontextmanager
|
|
||||||
async def whoo():
|
|
||||||
yield
|
|
||||||
yield
|
|
||||||
ctx = whoo()
|
|
||||||
await ctx.__aenter__()
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
await ctx.__aexit__(None, None, None)
|
|
||||||
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):
|
|
||||||
@asynccontextmanager
|
|
||||||
async def whoo():
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
except RuntimeError:
|
|
||||||
raise SyntaxError
|
|
||||||
|
|
||||||
ctx = whoo()
|
|
||||||
await ctx.__aenter__()
|
|
||||||
with self.assertRaises(SyntaxError):
|
|
||||||
await ctx.__aexit__(RuntimeError, None, None)
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_except(self):
|
|
||||||
state = []
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo():
|
|
||||||
state.append(1)
|
|
||||||
try:
|
|
||||||
yield 42
|
|
||||||
except ZeroDivisionError as e:
|
|
||||||
state.append(e.args[0])
|
|
||||||
self.assertEqual(state, [1, 42, 999])
|
|
||||||
async with woohoo() as x:
|
|
||||||
self.assertEqual(state, [1])
|
|
||||||
self.assertEqual(x, 42)
|
|
||||||
state.append(x)
|
|
||||||
raise ZeroDivisionError(999)
|
|
||||||
self.assertEqual(state, [1, 42, 999])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_except_stopiter(self):
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo():
|
|
||||||
yield
|
|
||||||
|
|
||||||
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():
|
|
||||||
raise stop_exc
|
|
||||||
except Exception as ex:
|
|
||||||
self.assertIs(ex, stop_exc)
|
|
||||||
else:
|
|
||||||
self.fail(f'{stop_exc} was suppressed')
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_contextmanager_wrap_runtimeerror(self):
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo():
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
except Exception as exc:
|
|
||||||
raise RuntimeError(f'caught {exc}') from exc
|
|
||||||
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
async with woohoo():
|
|
||||||
1 / 0
|
|
||||||
|
|
||||||
# If the context manager wrapped StopAsyncIteration in a RuntimeError,
|
|
||||||
# we also unwrap it, because we can't tell whether the wrapping was
|
|
||||||
# done by the generator machinery or by the generator itself.
|
|
||||||
with self.assertRaises(StopAsyncIteration):
|
|
||||||
async with woohoo():
|
|
||||||
raise StopAsyncIteration
|
|
||||||
|
|
||||||
def _create_contextmanager_attribs(self):
|
|
||||||
def attribs(**kw):
|
|
||||||
def decorate(func):
|
|
||||||
for k,v in kw.items():
|
|
||||||
setattr(func,k,v)
|
|
||||||
return func
|
|
||||||
return decorate
|
|
||||||
@asynccontextmanager
|
|
||||||
@attribs(foo='bar')
|
|
||||||
async def baz(spam):
|
|
||||||
"""Whee!"""
|
|
||||||
yield
|
|
||||||
return baz
|
|
||||||
|
|
||||||
def test_contextmanager_attribs(self):
|
|
||||||
baz = self._create_contextmanager_attribs()
|
|
||||||
self.assertEqual(baz.__name__,'baz')
|
|
||||||
self.assertEqual(baz.foo, 'bar')
|
|
||||||
|
|
||||||
@support.requires_docstrings
|
|
||||||
def test_contextmanager_doc_attrib(self):
|
|
||||||
baz = self._create_contextmanager_attribs()
|
|
||||||
self.assertEqual(baz.__doc__, "Whee!")
|
|
||||||
|
|
||||||
@support.requires_docstrings
|
|
||||||
@_async_test
|
|
||||||
async def test_instance_docstring_given_cm_docstring(self):
|
|
||||||
baz = self._create_contextmanager_attribs()(None)
|
|
||||||
self.assertEqual(baz.__doc__, "Whee!")
|
|
||||||
async with baz:
|
|
||||||
pass # suppress warning
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_keywords(self):
|
|
||||||
# Ensure no keyword arguments are inhibited
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo(self, func, args, kwds):
|
|
||||||
yield (self, func, args, kwds)
|
|
||||||
async with woohoo(self=11, func=22, args=33, kwds=44) as target:
|
|
||||||
self.assertEqual(target, (11, 22, 33, 44))
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_recursive(self):
|
|
||||||
depth = 0
|
|
||||||
ncols = 0
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def woohoo():
|
|
||||||
nonlocal ncols
|
|
||||||
ncols += 1
|
|
||||||
|
|
||||||
nonlocal depth
|
|
||||||
before = depth
|
|
||||||
depth += 1
|
|
||||||
yield
|
|
||||||
depth -= 1
|
|
||||||
self.assertEqual(depth, before)
|
|
||||||
|
|
||||||
@woohoo()
|
|
||||||
async def recursive():
|
|
||||||
if depth < 10:
|
|
||||||
await recursive()
|
|
||||||
|
|
||||||
await recursive()
|
|
||||||
|
|
||||||
self.assertEqual(ncols, 10)
|
|
||||||
self.assertEqual(depth, 0)
|
|
||||||
|
|
||||||
@_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):
|
|
||||||
|
|
||||||
@support.requires_docstrings
|
|
||||||
def test_instance_docs(self):
|
|
||||||
cm_docstring = aclosing.__doc__
|
|
||||||
obj = aclosing(None)
|
|
||||||
self.assertEqual(obj.__doc__, cm_docstring)
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_aclosing(self):
|
|
||||||
state = []
|
|
||||||
class C:
|
|
||||||
async def aclose(self):
|
|
||||||
state.append(1)
|
|
||||||
x = C()
|
|
||||||
self.assertEqual(state, [])
|
|
||||||
async with aclosing(x) as y:
|
|
||||||
self.assertEqual(x, y)
|
|
||||||
self.assertEqual(state, [1])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_aclosing_error(self):
|
|
||||||
state = []
|
|
||||||
class C:
|
|
||||||
async def aclose(self):
|
|
||||||
state.append(1)
|
|
||||||
x = C()
|
|
||||||
self.assertEqual(state, [])
|
|
||||||
with self.assertRaises(ZeroDivisionError):
|
|
||||||
async with aclosing(x) as y:
|
|
||||||
self.assertEqual(x, y)
|
|
||||||
1 / 0
|
|
||||||
self.assertEqual(state, [1])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_aclosing_bpo41229(self):
|
|
||||||
state = []
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def sync_resource():
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
state.append(1)
|
|
||||||
|
|
||||||
async def agenfunc():
|
|
||||||
with sync_resource():
|
|
||||||
yield -1
|
|
||||||
yield -2
|
|
||||||
|
|
||||||
x = agenfunc()
|
|
||||||
self.assertEqual(state, [])
|
|
||||||
with self.assertRaises(ZeroDivisionError):
|
|
||||||
async with aclosing(x) as y:
|
|
||||||
self.assertEqual(x, y)
|
|
||||||
self.assertEqual(-1, await x.__anext__())
|
|
||||||
1 / 0
|
|
||||||
self.assertEqual(state, [1])
|
|
||||||
|
|
||||||
|
|
||||||
class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase):
|
|
||||||
class SyncAsyncExitStack(AsyncExitStack):
|
|
||||||
@staticmethod
|
|
||||||
def run_coroutine(coro):
|
|
||||||
loop = asyncio.get_event_loop_policy().get_event_loop()
|
|
||||||
t = loop.create_task(coro)
|
|
||||||
t.add_done_callback(lambda f: loop.stop())
|
|
||||||
loop.run_forever()
|
|
||||||
|
|
||||||
exc = t.exception()
|
|
||||||
if not exc:
|
|
||||||
return t.result()
|
|
||||||
else:
|
|
||||||
context = exc.__context__
|
|
||||||
|
|
||||||
try:
|
|
||||||
raise exc
|
|
||||||
except:
|
|
||||||
exc.__context__ = context
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
return self.run_coroutine(self.aclose())
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self.run_coroutine(self.__aenter__())
|
|
||||||
|
|
||||||
def __exit__(self, *exc_details):
|
|
||||||
return self.run_coroutine(self.__aexit__(*exc_details))
|
|
||||||
|
|
||||||
exit_stack = SyncAsyncExitStack
|
|
||||||
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()
|
|
||||||
asyncio.set_event_loop(self.loop)
|
|
||||||
self.addCleanup(self.loop.close)
|
|
||||||
self.addCleanup(asyncio.set_event_loop_policy, None)
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_async_callback(self):
|
|
||||||
expected = [
|
|
||||||
((), {}),
|
|
||||||
((1,), {}),
|
|
||||||
((1,2), {}),
|
|
||||||
((), dict(example=1)),
|
|
||||||
((1,), dict(example=1)),
|
|
||||||
((1,2), dict(example=1)),
|
|
||||||
]
|
|
||||||
result = []
|
|
||||||
async def _exit(*args, **kwds):
|
|
||||||
"""Test metadata propagation"""
|
|
||||||
result.append((args, kwds))
|
|
||||||
|
|
||||||
async with AsyncExitStack() as stack:
|
|
||||||
for args, kwds in reversed(expected):
|
|
||||||
if args and kwds:
|
|
||||||
f = stack.push_async_callback(_exit, *args, **kwds)
|
|
||||||
elif args:
|
|
||||||
f = stack.push_async_callback(_exit, *args)
|
|
||||||
elif kwds:
|
|
||||||
f = stack.push_async_callback(_exit, **kwds)
|
|
||||||
else:
|
|
||||||
f = stack.push_async_callback(_exit)
|
|
||||||
self.assertIs(f, _exit)
|
|
||||||
for wrapper in stack._exit_callbacks:
|
|
||||||
self.assertIs(wrapper[1].__wrapped__, _exit)
|
|
||||||
self.assertNotEqual(wrapper[1].__name__, _exit.__name__)
|
|
||||||
self.assertIsNone(wrapper[1].__doc__, _exit.__doc__)
|
|
||||||
|
|
||||||
self.assertEqual(result, expected)
|
|
||||||
|
|
||||||
result = []
|
|
||||||
async with AsyncExitStack() as stack:
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
stack.push_async_callback(arg=1)
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
self.exit_stack.push_async_callback(arg=2)
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
stack.push_async_callback(callback=_exit, arg=3)
|
|
||||||
self.assertEqual(result, [])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_async_push(self):
|
|
||||||
exc_raised = ZeroDivisionError
|
|
||||||
async def _expect_exc(exc_type, exc, exc_tb):
|
|
||||||
self.assertIs(exc_type, exc_raised)
|
|
||||||
async def _suppress_exc(*exc_details):
|
|
||||||
return True
|
|
||||||
async def _expect_ok(exc_type, exc, exc_tb):
|
|
||||||
self.assertIsNone(exc_type)
|
|
||||||
self.assertIsNone(exc)
|
|
||||||
self.assertIsNone(exc_tb)
|
|
||||||
class ExitCM(object):
|
|
||||||
def __init__(self, check_exc):
|
|
||||||
self.check_exc = check_exc
|
|
||||||
async def __aenter__(self):
|
|
||||||
self.fail("Should not be called!")
|
|
||||||
async def __aexit__(self, *exc_details):
|
|
||||||
await self.check_exc(*exc_details)
|
|
||||||
|
|
||||||
async with self.exit_stack() as stack:
|
|
||||||
stack.push_async_exit(_expect_ok)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1], _expect_ok)
|
|
||||||
cm = ExitCM(_expect_ok)
|
|
||||||
stack.push_async_exit(cm)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1].__self__, cm)
|
|
||||||
stack.push_async_exit(_suppress_exc)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1], _suppress_exc)
|
|
||||||
cm = ExitCM(_expect_exc)
|
|
||||||
stack.push_async_exit(cm)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1].__self__, cm)
|
|
||||||
stack.push_async_exit(_expect_exc)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1], _expect_exc)
|
|
||||||
stack.push_async_exit(_expect_exc)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1], _expect_exc)
|
|
||||||
1/0
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_enter_async_context(self):
|
|
||||||
class TestCM(object):
|
|
||||||
async def __aenter__(self):
|
|
||||||
result.append(1)
|
|
||||||
async def __aexit__(self, *exc_details):
|
|
||||||
result.append(3)
|
|
||||||
|
|
||||||
result = []
|
|
||||||
cm = TestCM()
|
|
||||||
|
|
||||||
async with AsyncExitStack() as stack:
|
|
||||||
@stack.push_async_callback # Registered first => cleaned up last
|
|
||||||
async def _exit():
|
|
||||||
result.append(4)
|
|
||||||
self.assertIsNotNone(_exit)
|
|
||||||
await stack.enter_async_context(cm)
|
|
||||||
self.assertIs(stack._exit_callbacks[-1][1].__self__, cm)
|
|
||||||
result.append(2)
|
|
||||||
|
|
||||||
self.assertEqual(result, [1, 2, 3, 4])
|
|
||||||
|
|
||||||
@_async_test
|
|
||||||
async def test_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
|
|
||||||
async def raise_exc(exc):
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
saved_details = None
|
|
||||||
async def suppress_exc(*exc_details):
|
|
||||||
nonlocal saved_details
|
|
||||||
saved_details = exc_details
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with self.exit_stack() as stack:
|
|
||||||
stack.push_async_callback(raise_exc, IndexError)
|
|
||||||
stack.push_async_callback(raise_exc, KeyError)
|
|
||||||
stack.push_async_callback(raise_exc, AttributeError)
|
|
||||||
stack.push_async_exit(suppress_exc)
|
|
||||||
stack.push_async_callback(raise_exc, ValueError)
|
|
||||||
1 / 0
|
|
||||||
except IndexError as exc:
|
|
||||||
self.assertIsInstance(exc.__context__, KeyError)
|
|
||||||
self.assertIsInstance(exc.__context__.__context__, AttributeError)
|
|
||||||
# Inner exceptions were suppressed
|
|
||||||
self.assertIsNone(exc.__context__.__context__.__context__)
|
|
||||||
else:
|
|
||||||
self.fail("Expected IndexError, but no exception was raised")
|
|
||||||
# Check the inner exceptions
|
|
||||||
inner_exc = saved_details[1]
|
|
||||||
self.assertIsInstance(inner_exc, ValueError)
|
|
||||||
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
|
||||||
|
|
||||||
@_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
|
|
||||||
async def test_async_nullcontext(self):
|
|
||||||
class C:
|
|
||||||
pass
|
|
||||||
c = C()
|
|
||||||
async with nullcontext(c) as c_in:
|
|
||||||
self.assertIs(c_in, c)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
test_contextlib uses this folder for chdir tests
|
|
||||||
835
test_contextlib2.py
Executable file
835
test_contextlib2.py
Executable file
|
|
@ -0,0 +1,835 @@
|
||||||
|
"""Unit tests for contextlib2.py"""
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import __future__ # For PEP 479 conditional test
|
||||||
|
import contextlib2
|
||||||
|
from contextlib2 import * # Tests __all__
|
||||||
|
|
||||||
|
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
|
||||||
|
import unittest2 as unittest
|
||||||
|
|
||||||
|
requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2,
|
||||||
|
"Test requires docstrings")
|
||||||
|
|
||||||
|
class ContextManagerTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_contextmanager_plain(self):
|
||||||
|
state = []
|
||||||
|
@contextmanager
|
||||||
|
def woohoo():
|
||||||
|
state.append(1)
|
||||||
|
yield 42
|
||||||
|
state.append(999)
|
||||||
|
with woohoo() as x:
|
||||||
|
self.assertEqual(state, [1])
|
||||||
|
self.assertEqual(x, 42)
|
||||||
|
state.append(x)
|
||||||
|
self.assertEqual(state, [1, 42, 999])
|
||||||
|
|
||||||
|
def test_contextmanager_finally(self):
|
||||||
|
state = []
|
||||||
|
@contextmanager
|
||||||
|
def woohoo():
|
||||||
|
state.append(1)
|
||||||
|
try:
|
||||||
|
yield 42
|
||||||
|
finally:
|
||||||
|
state.append(999)
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with woohoo() as x:
|
||||||
|
self.assertEqual(state, [1])
|
||||||
|
self.assertEqual(x, 42)
|
||||||
|
state.append(x)
|
||||||
|
raise ZeroDivisionError()
|
||||||
|
self.assertEqual(state, [1, 42, 999])
|
||||||
|
|
||||||
|
def test_contextmanager_no_reraise(self):
|
||||||
|
@contextmanager
|
||||||
|
def whee():
|
||||||
|
yield
|
||||||
|
ctx = whee()
|
||||||
|
ctx.__enter__()
|
||||||
|
# Calling __exit__ should not result in an exception
|
||||||
|
self.assertFalse(ctx.__exit__(TypeError, TypeError("foo"), None))
|
||||||
|
|
||||||
|
def test_contextmanager_trap_yield_after_throw(self):
|
||||||
|
@contextmanager
|
||||||
|
def whoo():
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except:
|
||||||
|
yield
|
||||||
|
ctx = whoo()
|
||||||
|
ctx.__enter__()
|
||||||
|
self.assertRaises(
|
||||||
|
RuntimeError, ctx.__exit__, TypeError, TypeError("foo"), None
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_contextmanager_except(self):
|
||||||
|
state = []
|
||||||
|
@contextmanager
|
||||||
|
def woohoo():
|
||||||
|
state.append(1)
|
||||||
|
try:
|
||||||
|
yield 42
|
||||||
|
except ZeroDivisionError as e:
|
||||||
|
state.append(e.args[0])
|
||||||
|
self.assertEqual(state, [1, 42, 999])
|
||||||
|
with woohoo() as x:
|
||||||
|
self.assertEqual(state, [1])
|
||||||
|
self.assertEqual(x, 42)
|
||||||
|
state.append(x)
|
||||||
|
raise ZeroDivisionError(999)
|
||||||
|
self.assertEqual(state, [1, 42, 999])
|
||||||
|
|
||||||
|
def test_contextmanager_except_stopiter(self):
|
||||||
|
stop_exc = StopIteration('spam')
|
||||||
|
@contextmanager
|
||||||
|
def woohoo():
|
||||||
|
yield
|
||||||
|
try:
|
||||||
|
with self.assertWarnsRegex(PendingDeprecationWarning,
|
||||||
|
"StopIteration"):
|
||||||
|
with woohoo():
|
||||||
|
raise stop_exc
|
||||||
|
except Exception as ex:
|
||||||
|
self.assertIs(ex, stop_exc)
|
||||||
|
else:
|
||||||
|
self.fail('StopIteration was suppressed')
|
||||||
|
|
||||||
|
@unittest.skipUnless(hasattr(__future__, "generator_stop"),
|
||||||
|
"Test only valid for versions implementing PEP 479")
|
||||||
|
def test_contextmanager_except_pep479(self):
|
||||||
|
code = """\
|
||||||
|
from __future__ import generator_stop
|
||||||
|
from contextlib import contextmanager
|
||||||
|
@contextmanager
|
||||||
|
def woohoo():
|
||||||
|
yield
|
||||||
|
"""
|
||||||
|
locals = {}
|
||||||
|
exec(code, locals, locals)
|
||||||
|
woohoo = locals['woohoo']
|
||||||
|
|
||||||
|
stop_exc = StopIteration('spam')
|
||||||
|
try:
|
||||||
|
with woohoo():
|
||||||
|
raise stop_exc
|
||||||
|
except Exception as ex:
|
||||||
|
self.assertIs(ex, stop_exc)
|
||||||
|
else:
|
||||||
|
self.fail('StopIteration was suppressed')
|
||||||
|
|
||||||
|
def _create_contextmanager_attribs(self):
|
||||||
|
def attribs(**kw):
|
||||||
|
def decorate(func):
|
||||||
|
for k,v in kw.items():
|
||||||
|
setattr(func,k,v)
|
||||||
|
return func
|
||||||
|
return decorate
|
||||||
|
@contextmanager
|
||||||
|
@attribs(foo='bar')
|
||||||
|
def baz(spam):
|
||||||
|
"""Whee!"""
|
||||||
|
return baz
|
||||||
|
|
||||||
|
def test_contextmanager_attribs(self):
|
||||||
|
baz = self._create_contextmanager_attribs()
|
||||||
|
self.assertEqual(baz.__name__,'baz')
|
||||||
|
self.assertEqual(baz.foo, 'bar')
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_contextmanager_doc_attrib(self):
|
||||||
|
baz = self._create_contextmanager_attribs()
|
||||||
|
self.assertEqual(baz.__doc__, "Whee!")
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_instance_docstring_given_cm_docstring(self):
|
||||||
|
baz = self._create_contextmanager_attribs()(None)
|
||||||
|
self.assertEqual(baz.__doc__, "Whee!")
|
||||||
|
|
||||||
|
def test_keywords(self):
|
||||||
|
# Ensure no keyword arguments are inhibited
|
||||||
|
@contextmanager
|
||||||
|
def woohoo(self, func, args, kwds):
|
||||||
|
yield (self, func, args, kwds)
|
||||||
|
with woohoo(self=11, func=22, args=33, kwds=44) as target:
|
||||||
|
self.assertEqual(target, (11, 22, 33, 44))
|
||||||
|
|
||||||
|
|
||||||
|
class ClosingTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_instance_docs(self):
|
||||||
|
# Issue 19330: ensure context manager instances have good docstrings
|
||||||
|
cm_docstring = closing.__doc__
|
||||||
|
obj = closing(None)
|
||||||
|
self.assertEqual(obj.__doc__, cm_docstring)
|
||||||
|
|
||||||
|
def test_closing(self):
|
||||||
|
state = []
|
||||||
|
class C:
|
||||||
|
def close(self):
|
||||||
|
state.append(1)
|
||||||
|
x = C()
|
||||||
|
self.assertEqual(state, [])
|
||||||
|
with closing(x) as y:
|
||||||
|
self.assertEqual(x, y)
|
||||||
|
self.assertEqual(state, [1])
|
||||||
|
|
||||||
|
def test_closing_error(self):
|
||||||
|
state = []
|
||||||
|
class C:
|
||||||
|
def close(self):
|
||||||
|
state.append(1)
|
||||||
|
x = C()
|
||||||
|
self.assertEqual(state, [])
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with closing(x) as y:
|
||||||
|
self.assertEqual(x, y)
|
||||||
|
1 / 0
|
||||||
|
self.assertEqual(state, [1])
|
||||||
|
|
||||||
|
|
||||||
|
class mycontext(ContextDecorator):
|
||||||
|
"""Example decoration-compatible context manager for testing"""
|
||||||
|
started = False
|
||||||
|
exc = None
|
||||||
|
catch = False
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.started = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self.exc = exc
|
||||||
|
return self.catch
|
||||||
|
|
||||||
|
|
||||||
|
class TestContextDecorator(unittest.TestCase):
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_instance_docs(self):
|
||||||
|
# Issue 19330: ensure context manager instances have good docstrings
|
||||||
|
cm_docstring = mycontext.__doc__
|
||||||
|
obj = mycontext()
|
||||||
|
self.assertEqual(obj.__doc__, cm_docstring)
|
||||||
|
|
||||||
|
def test_contextdecorator(self):
|
||||||
|
context = mycontext()
|
||||||
|
with context as result:
|
||||||
|
self.assertIs(result, context)
|
||||||
|
self.assertTrue(context.started)
|
||||||
|
|
||||||
|
self.assertEqual(context.exc, (None, None, None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_contextdecorator_with_exception(self):
|
||||||
|
context = mycontext()
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(NameError, 'foo'):
|
||||||
|
with context:
|
||||||
|
raise NameError('foo')
|
||||||
|
self.assertIsNotNone(context.exc)
|
||||||
|
self.assertIs(context.exc[0], NameError)
|
||||||
|
|
||||||
|
context = mycontext()
|
||||||
|
context.catch = True
|
||||||
|
with context:
|
||||||
|
raise NameError('foo')
|
||||||
|
self.assertIsNotNone(context.exc)
|
||||||
|
self.assertIs(context.exc[0], NameError)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decorator(self):
|
||||||
|
context = mycontext()
|
||||||
|
|
||||||
|
@context
|
||||||
|
def test():
|
||||||
|
self.assertIsNone(context.exc)
|
||||||
|
self.assertTrue(context.started)
|
||||||
|
test()
|
||||||
|
self.assertEqual(context.exc, (None, None, None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_decorator_with_exception(self):
|
||||||
|
context = mycontext()
|
||||||
|
|
||||||
|
@context
|
||||||
|
def test():
|
||||||
|
self.assertIsNone(context.exc)
|
||||||
|
self.assertTrue(context.started)
|
||||||
|
raise NameError('foo')
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(NameError, 'foo'):
|
||||||
|
test()
|
||||||
|
self.assertIsNotNone(context.exc)
|
||||||
|
self.assertIs(context.exc[0], NameError)
|
||||||
|
|
||||||
|
|
||||||
|
def test_decorating_method(self):
|
||||||
|
context = mycontext()
|
||||||
|
|
||||||
|
class Test(object):
|
||||||
|
|
||||||
|
@context
|
||||||
|
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()
|
||||||
|
test.method(1, 2)
|
||||||
|
self.assertEqual(test.a, 1)
|
||||||
|
self.assertEqual(test.b, 2)
|
||||||
|
self.assertEqual(test.c, None)
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.method('a', 'b', 'c')
|
||||||
|
self.assertEqual(test.a, 'a')
|
||||||
|
self.assertEqual(test.b, 'b')
|
||||||
|
self.assertEqual(test.c, 'c')
|
||||||
|
|
||||||
|
test = Test()
|
||||||
|
test.method(a=1, b=2)
|
||||||
|
self.assertEqual(test.a, 1)
|
||||||
|
self.assertEqual(test.b, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_typo_enter(self):
|
||||||
|
class mycontext(ContextDecorator):
|
||||||
|
def __unter__(self):
|
||||||
|
pass
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
with mycontext():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_typo_exit(self):
|
||||||
|
class mycontext(ContextDecorator):
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
def __uxit__(self, *exc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
with mycontext():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_contextdecorator_as_mixin(self):
|
||||||
|
class somecontext(object):
|
||||||
|
started = False
|
||||||
|
exc = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.started = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self.exc = exc
|
||||||
|
|
||||||
|
class mycontext(somecontext, ContextDecorator):
|
||||||
|
pass
|
||||||
|
|
||||||
|
context = mycontext()
|
||||||
|
@context
|
||||||
|
def test():
|
||||||
|
self.assertIsNone(context.exc)
|
||||||
|
self.assertTrue(context.started)
|
||||||
|
test()
|
||||||
|
self.assertEqual(context.exc, (None, None, None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_contextmanager_as_decorator(self):
|
||||||
|
@contextmanager
|
||||||
|
def woohoo(y):
|
||||||
|
state.append(y)
|
||||||
|
yield
|
||||||
|
state.append(999)
|
||||||
|
|
||||||
|
state = []
|
||||||
|
@woohoo(1)
|
||||||
|
def test(x):
|
||||||
|
self.assertEqual(state, [1])
|
||||||
|
state.append(x)
|
||||||
|
test('something')
|
||||||
|
self.assertEqual(state, [1, 'something', 999])
|
||||||
|
|
||||||
|
# Issue #11647: Ensure the decorated function is 'reusable'
|
||||||
|
state = []
|
||||||
|
test('something else')
|
||||||
|
self.assertEqual(state, [1, 'something else', 999])
|
||||||
|
|
||||||
|
# Detailed exception chaining checks only make sense on Python 3
|
||||||
|
check_exception_chaining = contextlib2._HAVE_EXCEPTION_CHAINING
|
||||||
|
|
||||||
|
class TestExitStack(unittest.TestCase):
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_instance_docs(self):
|
||||||
|
# Issue 19330: ensure context manager instances have good docstrings
|
||||||
|
cm_docstring = ExitStack.__doc__
|
||||||
|
obj = ExitStack()
|
||||||
|
self.assertEqual(obj.__doc__, cm_docstring)
|
||||||
|
|
||||||
|
def test_no_resources(self):
|
||||||
|
with ExitStack():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_callback(self):
|
||||||
|
expected = [
|
||||||
|
((), {}),
|
||||||
|
((1,), {}),
|
||||||
|
((1,2), {}),
|
||||||
|
((), dict(example=1)),
|
||||||
|
((1,), dict(example=1)),
|
||||||
|
((1,2), dict(example=1)),
|
||||||
|
]
|
||||||
|
result = []
|
||||||
|
def _exit(*args, **kwds):
|
||||||
|
"""Test metadata propagation"""
|
||||||
|
result.append((args, kwds))
|
||||||
|
with ExitStack() as stack:
|
||||||
|
for args, kwds in reversed(expected):
|
||||||
|
if args and kwds:
|
||||||
|
f = stack.callback(_exit, *args, **kwds)
|
||||||
|
elif args:
|
||||||
|
f = stack.callback(_exit, *args)
|
||||||
|
elif kwds:
|
||||||
|
f = stack.callback(_exit, **kwds)
|
||||||
|
else:
|
||||||
|
f = stack.callback(_exit)
|
||||||
|
self.assertIs(f, _exit)
|
||||||
|
for wrapper in stack._exit_callbacks:
|
||||||
|
self.assertIs(wrapper.__wrapped__, _exit)
|
||||||
|
self.assertNotEqual(wrapper.__name__, _exit.__name__)
|
||||||
|
self.assertIsNone(wrapper.__doc__, _exit.__doc__)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_push(self):
|
||||||
|
exc_raised = ZeroDivisionError
|
||||||
|
def _expect_exc(exc_type, exc, exc_tb):
|
||||||
|
self.assertIs(exc_type, exc_raised)
|
||||||
|
def _suppress_exc(*exc_details):
|
||||||
|
return True
|
||||||
|
def _expect_ok(exc_type, exc, exc_tb):
|
||||||
|
self.assertIsNone(exc_type)
|
||||||
|
self.assertIsNone(exc)
|
||||||
|
self.assertIsNone(exc_tb)
|
||||||
|
class ExitCM(object):
|
||||||
|
def __init__(self, check_exc):
|
||||||
|
self.check_exc = check_exc
|
||||||
|
def __enter__(self):
|
||||||
|
self.fail("Should not be called!")
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
self.check_exc(*exc_details)
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.push(_expect_ok)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1], _expect_ok)
|
||||||
|
cm = ExitCM(_expect_ok)
|
||||||
|
stack.push(cm)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||||
|
stack.push(_suppress_exc)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1], _suppress_exc)
|
||||||
|
cm = ExitCM(_expect_exc)
|
||||||
|
stack.push(cm)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||||
|
stack.push(_expect_exc)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1], _expect_exc)
|
||||||
|
stack.push(_expect_exc)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1], _expect_exc)
|
||||||
|
1/0
|
||||||
|
|
||||||
|
def test_enter_context(self):
|
||||||
|
class TestCM(object):
|
||||||
|
def __enter__(self):
|
||||||
|
result.append(1)
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
result.append(3)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
cm = TestCM()
|
||||||
|
with ExitStack() as stack:
|
||||||
|
@stack.callback # Registered first => cleaned up last
|
||||||
|
def _exit():
|
||||||
|
result.append(4)
|
||||||
|
self.assertIsNotNone(_exit)
|
||||||
|
stack.enter_context(cm)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||||
|
result.append(2)
|
||||||
|
self.assertEqual(result, [1, 2, 3, 4])
|
||||||
|
|
||||||
|
def test_close(self):
|
||||||
|
result = []
|
||||||
|
with ExitStack() as stack:
|
||||||
|
@stack.callback
|
||||||
|
def _exit():
|
||||||
|
result.append(1)
|
||||||
|
self.assertIsNotNone(_exit)
|
||||||
|
stack.close()
|
||||||
|
result.append(2)
|
||||||
|
self.assertEqual(result, [1, 2])
|
||||||
|
|
||||||
|
def test_pop_all(self):
|
||||||
|
result = []
|
||||||
|
with ExitStack() as stack:
|
||||||
|
@stack.callback
|
||||||
|
def _exit():
|
||||||
|
result.append(3)
|
||||||
|
self.assertIsNotNone(_exit)
|
||||||
|
new_stack = stack.pop_all()
|
||||||
|
result.append(1)
|
||||||
|
result.append(2)
|
||||||
|
new_stack.close()
|
||||||
|
self.assertEqual(result, [1, 2, 3])
|
||||||
|
|
||||||
|
def test_exit_raise(self):
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.push(lambda *exc: False)
|
||||||
|
1/0
|
||||||
|
|
||||||
|
def test_exit_suppress(self):
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.push(lambda *exc: True)
|
||||||
|
1/0
|
||||||
|
|
||||||
|
def test_exit_exception_chaining_reference(self):
|
||||||
|
# Sanity check to make sure that ExitStack chaining matches
|
||||||
|
# actual nested with statements
|
||||||
|
class RaiseExc:
|
||||||
|
def __init__(self, exc):
|
||||||
|
self.exc = exc
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
raise self.exc
|
||||||
|
|
||||||
|
class RaiseExcWithContext:
|
||||||
|
def __init__(self, outer, inner):
|
||||||
|
self.outer = outer
|
||||||
|
self.inner = inner
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
try:
|
||||||
|
raise self.inner
|
||||||
|
except:
|
||||||
|
raise self.outer
|
||||||
|
|
||||||
|
class SuppressExc:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
def __exit__(self, *exc_details):
|
||||||
|
self.__class__.saved_details = exc_details
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
with RaiseExc(IndexError):
|
||||||
|
with RaiseExcWithContext(KeyError, AttributeError):
|
||||||
|
with SuppressExc():
|
||||||
|
with RaiseExc(ValueError):
|
||||||
|
1 / 0
|
||||||
|
except IndexError as exc:
|
||||||
|
if check_exception_chaining:
|
||||||
|
self.assertIsInstance(exc.__context__, KeyError)
|
||||||
|
self.assertIsInstance(exc.__context__.__context__, AttributeError)
|
||||||
|
# Inner exceptions were suppressed
|
||||||
|
self.assertIsNone(exc.__context__.__context__.__context__)
|
||||||
|
else:
|
||||||
|
self.fail("Expected IndexError, but no exception was raised")
|
||||||
|
# Check the inner exceptions
|
||||||
|
inner_exc = SuppressExc.saved_details[1]
|
||||||
|
self.assertIsInstance(inner_exc, ValueError)
|
||||||
|
if check_exception_chaining:
|
||||||
|
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
||||||
|
|
||||||
|
def test_exit_exception_chaining(self):
|
||||||
|
# Ensure exception chaining matches the reference behaviour
|
||||||
|
def raise_exc(exc):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
saved_details = [None]
|
||||||
|
def suppress_exc(*exc_details):
|
||||||
|
saved_details[0] = exc_details
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.callback(raise_exc, IndexError)
|
||||||
|
stack.callback(raise_exc, KeyError)
|
||||||
|
stack.callback(raise_exc, AttributeError)
|
||||||
|
stack.push(suppress_exc)
|
||||||
|
stack.callback(raise_exc, ValueError)
|
||||||
|
1 / 0
|
||||||
|
except IndexError as exc:
|
||||||
|
if check_exception_chaining:
|
||||||
|
self.assertIsInstance(exc.__context__, KeyError)
|
||||||
|
self.assertIsInstance(exc.__context__.__context__, AttributeError)
|
||||||
|
# Inner exceptions were suppressed
|
||||||
|
self.assertIsNone(exc.__context__.__context__.__context__)
|
||||||
|
else:
|
||||||
|
self.fail("Expected IndexError, but no exception was raised")
|
||||||
|
# Check the inner exceptions
|
||||||
|
inner_exc = saved_details[0][1]
|
||||||
|
self.assertIsInstance(inner_exc, ValueError)
|
||||||
|
if check_exception_chaining:
|
||||||
|
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
|
||||||
|
|
||||||
|
def test_exit_exception_non_suppressing(self):
|
||||||
|
# http://bugs.python.org/issue19092
|
||||||
|
def raise_exc(exc):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
def suppress_exc(*exc_details):
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.callback(lambda: None)
|
||||||
|
stack.callback(raise_exc, IndexError)
|
||||||
|
except Exception as exc:
|
||||||
|
self.assertIsInstance(exc, IndexError)
|
||||||
|
else:
|
||||||
|
self.fail("Expected IndexError, but no exception was raised")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.callback(raise_exc, KeyError)
|
||||||
|
stack.push(suppress_exc)
|
||||||
|
stack.callback(raise_exc, IndexError)
|
||||||
|
except Exception as exc:
|
||||||
|
self.assertIsInstance(exc, KeyError)
|
||||||
|
else:
|
||||||
|
self.fail("Expected KeyError, but no exception was raised")
|
||||||
|
|
||||||
|
def test_exit_exception_with_correct_context(self):
|
||||||
|
# http://bugs.python.org/issue20317
|
||||||
|
@contextmanager
|
||||||
|
def gets_the_context_right(exc):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
exc1 = Exception(1)
|
||||||
|
exc2 = Exception(2)
|
||||||
|
exc3 = Exception(3)
|
||||||
|
exc4 = Exception(4)
|
||||||
|
|
||||||
|
# The contextmanager already fixes the context, so prior to the
|
||||||
|
# fix, ExitStack would try to fix it *again* and get into an
|
||||||
|
# infinite self-referential loop
|
||||||
|
try:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.enter_context(gets_the_context_right(exc4))
|
||||||
|
stack.enter_context(gets_the_context_right(exc3))
|
||||||
|
stack.enter_context(gets_the_context_right(exc2))
|
||||||
|
raise exc1
|
||||||
|
except Exception as exc:
|
||||||
|
self.assertIs(exc, exc4)
|
||||||
|
if check_exception_chaining:
|
||||||
|
self.assertIs(exc.__context__, exc3)
|
||||||
|
self.assertIs(exc.__context__.__context__, exc2)
|
||||||
|
self.assertIs(exc.__context__.__context__.__context__, exc1)
|
||||||
|
self.assertIsNone(
|
||||||
|
exc.__context__.__context__.__context__.__context__)
|
||||||
|
|
||||||
|
def test_exit_exception_with_existing_context(self):
|
||||||
|
# Addresses a lack of test coverage discovered after checking in a
|
||||||
|
# fix for issue 20317 that still contained debugging code.
|
||||||
|
def raise_nested(inner_exc, outer_exc):
|
||||||
|
try:
|
||||||
|
raise inner_exc
|
||||||
|
finally:
|
||||||
|
raise outer_exc
|
||||||
|
exc1 = Exception(1)
|
||||||
|
exc2 = Exception(2)
|
||||||
|
exc3 = Exception(3)
|
||||||
|
exc4 = Exception(4)
|
||||||
|
exc5 = Exception(5)
|
||||||
|
try:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.callback(raise_nested, exc4, exc5)
|
||||||
|
stack.callback(raise_nested, exc2, exc3)
|
||||||
|
raise exc1
|
||||||
|
except Exception as exc:
|
||||||
|
self.assertIs(exc, exc5)
|
||||||
|
if check_exception_chaining:
|
||||||
|
self.assertIs(exc.__context__, exc4)
|
||||||
|
self.assertIs(exc.__context__.__context__, exc3)
|
||||||
|
self.assertIs(exc.__context__.__context__.__context__, exc2)
|
||||||
|
self.assertIs(
|
||||||
|
exc.__context__.__context__.__context__.__context__, exc1)
|
||||||
|
self.assertIsNone(
|
||||||
|
exc.__context__.__context__.__context__.__context__.__context__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_body_exception_suppress(self):
|
||||||
|
def suppress_exc(*exc_details):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.push(suppress_exc)
|
||||||
|
1/0
|
||||||
|
except IndexError as exc:
|
||||||
|
self.fail("Expected no exception, got IndexError")
|
||||||
|
|
||||||
|
def test_exit_exception_chaining_suppress(self):
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.push(lambda *exc: True)
|
||||||
|
stack.push(lambda *exc: 1/0)
|
||||||
|
stack.push(lambda *exc: {}[1])
|
||||||
|
|
||||||
|
def test_excessive_nesting(self):
|
||||||
|
# The original implementation would die with RecursionError here
|
||||||
|
with ExitStack() as stack:
|
||||||
|
for i in range(10000):
|
||||||
|
stack.callback(int)
|
||||||
|
|
||||||
|
def test_instance_bypass(self):
|
||||||
|
class Example(object): pass
|
||||||
|
cm = Example()
|
||||||
|
cm.__exit__ = object()
|
||||||
|
stack = ExitStack()
|
||||||
|
self.assertRaises(AttributeError, stack.enter_context, cm)
|
||||||
|
stack.push(cm)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1], cm)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedirectStream:
|
||||||
|
|
||||||
|
redirect_stream = None
|
||||||
|
orig_stream = None
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_instance_docs(self):
|
||||||
|
# Issue 19330: ensure context manager instances have good docstrings
|
||||||
|
cm_docstring = self.redirect_stream.__doc__
|
||||||
|
obj = self.redirect_stream(None)
|
||||||
|
self.assertEqual(obj.__doc__, cm_docstring)
|
||||||
|
|
||||||
|
def test_no_redirect_in_init(self):
|
||||||
|
orig_stdout = getattr(sys, self.orig_stream)
|
||||||
|
self.redirect_stream(None)
|
||||||
|
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||||
|
|
||||||
|
def test_redirect_to_string_io(self):
|
||||||
|
f = io.StringIO()
|
||||||
|
msg = "Consider an API like help(), which prints directly to stdout"
|
||||||
|
orig_stdout = getattr(sys, self.orig_stream)
|
||||||
|
with self.redirect_stream(f):
|
||||||
|
print(msg, file=getattr(sys, self.orig_stream))
|
||||||
|
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||||
|
s = f.getvalue().strip()
|
||||||
|
self.assertEqual(s, msg)
|
||||||
|
|
||||||
|
def test_enter_result_is_target(self):
|
||||||
|
f = io.StringIO()
|
||||||
|
with self.redirect_stream(f) as enter_result:
|
||||||
|
self.assertIs(enter_result, f)
|
||||||
|
|
||||||
|
def test_cm_is_reusable(self):
|
||||||
|
f = io.StringIO()
|
||||||
|
write_to_f = self.redirect_stream(f)
|
||||||
|
orig_stdout = getattr(sys, self.orig_stream)
|
||||||
|
with write_to_f:
|
||||||
|
print("Hello", end=" ", file=getattr(sys, self.orig_stream))
|
||||||
|
with write_to_f:
|
||||||
|
print("World!", file=getattr(sys, self.orig_stream))
|
||||||
|
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||||
|
s = f.getvalue()
|
||||||
|
self.assertEqual(s, "Hello World!\n")
|
||||||
|
|
||||||
|
def test_cm_is_reentrant(self):
|
||||||
|
f = io.StringIO()
|
||||||
|
write_to_f = self.redirect_stream(f)
|
||||||
|
orig_stdout = getattr(sys, self.orig_stream)
|
||||||
|
with write_to_f:
|
||||||
|
print("Hello", end=" ", file=getattr(sys, self.orig_stream))
|
||||||
|
with write_to_f:
|
||||||
|
print("World!", file=getattr(sys, self.orig_stream))
|
||||||
|
self.assertIs(getattr(sys, self.orig_stream), orig_stdout)
|
||||||
|
s = f.getvalue()
|
||||||
|
self.assertEqual(s, "Hello World!\n")
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedirectStdout(TestRedirectStream, unittest.TestCase):
|
||||||
|
|
||||||
|
redirect_stream = redirect_stdout
|
||||||
|
orig_stream = "stdout"
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedirectStderr(TestRedirectStream, unittest.TestCase):
|
||||||
|
|
||||||
|
redirect_stream = redirect_stderr
|
||||||
|
orig_stream = "stderr"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuppress(unittest.TestCase):
|
||||||
|
|
||||||
|
@requires_docstrings
|
||||||
|
def test_instance_docs(self):
|
||||||
|
# Issue 19330: ensure context manager instances have good docstrings
|
||||||
|
cm_docstring = suppress.__doc__
|
||||||
|
obj = suppress()
|
||||||
|
self.assertEqual(obj.__doc__, cm_docstring)
|
||||||
|
|
||||||
|
def test_no_result_from_enter(self):
|
||||||
|
with suppress(ValueError) as enter_result:
|
||||||
|
self.assertIsNone(enter_result)
|
||||||
|
|
||||||
|
def test_no_exception(self):
|
||||||
|
with suppress(ValueError):
|
||||||
|
self.assertEqual(pow(2, 5), 32)
|
||||||
|
|
||||||
|
def test_exact_exception(self):
|
||||||
|
with suppress(TypeError):
|
||||||
|
len(5)
|
||||||
|
|
||||||
|
def test_exception_hierarchy(self):
|
||||||
|
with suppress(LookupError):
|
||||||
|
'Hello'[50]
|
||||||
|
|
||||||
|
def test_other_exception(self):
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with suppress(TypeError):
|
||||||
|
1/0
|
||||||
|
|
||||||
|
def test_no_args(self):
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with suppress():
|
||||||
|
1/0
|
||||||
|
|
||||||
|
def test_multiple_exception_args(self):
|
||||||
|
with suppress(ZeroDivisionError, TypeError):
|
||||||
|
1/0
|
||||||
|
with suppress(ZeroDivisionError, TypeError):
|
||||||
|
len(5)
|
||||||
|
|
||||||
|
def test_cm_is_reentrant(self):
|
||||||
|
ignore_exceptions = suppress(Exception)
|
||||||
|
with ignore_exceptions:
|
||||||
|
pass
|
||||||
|
with ignore_exceptions:
|
||||||
|
len(5)
|
||||||
|
with ignore_exceptions:
|
||||||
|
with ignore_exceptions: # Check nested usage
|
||||||
|
len(5)
|
||||||
|
outer_continued = True
|
||||||
|
1/0
|
||||||
|
self.assertTrue(outer_continued)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import unittest
|
||||||
|
unittest.main()
|
||||||
27
tox.ini
27
tox.ini
|
|
@ -1,24 +1,11 @@
|
||||||
[tox]
|
[tox]
|
||||||
# Python 3.8 is the first version with positional-only argument syntax support
|
envlist = py27, pypy, py34, py35, pypy3
|
||||||
envlist = py{38,39,3.10,3.11,3.12,py3}
|
|
||||||
skip_missing_interpreters = True
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands = {envpython} test_contextlib2.py
|
||||||
coverage run -m unittest discover -t . -s test
|
|
||||||
coverage report
|
|
||||||
coverage xml
|
|
||||||
# mypy won't install on PyPy, so only run the typechecking on CPython
|
|
||||||
!pypy3: python -m mypy.stubtest --allowlist dev/mypy.allowlist contextlib2
|
|
||||||
deps =
|
|
||||||
coverage
|
|
||||||
!pypy3: mypy
|
|
||||||
|
|
||||||
[gh-actions]
|
[testenv:py27]
|
||||||
python =
|
deps = unittest2
|
||||||
3.8: py38
|
|
||||||
3.9: py39
|
[testenv:pypy]
|
||||||
3.10: py3.10
|
deps = unittest2
|
||||||
3.11: py3.11
|
|
||||||
3.12: py3.12
|
|
||||||
pypy-3.10: pypy3
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue