mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-03-19 15:10:24 +00:00
Compare commits
136 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a547a3247 | ||
|
|
e713c7cd64 | ||
|
|
0d2a7d056e | ||
|
|
15d711ad16 | ||
|
|
892462960d | ||
|
|
3fbca593ec | ||
|
|
f64cf04df8 | ||
|
|
8fe4d73971 | ||
|
|
7b862aaa80 | ||
|
|
defc103aae | ||
|
|
7e2a0a900a | ||
|
|
7c6b72683c | ||
|
|
4be95fed29 | ||
|
|
690922cfde | ||
|
|
c127f51df1 | ||
|
|
241de07097 | ||
|
|
ff30b3a68f | ||
|
|
9ef8594837 | ||
|
|
89893bba60 | ||
|
|
26ad9aef80 | ||
|
|
c1d9e6a4a7 | ||
|
|
d63f68b104 | ||
|
|
0828b5a332 | ||
|
|
1ea4e4dfe0 | ||
|
|
3feaef8f91 | ||
|
|
89c5189648 | ||
|
|
5373afc4ba | ||
|
|
3d2540dda1 | ||
|
|
d78420a808 | ||
|
|
a35252eefd | ||
|
|
16db3dcf86 | ||
|
|
2dc4c04492 | ||
|
|
908e2da622 | ||
|
|
c907a9975e | ||
|
|
94174bafeb | ||
|
|
fffe1d96be | ||
|
|
9a1453b673 | ||
|
|
190f6af5ef | ||
|
|
181b9c212f | ||
|
|
bed62be63b | ||
|
|
4b353b9815 | ||
|
|
0fb391a30c | ||
|
|
841d0ae73a | ||
|
|
3f6ee4d314 | ||
|
|
77e93832f0 | ||
|
|
72c47e4629 | ||
|
|
802688462c | ||
|
|
a858a8f4d6 | ||
|
|
46244c827d | ||
|
|
e42cd73fe9 | ||
|
|
b99ed09dfd | ||
|
|
032662d6e9 | ||
|
|
4b39470cbb | ||
|
|
94a3b86586 | ||
|
|
630f73a3a5 | ||
|
|
6a40a1ee80 | ||
|
|
cf0cece837 | ||
|
|
17b0a93e5b | ||
|
|
94f3881963 | ||
|
|
eed96067d7 | ||
|
|
cb66e76cd8 | ||
|
|
cef92f49f9 | ||
|
|
dbcddef29f | ||
|
|
093bc69490 | ||
|
|
a1ea615bb3 | ||
|
|
5f8498c19f | ||
|
|
34947502e5 | ||
|
|
07825fc9d9 | ||
|
|
ca6d76fb9f | ||
|
|
5eba33be65 | ||
|
|
90b643f5fb | ||
|
|
4a2baeceab | ||
|
|
f50a62ed77 | ||
|
|
403bb51102 | ||
|
|
cd80f6860d | ||
|
|
72bde0ee1d | ||
|
|
70cba0264a | ||
|
|
b06a025948 | ||
|
|
178007ad39 | ||
|
|
78ec3a1f4b | ||
|
|
f8a3439209 | ||
|
|
7eb119a73e | ||
|
|
bf72a830c2 | ||
|
|
603052bd0c | ||
|
|
cc63b02723 | ||
|
|
571c6f7a0f | ||
|
|
dd12dc65c2 | ||
|
|
717cb47d34 | ||
|
|
739bd253bd | ||
|
|
a8e6733e21 | ||
|
|
656a4eba29 | ||
|
|
57206f1181 | ||
|
|
dd2388f7e6 | ||
|
|
5a5d456af1 | ||
|
|
17cb926d56 | ||
|
|
977da10ab0 | ||
|
|
68069e372b | ||
|
|
3d2e7ab60e | ||
|
|
57eb33db7b | ||
|
|
8728cae63b | ||
|
|
166b07a463 | ||
|
|
44966d33d3 | ||
|
|
d533028739 | ||
|
|
31aa120bb8 | ||
|
|
2f4fd22e2f | ||
|
|
e4fbea45c6 | ||
|
|
1d0b33bbb9 | ||
|
|
4e8376e678 | ||
|
|
b12f399859 | ||
|
|
6482530d18 | ||
|
|
864625d870 | ||
|
|
6578b59179 | ||
|
|
6ff3657fdf | ||
|
|
8fa12aff40 | ||
|
|
0791b6a3f2 | ||
|
|
911b459522 | ||
|
|
c8efb9adcc | ||
|
|
738954b2a3 | ||
|
|
184eaa1240 | ||
|
|
1fb0cf99cb | ||
|
|
0433cd4844 | ||
|
|
f84ad22549 | ||
|
|
01069f94d2 | ||
|
|
8b856b8b51 | ||
|
|
f41d813441 | ||
|
|
39b2081953 | ||
|
|
af360370a8 | ||
|
|
943e50a52d | ||
|
|
c8b517d807 | ||
|
|
b93a40d958 | ||
|
|
e48fbea5a5 | ||
|
|
20cdf6eda5 | ||
|
|
4469999858 | ||
|
|
deed672a89 | ||
|
|
15485962d1 | ||
|
|
d6aabae9db |
43 changed files with 6098 additions and 2007 deletions
6
.coveragerc
Normal file
6
.coveragerc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[run]
|
||||||
|
source = contextlib2
|
||||||
|
branch = 1
|
||||||
|
|
||||||
|
[report]
|
||||||
|
omit = *test*
|
||||||
53
.github/workflows/release.yml
vendored
Normal file
53
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
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
Normal file
56
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
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
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
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
18
.hgignore
|
|
@ -1,18 +0,0 @@
|
||||||
syntax: glob
|
|
||||||
*.swp
|
|
||||||
*.kate-swp
|
|
||||||
*.o
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
*.cover
|
|
||||||
*~
|
|
||||||
.tox/
|
|
||||||
__pycache__
|
|
||||||
.coverage
|
|
||||||
coverage/
|
|
||||||
htmlcov/
|
|
||||||
_build/
|
|
||||||
dist/
|
|
||||||
MANIFEST
|
|
||||||
.tox
|
|
||||||
14
.pre-commit-config.yaml
Normal file
14
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
|
rev: v1.10.0
|
||||||
|
hooks:
|
||||||
|
- id: python-check-blanket-noqa
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-yaml
|
||||||
|
|
||||||
|
ci:
|
||||||
|
autoupdate_schedule: quarterly
|
||||||
35
.readthedocs.yaml
Normal file
35
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Read the Docs configuration file for Sphinx projects
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the OS, Python version and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.12"
|
||||||
|
# You can also specify other tool versions:
|
||||||
|
# nodejs: "20"
|
||||||
|
# rust: "1.70"
|
||||||
|
# golang: "1.20"
|
||||||
|
|
||||||
|
# Build documentation in the "docs/" directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
||||||
|
# builder: "dirhtml"
|
||||||
|
# Fail on all warnings to avoid broken references
|
||||||
|
# fail_on_warning: true
|
||||||
|
|
||||||
|
# Optionally build your docs in additional formats such as PDF and ePub
|
||||||
|
# formats:
|
||||||
|
# - pdf
|
||||||
|
# - epub
|
||||||
|
|
||||||
|
# Optional but recommended, declare the Python requirements required
|
||||||
|
# to build your documentation
|
||||||
|
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
As contributors and maintainers of the Jazzband projects, and in the interest of
|
||||||
|
fostering an open and welcoming community, we pledge to respect all people who
|
||||||
|
contribute through reporting issues, posting feature requests, updating documentation,
|
||||||
|
submitting pull requests or patches, and other activities.
|
||||||
|
|
||||||
|
We are committed to making participation in the Jazzband a harassment-free experience
|
||||||
|
for everyone, regardless of the level of experience, gender, gender identity and
|
||||||
|
expression, sexual orientation, disability, personal appearance, body size, race,
|
||||||
|
ethnicity, age, religion, or nationality.
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery
|
||||||
|
- Personal attacks
|
||||||
|
- Trolling or insulting/derogatory comments
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing other's private information, such as physical or electronic addresses,
|
||||||
|
without explicit permission
|
||||||
|
- Other unethical or unprofessional conduct
|
||||||
|
|
||||||
|
The Jazzband roadies have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are not
|
||||||
|
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
|
||||||
|
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
By adopting this Code of Conduct, the roadies commit themselves to fairly and
|
||||||
|
consistently applying these principles to every aspect of managing the jazzband
|
||||||
|
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
|
||||||
|
removed from the Jazzband roadies.
|
||||||
|
|
||||||
|
This code of conduct applies both within project spaces and in public spaces when an
|
||||||
|
individual is representing the project or its community.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||||
|
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
|
||||||
|
investigated and will result in a response that is deemed necessary and appropriate to
|
||||||
|
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
|
||||||
|
reporter of an incident.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
|
||||||
|
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
|
||||||
|
|
||||||
|
[homepage]: https://contributor-covenant.org
|
||||||
|
[version]: https://contributor-covenant.org/version/1/3/0/
|
||||||
5
CONTRIBUTING.md
Normal file
5
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[](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,4 +1,6 @@
|
||||||
|
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,3 +1,5 @@
|
||||||
include *.py *.txt *.rst *.md
|
include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in
|
||||||
|
recursive-include contextlib2 *.py *.pyi py.typed
|
||||||
recursive-include docs *.rst *.py make.bat Makefile
|
recursive-include docs *.rst *.py make.bat Makefile
|
||||||
|
recursive-include test *.py
|
||||||
|
recursive-include dev *.patch *.allowlist *.sh
|
||||||
|
|
|
||||||
177
NEWS.rst
177
NEWS.rst
|
|
@ -1,8 +1,148 @@
|
||||||
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
|
||||||
|
|
@ -13,31 +153,36 @@ 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)
|
||||||
~~~~~~~~~~~~~~~~~~
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
* Issue #7: Add MANIFEST.in so PyPI package contains all relevant files
|
* (BitBucket) Issue #7: Add MANIFEST.in so PyPI package contains all relevant
|
||||||
|
files (patch contributed by Doug Latornell)
|
||||||
|
|
||||||
|
|
||||||
0.3 (2012-01-04)
|
0.3 (2012-01-04)
|
||||||
~~~~~~~~~~~~~~~~
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
* Issue #5: ContextStack.register no longer pointlessly returns the wrapped
|
* (BitBucket) Issue #5: ContextStack.register no longer pointlessly returns the
|
||||||
function
|
wrapped function
|
||||||
* Issue #2: Add examples and recipes section to docs
|
* (BitBucket) Issue #2: Add examples and recipes section to docs
|
||||||
* Issue #3: ContextStack.register_exit() now accepts objects with __exit__
|
* (BitBucket) Issue #3: ContextStack.register_exit() now accepts objects with
|
||||||
attributes in addition to accepting exit callbacks directly
|
__exit__ attributes in addition to accepting exit callbacks directly
|
||||||
* Issue #1: Add ContextStack.preserve() to move all registered callbacks to
|
* (BitBucket) Issue #1: Add ContextStack.preserve() to move all registered
|
||||||
a new ContextStack object
|
callbacks to 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)
|
||||||
|
|
@ -45,14 +190,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
40
README.md
|
|
@ -1,40 +0,0 @@
|
||||||
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
Normal file
102
README.rst
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
.. 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 @@
|
||||||
0.5.0
|
24.6.0rc1
|
||||||
|
|
|
||||||
419
contextlib2.py
419
contextlib2.py
|
|
@ -1,419 +0,0 @@
|
||||||
"""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()
|
|
||||||
876
contextlib2/__init__.py
Normal file
876
contextlib2/__init__.py
Normal file
|
|
@ -0,0 +1,876 @@
|
||||||
|
"""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()
|
||||||
201
contextlib2/__init__.pyi
Normal file
201
contextlib2/__init__.pyi
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
# 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: ...
|
||||||
0
contextlib2/py.typed
Normal file
0
contextlib2/py.typed
Normal file
10
dev/mypy.allowlist
Normal file
10
dev/mypy.allowlist
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# 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__
|
||||||
116
dev/py3_12_py_to_contextlib2.patch
Normal file
116
dev/py3_12_py_to_contextlib2.patch
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/contextlib.py 2024-05-23 11:57:09.210023505 +1000
|
||||||
|
+++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.py 2024-05-23 17:05:06.549142813 +1000
|
||||||
|
@@ -5,7 +5,46 @@
|
||||||
|
import _collections_abc
|
||||||
|
from collections import deque
|
||||||
|
from functools import wraps
|
||||||
|
-from types import MethodType, GenericAlias
|
||||||
|
+from types import MethodType
|
||||||
|
+
|
||||||
|
+# Python 3.8 compatibility: GenericAlias may not be defined
|
||||||
|
+try:
|
||||||
|
+ from types import GenericAlias
|
||||||
|
+except ImportError:
|
||||||
|
+ # If the real GenericAlias type doesn't exist, __class_getitem__ won't be used,
|
||||||
|
+ # so the fallback placeholder doesn't need to provide any meaningful behaviour
|
||||||
|
+ # (typecheckers may still be unhappy, but for that problem the answer is
|
||||||
|
+ # "use a newer Python version with better typechecking support")
|
||||||
|
+ class GenericAlias:
|
||||||
|
+ pass
|
||||||
|
+
|
||||||
|
+# Python 3.10 and earlier compatibility: BaseExceptionGroup may not be defined
|
||||||
|
+try:
|
||||||
|
+ BaseExceptionGroup
|
||||||
|
+except NameError:
|
||||||
|
+ # If the real BaseExceptionGroup type doesn't exist, it will never actually
|
||||||
|
+ # be raised. This means the fallback placeholder doesn't need to provide
|
||||||
|
+ # any meaningful behaviour, it just needs to be compatible with 'issubclass'
|
||||||
|
+ class BaseExceptionGroup(BaseException):
|
||||||
|
+ pass
|
||||||
|
+
|
||||||
|
+# Python 3.9 and earlier compatibility: anext may not be defined
|
||||||
|
+try:
|
||||||
|
+ anext
|
||||||
|
+except NameError:
|
||||||
|
+ def anext(obj, /):
|
||||||
|
+ return obj.__anext__()
|
||||||
|
+
|
||||||
|
+# Python 3.11+ behaviour consistency: replace AttributeError with TypeError
|
||||||
|
+if sys.version_info >= (3, 11):
|
||||||
|
+ # enter_context() and enter_async_context() follow the change in the
|
||||||
|
+ # exception type raised by with statements and async with statements
|
||||||
|
+ _CL2_ERROR_TO_CONVERT = AttributeError
|
||||||
|
+else:
|
||||||
|
+ # On older versions, raise AttributeError without any changes
|
||||||
|
+ class _CL2_ERROR_TO_CONVERT(Exception):
|
||||||
|
+ pass
|
||||||
|
+
|
||||||
|
|
||||||
|
__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
|
||||||
|
"AbstractContextManager", "AbstractAsyncContextManager",
|
||||||
|
@@ -62,6 +101,24 @@
|
||||||
|
class ContextDecorator(object):
|
||||||
|
"A base class or mixin that enables context managers to work as decorators."
|
||||||
|
|
||||||
|
+ def refresh_cm(self):
|
||||||
|
+ """Returns the context manager used to actually wrap the call to the
|
||||||
|
+ decorated function.
|
||||||
|
+
|
||||||
|
+ The default implementation just returns *self*.
|
||||||
|
+
|
||||||
|
+ Overriding this method allows otherwise one-shot context managers
|
||||||
|
+ like _GeneratorContextManager to support use as decorators via
|
||||||
|
+ implicit recreation.
|
||||||
|
+
|
||||||
|
+ DEPRECATED: refresh_cm was never added to the standard library's
|
||||||
|
+ ContextDecorator API
|
||||||
|
+ """
|
||||||
|
+ import warnings # Only import if needed for the deprecation warning
|
||||||
|
+ warnings.warn("refresh_cm was never added to the standard library",
|
||||||
|
+ DeprecationWarning)
|
||||||
|
+ return self._recreate_cm()
|
||||||
|
+
|
||||||
|
def _recreate_cm(self):
|
||||||
|
"""Return a recreated instance of self.
|
||||||
|
|
||||||
|
@@ -520,7 +577,7 @@
|
||||||
|
try:
|
||||||
|
_enter = cls.__enter__
|
||||||
|
_exit = cls.__exit__
|
||||||
|
- except AttributeError:
|
||||||
|
+ except _CL2_ERROR_TO_CONVERT:
|
||||||
|
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
|
||||||
|
f"not support the context manager protocol") from None
|
||||||
|
result = _enter(cm)
|
||||||
|
@@ -652,7 +709,7 @@
|
||||||
|
try:
|
||||||
|
_enter = cls.__aenter__
|
||||||
|
_exit = cls.__aexit__
|
||||||
|
- except AttributeError:
|
||||||
|
+ except _CL2_ERROR_TO_CONVERT:
|
||||||
|
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
|
||||||
|
f"not support the asynchronous context manager protocol"
|
||||||
|
) from None
|
||||||
|
@@ -798,3 +855,22 @@
|
||||||
|
|
||||||
|
def __exit__(self, *excinfo):
|
||||||
|
os.chdir(self._old_cwd.pop())
|
||||||
|
+
|
||||||
|
+# Preserve backwards compatibility
|
||||||
|
+class ContextStack(ExitStack):
|
||||||
|
+ """(DEPRECATED) Backwards compatibility alias for ExitStack"""
|
||||||
|
+
|
||||||
|
+ def __init__(self):
|
||||||
|
+ import warnings # Only import if needed for the deprecation warning
|
||||||
|
+ warnings.warn("ContextStack has been renamed to ExitStack",
|
||||||
|
+ DeprecationWarning)
|
||||||
|
+ super(ContextStack, self).__init__()
|
||||||
|
+
|
||||||
|
+ def register_exit(self, callback):
|
||||||
|
+ return self.push(callback)
|
||||||
|
+
|
||||||
|
+ def register(self, callback, *args, **kwds):
|
||||||
|
+ return self.callback(callback, *args, **kwds)
|
||||||
|
+
|
||||||
|
+ def preserve(self):
|
||||||
|
+ return self.pop_all()
|
||||||
112
dev/py3_12_pyi_to_contextlib2.patch
Normal file
112
dev/py3_12_pyi_to_contextlib2.patch
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
--- /home/ncoghlan/devel/contextlib2/dev/typeshed_contextlib.pyi 2024-05-23 12:40:10.170754997 +1000
|
||||||
|
+++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.pyi 2024-05-23 16:47:15.874656809 +1000
|
||||||
|
@@ -1,3 +1,20 @@
|
||||||
|
+# Type hints copied from the typeshed project under the Apache License 2.0
|
||||||
|
+# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE
|
||||||
|
+
|
||||||
|
+# For updates: https://github.com/python/typeshed/blob/main/stdlib/contextlib.pyi
|
||||||
|
+
|
||||||
|
+# Last updated: 2024-05-22
|
||||||
|
+# Updated from: https://github.com/python/typeshed/blob/aa2d33df211e1e4f70883388febf750ac524d2bb/stdlib/contextlib.pyi
|
||||||
|
+# Saved to: dev/typeshed_contextlib.pyi
|
||||||
|
+
|
||||||
|
+# contextlib2 API adaptation notes:
|
||||||
|
+# * the various 'if True:' guards replace sys.version checks in the original
|
||||||
|
+# typeshed file (those APIs are available on all supported versions)
|
||||||
|
+# * any commented out 'if True:' guards replace sys.version checks in the original
|
||||||
|
+# typeshed file where the affected APIs haven't been backported yet
|
||||||
|
+# * deliberately omitted APIs are listed in `dev/mypy.allowlist`
|
||||||
|
+# (e.g. deprecated experimental APIs that never graduated to the stdlib)
|
||||||
|
+
|
||||||
|
import abc
|
||||||
|
import sys
|
||||||
|
from _typeshed import FileDescriptorOrPath, Unused
|
||||||
|
@@ -22,10 +39,10 @@
|
||||||
|
"nullcontext",
|
||||||
|
]
|
||||||
|
|
||||||
|
-if sys.version_info >= (3, 10):
|
||||||
|
+if True:
|
||||||
|
__all__ += ["aclosing"]
|
||||||
|
|
||||||
|
-if sys.version_info >= (3, 11):
|
||||||
|
+if True:
|
||||||
|
__all__ += ["chdir"]
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
@@ -65,18 +82,14 @@
|
||||||
|
func: Callable[..., Generator[_T_co, Any, Any]]
|
||||||
|
args: tuple[Any, ...]
|
||||||
|
kwds: dict[str, Any]
|
||||||
|
- if sys.version_info >= (3, 9):
|
||||||
|
+ if True:
|
||||||
|
def __exit__(
|
||||||
|
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
) -> bool | None: ...
|
||||||
|
- else:
|
||||||
|
- def __exit__(
|
||||||
|
- self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
- ) -> bool | None: ...
|
||||||
|
|
||||||
|
def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ...
|
||||||
|
|
||||||
|
-if sys.version_info >= (3, 10):
|
||||||
|
+if True:
|
||||||
|
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
|
||||||
|
|
||||||
|
class AsyncContextDecorator:
|
||||||
|
@@ -94,17 +107,6 @@
|
||||||
|
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
) -> bool | None: ...
|
||||||
|
|
||||||
|
-else:
|
||||||
|
- class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]):
|
||||||
|
- def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
|
||||||
|
- gen: AsyncGenerator[_T_co, Any]
|
||||||
|
- func: Callable[..., AsyncGenerator[_T_co, Any]]
|
||||||
|
- args: tuple[Any, ...]
|
||||||
|
- kwds: dict[str, Any]
|
||||||
|
- async def __aexit__(
|
||||||
|
- self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
- ) -> bool | None: ...
|
||||||
|
-
|
||||||
|
def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ...
|
||||||
|
|
||||||
|
class _SupportsClose(Protocol):
|
||||||
|
@@ -116,7 +118,7 @@
|
||||||
|
def __init__(self, thing: _SupportsCloseT) -> None: ...
|
||||||
|
def __exit__(self, *exc_info: Unused) -> None: ...
|
||||||
|
|
||||||
|
-if sys.version_info >= (3, 10):
|
||||||
|
+if True:
|
||||||
|
class _SupportsAclose(Protocol):
|
||||||
|
def aclose(self) -> Awaitable[object]: ...
|
||||||
|
|
||||||
|
@@ -177,7 +179,7 @@
|
||||||
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
|
||||||
|
) -> bool: ...
|
||||||
|
|
||||||
|
-if sys.version_info >= (3, 10):
|
||||||
|
+if True:
|
||||||
|
class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]):
|
||||||
|
enter_result: _T
|
||||||
|
@overload
|
||||||
|
@@ -189,17 +191,7 @@
|
||||||
|
async def __aenter__(self) -> _T: ...
|
||||||
|
async def __aexit__(self, *exctype: Unused) -> None: ...
|
||||||
|
|
||||||
|
-else:
|
||||||
|
- class nullcontext(AbstractContextManager[_T, None]):
|
||||||
|
- enter_result: _T
|
||||||
|
- @overload
|
||||||
|
- def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
|
||||||
|
- @overload
|
||||||
|
- def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
|
||||||
|
- def __enter__(self) -> _T: ...
|
||||||
|
- def __exit__(self, *exctype: Unused) -> None: ...
|
||||||
|
-
|
||||||
|
-if sys.version_info >= (3, 11):
|
||||||
|
+if True:
|
||||||
|
_T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath)
|
||||||
|
|
||||||
|
class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]):
|
||||||
453
dev/py3_12_rst_to_contextlib2.patch
Normal file
453
dev/py3_12_rst_to_contextlib2.patch
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
--- /home/ncoghlan/devel/contextlib2/../cpython/Doc/library/contextlib.rst 2024-05-20 12:53:59.936907756 +1000
|
||||||
|
+++ /home/ncoghlan/devel/contextlib2/docs/contextlib2.rst 2024-05-23 17:39:52.671083724 +1000
|
||||||
|
@@ -1,20 +1,5 @@
|
||||||
|
-:mod:`!contextlib` --- Utilities for :keyword:`!with`\ -statement contexts
|
||||||
|
-==========================================================================
|
||||||
|
-
|
||||||
|
-.. module:: contextlib
|
||||||
|
- :synopsis: Utilities for with-statement contexts.
|
||||||
|
-
|
||||||
|
-**Source code:** :source:`Lib/contextlib.py`
|
||||||
|
-
|
||||||
|
---------------
|
||||||
|
-
|
||||||
|
-This module provides utilities for common tasks involving the :keyword:`with`
|
||||||
|
-statement. For more information see also :ref:`typecontextmanager` and
|
||||||
|
-:ref:`context-managers`.
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-Utilities
|
||||||
|
----------
|
||||||
|
+API Reference
|
||||||
|
+-------------
|
||||||
|
|
||||||
|
Functions and classes provided:
|
||||||
|
|
||||||
|
@@ -26,8 +11,8 @@
|
||||||
|
``self`` while :meth:`object.__exit__` is an abstract method which by default
|
||||||
|
returns ``None``. See also the definition of :ref:`typecontextmanager`.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.6
|
||||||
|
-
|
||||||
|
+ .. versionadded:: 0.6.0
|
||||||
|
+ Part of the standard library in Python 3.6 and later
|
||||||
|
|
||||||
|
.. class:: AbstractAsyncContextManager
|
||||||
|
|
||||||
|
@@ -38,8 +23,8 @@
|
||||||
|
returns ``None``. See also the definition of
|
||||||
|
:ref:`async-context-managers`.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.7
|
||||||
|
-
|
||||||
|
+ .. versionadded:: 21.6.0
|
||||||
|
+ Part of the standard library in Python 3.7 and later
|
||||||
|
|
||||||
|
.. decorator:: contextmanager
|
||||||
|
|
||||||
|
@@ -49,12 +34,12 @@
|
||||||
|
|
||||||
|
While many objects natively support use in with statements, sometimes a
|
||||||
|
resource needs to be managed that isn't a context manager in its own right,
|
||||||
|
- and doesn't implement a ``close()`` method for use with ``contextlib.closing``
|
||||||
|
+ and doesn't implement a ``close()`` method for use with ``contextlib2.closing``
|
||||||
|
|
||||||
|
An abstract example would be the following to ensure correct resource
|
||||||
|
management::
|
||||||
|
|
||||||
|
- from contextlib import contextmanager
|
||||||
|
+ from contextlib2 import contextmanager
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def managed_resource(*args, **kwds):
|
||||||
|
@@ -95,13 +80,10 @@
|
||||||
|
created by :func:`contextmanager` to meet the requirement that context
|
||||||
|
managers support multiple invocations in order to be used as decorators).
|
||||||
|
|
||||||
|
- .. versionchanged:: 3.2
|
||||||
|
- Use of :class:`ContextDecorator`.
|
||||||
|
-
|
||||||
|
|
||||||
|
.. decorator:: asynccontextmanager
|
||||||
|
|
||||||
|
- Similar to :func:`~contextlib.contextmanager`, but creates an
|
||||||
|
+ Similar to :func:`~contextlib2.contextmanager`, but creates an
|
||||||
|
:ref:`asynchronous context manager <async-context-managers>`.
|
||||||
|
|
||||||
|
This function is a :term:`decorator` that can be used to define a factory
|
||||||
|
@@ -112,7 +94,7 @@
|
||||||
|
|
||||||
|
A simple example::
|
||||||
|
|
||||||
|
- from contextlib import asynccontextmanager
|
||||||
|
+ from contextlib2 import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def get_connection():
|
||||||
|
@@ -126,13 +108,16 @@
|
||||||
|
async with get_connection() as conn:
|
||||||
|
return conn.query('SELECT ...')
|
||||||
|
|
||||||
|
- .. versionadded:: 3.7
|
||||||
|
+ .. versionadded:: 21.6.0
|
||||||
|
+ Part of the standard library in Python 3.7 and later, enhanced in
|
||||||
|
+ Python 3.10 and later to allow created async context managers to be used
|
||||||
|
+ as async function decorators.
|
||||||
|
|
||||||
|
Context managers defined with :func:`asynccontextmanager` can be used
|
||||||
|
either as decorators or with :keyword:`async with` statements::
|
||||||
|
|
||||||
|
import time
|
||||||
|
- from contextlib import asynccontextmanager
|
||||||
|
+ from contextlib2 import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def timeit():
|
||||||
|
@@ -151,17 +136,13 @@
|
||||||
|
created by :func:`asynccontextmanager` to meet the requirement that context
|
||||||
|
managers support multiple invocations in order to be used as decorators.
|
||||||
|
|
||||||
|
- .. versionchanged:: 3.10
|
||||||
|
- Async context managers created with :func:`asynccontextmanager` can
|
||||||
|
- be used as decorators.
|
||||||
|
-
|
||||||
|
|
||||||
|
.. function:: closing(thing)
|
||||||
|
|
||||||
|
Return a context manager that closes *thing* upon completion of the block. This
|
||||||
|
is basically equivalent to::
|
||||||
|
|
||||||
|
- from contextlib import contextmanager
|
||||||
|
+ from contextlib2 import contextmanager
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def closing(thing):
|
||||||
|
@@ -172,7 +153,7 @@
|
||||||
|
|
||||||
|
And lets you write code like this::
|
||||||
|
|
||||||
|
- from contextlib import closing
|
||||||
|
+ from contextlib2 import closing
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
with closing(urlopen('https://www.python.org')) as page:
|
||||||
|
@@ -196,7 +177,7 @@
|
||||||
|
Return an async context manager that calls the ``aclose()`` method of *thing*
|
||||||
|
upon completion of the block. This is basically equivalent to::
|
||||||
|
|
||||||
|
- from contextlib import asynccontextmanager
|
||||||
|
+ from contextlib2 import asynccontextmanager
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def aclosing(thing):
|
||||||
|
@@ -209,7 +190,7 @@
|
||||||
|
generators when they happen to exit early by :keyword:`break` or an
|
||||||
|
exception. For example::
|
||||||
|
|
||||||
|
- from contextlib import aclosing
|
||||||
|
+ from contextlib2 import aclosing
|
||||||
|
|
||||||
|
async with aclosing(my_generator()) as values:
|
||||||
|
async for value in values:
|
||||||
|
@@ -221,7 +202,8 @@
|
||||||
|
variables work as expected, and the exit code isn't run after the
|
||||||
|
lifetime of some task it depends on).
|
||||||
|
|
||||||
|
- .. versionadded:: 3.10
|
||||||
|
+ .. versionadded:: 21.6.0
|
||||||
|
+ Part of the standard library in Python 3.10 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. _simplifying-support-for-single-optional-context-managers:
|
||||||
|
@@ -235,10 +217,10 @@
|
||||||
|
def myfunction(arg, ignore_exceptions=False):
|
||||||
|
if ignore_exceptions:
|
||||||
|
# Use suppress to ignore all exceptions.
|
||||||
|
- cm = contextlib.suppress(Exception)
|
||||||
|
+ cm = contextlib2.suppress(Exception)
|
||||||
|
else:
|
||||||
|
# Do not ignore any exceptions, cm has no effect.
|
||||||
|
- cm = contextlib.nullcontext()
|
||||||
|
+ cm = contextlib2.nullcontext()
|
||||||
|
with cm:
|
||||||
|
# Do something
|
||||||
|
|
||||||
|
@@ -269,11 +251,11 @@
|
||||||
|
async with cm as session:
|
||||||
|
# Send http requests with session
|
||||||
|
|
||||||
|
- .. versionadded:: 3.7
|
||||||
|
-
|
||||||
|
- .. versionchanged:: 3.10
|
||||||
|
- :term:`asynchronous context manager` support was added.
|
||||||
|
+ .. versionadded:: 0.6.0
|
||||||
|
+ Part of the standard library in Python 3.7 and later
|
||||||
|
|
||||||
|
+ .. versionchanged:: 21.6.0
|
||||||
|
+ Updated to Python 3.10 version with :term:`asynchronous context manager` support
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: suppress(*exceptions)
|
||||||
|
@@ -290,7 +272,7 @@
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
- from contextlib import suppress
|
||||||
|
+ from contextlib2 import suppress
|
||||||
|
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.remove('somefile.tmp')
|
||||||
|
@@ -314,13 +296,15 @@
|
||||||
|
|
||||||
|
If the code within the :keyword:`!with` block raises a
|
||||||
|
:exc:`BaseExceptionGroup`, suppressed exceptions are removed from the
|
||||||
|
- group. If any exceptions in the group are not suppressed, a group containing them is re-raised.
|
||||||
|
+ group. If any exceptions in the group are not suppressed, a group containing
|
||||||
|
+ them is re-raised.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.4
|
||||||
|
+ .. versionadded:: 0.5
|
||||||
|
+ Part of the standard library in Python 3.4 and later
|
||||||
|
|
||||||
|
- .. versionchanged:: 3.12
|
||||||
|
- ``suppress`` now supports suppressing exceptions raised as
|
||||||
|
- part of an :exc:`BaseExceptionGroup`.
|
||||||
|
+ .. versionchanged:: 24.6.0
|
||||||
|
+ Updated to Python 3.12 version that supports suppressing exceptions raised
|
||||||
|
+ as part of a :exc:`BaseExceptionGroup`.
|
||||||
|
|
||||||
|
.. function:: redirect_stdout(new_target)
|
||||||
|
|
||||||
|
@@ -359,17 +343,19 @@
|
||||||
|
|
||||||
|
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.4
|
||||||
|
+ .. versionadded:: 0.5
|
||||||
|
+ Part of the standard library in Python 3.4 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: redirect_stderr(new_target)
|
||||||
|
|
||||||
|
- Similar to :func:`~contextlib.redirect_stdout` but redirecting
|
||||||
|
+ Similar to :func:`~contextlib2.redirect_stdout` but redirecting
|
||||||
|
:data:`sys.stderr` to another file or file-like object.
|
||||||
|
|
||||||
|
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.5
|
||||||
|
+ .. versionadded:: 0.5
|
||||||
|
+ Part of the standard library in Python 3.5 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. function:: chdir(path)
|
||||||
|
@@ -386,7 +372,8 @@
|
||||||
|
|
||||||
|
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.11
|
||||||
|
+ .. versionadded:: 24.6.0
|
||||||
|
+ Part of the standard library in Python 3.11 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: ContextDecorator()
|
||||||
|
@@ -402,7 +389,7 @@
|
||||||
|
|
||||||
|
Example of ``ContextDecorator``::
|
||||||
|
|
||||||
|
- from contextlib import ContextDecorator
|
||||||
|
+ from contextlib2 import ContextDecorator
|
||||||
|
|
||||||
|
class mycontext(ContextDecorator):
|
||||||
|
def __enter__(self):
|
||||||
|
@@ -449,7 +436,7 @@
|
||||||
|
Existing context managers that already have a base class can be extended by
|
||||||
|
using ``ContextDecorator`` as a mixin class::
|
||||||
|
|
||||||
|
- from contextlib import ContextDecorator
|
||||||
|
+ from contextlib2 import ContextDecorator
|
||||||
|
|
||||||
|
class mycontext(ContextBaseClass, ContextDecorator):
|
||||||
|
def __enter__(self):
|
||||||
|
@@ -464,8 +451,6 @@
|
||||||
|
statements. If this is not the case, then the original construct with the
|
||||||
|
explicit :keyword:`!with` statement inside the function should be used.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.2
|
||||||
|
-
|
||||||
|
|
||||||
|
.. class:: AsyncContextDecorator
|
||||||
|
|
||||||
|
@@ -474,7 +459,7 @@
|
||||||
|
Example of ``AsyncContextDecorator``::
|
||||||
|
|
||||||
|
from asyncio import run
|
||||||
|
- from contextlib import AsyncContextDecorator
|
||||||
|
+ from contextlib2 import AsyncContextDecorator
|
||||||
|
|
||||||
|
class mycontext(AsyncContextDecorator):
|
||||||
|
async def __aenter__(self):
|
||||||
|
@@ -505,7 +490,8 @@
|
||||||
|
The bit in the middle
|
||||||
|
Finishing
|
||||||
|
|
||||||
|
- .. versionadded:: 3.10
|
||||||
|
+ .. versionadded:: 21.6.0
|
||||||
|
+ Part of the standard library in Python 3.10 and later
|
||||||
|
|
||||||
|
|
||||||
|
.. class:: ExitStack()
|
||||||
|
@@ -547,7 +533,8 @@
|
||||||
|
foundation for higher level context managers that manipulate the exit
|
||||||
|
stack in application specific ways.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.3
|
||||||
|
+ .. versionadded:: 0.4
|
||||||
|
+ Part of the standard library in Python 3.3 and later
|
||||||
|
|
||||||
|
.. method:: enter_context(cm)
|
||||||
|
|
||||||
|
@@ -558,9 +545,10 @@
|
||||||
|
These context managers may suppress exceptions just as they normally
|
||||||
|
would if used directly as part of a :keyword:`with` statement.
|
||||||
|
|
||||||
|
- .. versionchanged:: 3.11
|
||||||
|
- Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
|
||||||
|
- is not a context manager.
|
||||||
|
+ .. versionchanged:: 24.6.0
|
||||||
|
+ When running on Python 3.11 or later, raises :exc:`TypeError` instead
|
||||||
|
+ of :exc:`AttributeError` if *cm* is not a context manager. This aligns
|
||||||
|
+ with the behaviour of :keyword:`with` statements in Python 3.11+.
|
||||||
|
|
||||||
|
.. method:: push(exit)
|
||||||
|
|
||||||
|
@@ -627,14 +615,16 @@
|
||||||
|
The :meth:`~ExitStack.close` method is not implemented; :meth:`aclose` must be used
|
||||||
|
instead.
|
||||||
|
|
||||||
|
- .. coroutinemethod:: enter_async_context(cm)
|
||||||
|
+ .. method:: enter_async_context(cm)
|
||||||
|
+ :async:
|
||||||
|
|
||||||
|
Similar to :meth:`ExitStack.enter_context` but expects an asynchronous context
|
||||||
|
manager.
|
||||||
|
|
||||||
|
- .. versionchanged:: 3.11
|
||||||
|
- Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
|
||||||
|
- is not an asynchronous context manager.
|
||||||
|
+ .. versionchanged:: 24.6.0
|
||||||
|
+ When running on Python 3.11 or later, raises :exc:`TypeError` instead
|
||||||
|
+ of :exc:`AttributeError` if *cm* is not an asynchronous context manager.
|
||||||
|
+ This aligns with the behaviour of ``async with`` statements in Python 3.11+.
|
||||||
|
|
||||||
|
.. method:: push_async_exit(exit)
|
||||||
|
|
||||||
|
@@ -645,7 +635,8 @@
|
||||||
|
|
||||||
|
Similar to :meth:`ExitStack.callback` but expects a coroutine function.
|
||||||
|
|
||||||
|
- .. coroutinemethod:: aclose()
|
||||||
|
+ .. method:: aclose()
|
||||||
|
+ :async:
|
||||||
|
|
||||||
|
Similar to :meth:`ExitStack.close` but properly handles awaitables.
|
||||||
|
|
||||||
|
@@ -658,13 +649,15 @@
|
||||||
|
# the async with statement, even if attempts to open a connection
|
||||||
|
# later in the list raise an exception.
|
||||||
|
|
||||||
|
- .. versionadded:: 3.7
|
||||||
|
+ .. versionadded:: 21.6.0
|
||||||
|
+ Part of the standard library in Python 3.7 and later
|
||||||
|
+
|
||||||
|
|
||||||
|
Examples and Recipes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
This section describes some examples and recipes for making effective use of
|
||||||
|
-the tools provided by :mod:`contextlib`.
|
||||||
|
+the tools provided by :mod:`contextlib2`.
|
||||||
|
|
||||||
|
|
||||||
|
Supporting a variable number of context managers
|
||||||
|
@@ -728,7 +721,7 @@
|
||||||
|
acquisition and release functions, along with an optional validation function,
|
||||||
|
and maps them to the context management protocol::
|
||||||
|
|
||||||
|
- from contextlib import contextmanager, AbstractContextManager, ExitStack
|
||||||
|
+ from contextlib2 import contextmanager, AbstractContextManager, ExitStack
|
||||||
|
|
||||||
|
class ResourceManager(AbstractContextManager):
|
||||||
|
|
||||||
|
@@ -788,7 +781,7 @@
|
||||||
|
execution at the end of a ``with`` statement, and then later decide to skip
|
||||||
|
executing that callback::
|
||||||
|
|
||||||
|
- from contextlib import ExitStack
|
||||||
|
+ from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
stack.callback(cleanup_resources)
|
||||||
|
@@ -802,7 +795,7 @@
|
||||||
|
If a particular application uses this pattern a lot, it can be simplified
|
||||||
|
even further by means of a small helper class::
|
||||||
|
|
||||||
|
- from contextlib import ExitStack
|
||||||
|
+ from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
class Callback(ExitStack):
|
||||||
|
def __init__(self, callback, /, *args, **kwds):
|
||||||
|
@@ -822,7 +815,7 @@
|
||||||
|
:meth:`ExitStack.callback` to declare the resource cleanup in
|
||||||
|
advance::
|
||||||
|
|
||||||
|
- from contextlib import ExitStack
|
||||||
|
+ from contextlib2 import ExitStack
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
@stack.callback
|
||||||
|
@@ -849,7 +842,7 @@
|
||||||
|
inheriting from :class:`ContextDecorator` provides both capabilities in a
|
||||||
|
single definition::
|
||||||
|
|
||||||
|
- from contextlib import ContextDecorator
|
||||||
|
+ from contextlib2 import ContextDecorator
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
@@ -911,7 +904,7 @@
|
||||||
|
context managers, and will complain about the underlying generator failing
|
||||||
|
to yield if an attempt is made to use them a second time::
|
||||||
|
|
||||||
|
- >>> from contextlib import contextmanager
|
||||||
|
+ >>> from contextlib2 import contextmanager
|
||||||
|
>>> @contextmanager
|
||||||
|
... def singleuse():
|
||||||
|
... print("Before")
|
||||||
|
@@ -946,7 +939,7 @@
|
||||||
|
:func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very
|
||||||
|
simple example of reentrant use::
|
||||||
|
|
||||||
|
- >>> from contextlib import redirect_stdout
|
||||||
|
+ >>> from contextlib2 import redirect_stdout
|
||||||
|
>>> from io import StringIO
|
||||||
|
>>> stream = StringIO()
|
||||||
|
>>> write_to_stream = redirect_stdout(stream)
|
||||||
|
@@ -992,7 +985,7 @@
|
||||||
|
when leaving any with statement, regardless of where those callbacks
|
||||||
|
were added::
|
||||||
|
|
||||||
|
- >>> from contextlib import ExitStack
|
||||||
|
+ >>> from contextlib2 import ExitStack
|
||||||
|
>>> stack = ExitStack()
|
||||||
|
>>> with stack:
|
||||||
|
... stack.callback(print, "Callback: from first context")
|
||||||
|
@@ -1026,7 +1019,7 @@
|
||||||
|
Using separate :class:`ExitStack` instances instead of reusing a single
|
||||||
|
instance avoids that problem::
|
||||||
|
|
||||||
|
- >>> from contextlib import ExitStack
|
||||||
|
+ >>> from contextlib2 import ExitStack
|
||||||
|
>>> with ExitStack() as outer_stack:
|
||||||
|
... outer_stack.callback(print, "Callback: from outer context")
|
||||||
|
... with ExitStack() as inner_stack:
|
||||||
67
dev/py3_12_test_async_to_contextlib2.patch
Normal file
67
dev/py3_12_test_async_to_contextlib2.patch
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib_async.py 2024-05-23 11:57:09.276022441 +1000
|
||||||
|
+++ /home/ncoghlan/devel/contextlib2/test/test_contextlib_async.py 2024-05-23 17:39:05.799797895 +1000
|
||||||
|
@@ -1,5 +1,7 @@
|
||||||
|
+"""Unit tests for asynchronous features of contextlib2.py"""
|
||||||
|
+
|
||||||
|
import asyncio
|
||||||
|
-from contextlib import (
|
||||||
|
+from contextlib2 import (
|
||||||
|
asynccontextmanager, AbstractAsyncContextManager,
|
||||||
|
AsyncExitStack, nullcontext, aclosing, contextmanager)
|
||||||
|
import functools
|
||||||
|
@@ -7,7 +9,7 @@
|
||||||
|
import unittest
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
-from test.test_contextlib import TestBaseExitStack
|
||||||
|
+from .test_contextlib import TestBaseExitStack
|
||||||
|
|
||||||
|
support.requires_working_socket(module=True)
|
||||||
|
|
||||||
|
@@ -202,7 +204,8 @@
|
||||||
|
await ctx.__aexit__(TypeError, TypeError('foo'), None)
|
||||||
|
if support.check_impl_detail(cpython=True):
|
||||||
|
# The "gen" attribute is an implementation detail.
|
||||||
|
- self.assertFalse(ctx.gen.ag_suspended)
|
||||||
|
+ if support.cl2_async_gens_have_ag_suspended:
|
||||||
|
+ self.assertFalse(ctx.gen.ag_suspended)
|
||||||
|
|
||||||
|
@_async_test
|
||||||
|
async def test_contextmanager_trap_no_yield(self):
|
||||||
|
@@ -226,7 +229,8 @@
|
||||||
|
await ctx.__aexit__(None, None, None)
|
||||||
|
if support.check_impl_detail(cpython=True):
|
||||||
|
# The "gen" attribute is an implementation detail.
|
||||||
|
- self.assertFalse(ctx.gen.ag_suspended)
|
||||||
|
+ if support.cl2_async_gens_have_ag_suspended:
|
||||||
|
+ self.assertFalse(ctx.gen.ag_suspended)
|
||||||
|
|
||||||
|
@_async_test
|
||||||
|
async def test_contextmanager_non_normalised(self):
|
||||||
|
@@ -669,12 +673,13 @@
|
||||||
|
async def __aenter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
+ expected_error, expected_text = support.cl2_cm_api_exc_info_async()
|
||||||
|
async with self.exit_stack() as stack:
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
await stack.enter_async_context(LacksEnterAndExit())
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
await stack.enter_async_context(LacksEnter())
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
await stack.enter_async_context(LacksExit())
|
||||||
|
self.assertFalse(stack._exit_callbacks)
|
||||||
|
|
||||||
|
@@ -752,7 +757,8 @@
|
||||||
|
cm.__aenter__ = object()
|
||||||
|
cm.__aexit__ = object()
|
||||||
|
stack = self.exit_stack()
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
|
||||||
|
+ expected_error, expected_text = support.cl2_cm_api_exc_info_async()
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
await stack.enter_async_context(cm)
|
||||||
|
stack.push_async_exit(cm)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1][1], cm)
|
||||||
106
dev/py3_12_test_to_contextlib2.patch
Normal file
106
dev/py3_12_test_to_contextlib2.patch
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib.py 2024-05-23 11:57:09.276022441 +1000
|
||||||
|
+++ /home/ncoghlan/devel/contextlib2/test/test_contextlib.py 2024-05-23 17:38:37.295232213 +1000
|
||||||
|
@@ -1,4 +1,4 @@
|
||||||
|
-"""Unit tests for contextlib.py, and other context managers."""
|
||||||
|
+"""Unit tests for synchronous features of contextlib2.py"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
@@ -7,7 +7,7 @@
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import unittest
|
||||||
|
-from contextlib import * # Tests __all__
|
||||||
|
+from contextlib2 import * # Tests __all__
|
||||||
|
from test import support
|
||||||
|
from test.support import os_helper
|
||||||
|
from test.support.testcase import ExceptionIsLikeMixin
|
||||||
|
@@ -161,7 +161,8 @@
|
||||||
|
ctx.__exit__(TypeError, TypeError("foo"), None)
|
||||||
|
if support.check_impl_detail(cpython=True):
|
||||||
|
# The "gen" attribute is an implementation detail.
|
||||||
|
- self.assertFalse(ctx.gen.gi_suspended)
|
||||||
|
+ if support.cl2_gens_have_gi_suspended:
|
||||||
|
+ self.assertFalse(ctx.gen.gi_suspended)
|
||||||
|
|
||||||
|
def test_contextmanager_trap_no_yield(self):
|
||||||
|
@contextmanager
|
||||||
|
@@ -183,7 +184,8 @@
|
||||||
|
ctx.__exit__(None, None, None)
|
||||||
|
if support.check_impl_detail(cpython=True):
|
||||||
|
# The "gen" attribute is an implementation detail.
|
||||||
|
- self.assertFalse(ctx.gen.gi_suspended)
|
||||||
|
+ if support.cl2_gens_have_gi_suspended:
|
||||||
|
+ self.assertFalse(ctx.gen.gi_suspended)
|
||||||
|
|
||||||
|
def test_contextmanager_non_normalised(self):
|
||||||
|
@contextmanager
|
||||||
|
@@ -610,7 +612,8 @@
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||||
|
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__enter__")
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
with mycontext():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@@ -622,7 +625,8 @@
|
||||||
|
def __uxit__(self, *exc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'):
|
||||||
|
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__exit__")
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
with mycontext():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@@ -790,12 +794,13 @@
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync()
|
||||||
|
with self.exit_stack() as stack:
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
stack.enter_context(LacksEnterAndExit())
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
stack.enter_context(LacksEnter())
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
stack.enter_context(LacksExit())
|
||||||
|
self.assertFalse(stack._exit_callbacks)
|
||||||
|
|
||||||
|
@@ -858,8 +863,11 @@
|
||||||
|
[('_exit_wrapper', 'callback(*args, **kwds)'),
|
||||||
|
('raise_exc', 'raise exc')]
|
||||||
|
|
||||||
|
- self.assertEqual(
|
||||||
|
- [(f.name, f.line) for f in ve_frames], expected)
|
||||||
|
+ # This check fails on PyPy 3.10
|
||||||
|
+ # It also fails on CPython 3.9 and earlier versions
|
||||||
|
+ if support.check_impl_detail(cpython=True) and support.cl2_check_traceback_details:
|
||||||
|
+ self.assertEqual(
|
||||||
|
+ [(f.name, f.line) for f in ve_frames], expected)
|
||||||
|
|
||||||
|
self.assertIsInstance(exc.__context__, ZeroDivisionError)
|
||||||
|
zde_frames = traceback.extract_tb(exc.__context__.__traceback__)
|
||||||
|
@@ -1093,7 +1101,8 @@
|
||||||
|
cm.__enter__ = object()
|
||||||
|
cm.__exit__ = object()
|
||||||
|
stack = self.exit_stack()
|
||||||
|
- with self.assertRaisesRegex(TypeError, 'the context manager'):
|
||||||
|
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync()
|
||||||
|
+ with self.assertRaisesRegex(expected_error, expected_text):
|
||||||
|
stack.enter_context(cm)
|
||||||
|
stack.push(cm)
|
||||||
|
self.assertIs(stack._exit_callbacks[-1][1], cm)
|
||||||
|
@@ -1264,6 +1273,7 @@
|
||||||
|
1/0
|
||||||
|
self.assertTrue(outer_continued)
|
||||||
|
|
||||||
|
+ @support.cl2_requires_exception_groups
|
||||||
|
def test_exception_groups(self):
|
||||||
|
eg_ve = lambda: ExceptionGroup(
|
||||||
|
"EG with ValueErrors only",
|
||||||
27
dev/save_diff_snapshot.sh
Executable file
27
dev/save_diff_snapshot.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
git_root="$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
cpython_dir="${1:-$git_root/../cpython}"
|
||||||
|
|
||||||
|
diff_prefix="py3_12" # Update based on the version being synced
|
||||||
|
|
||||||
|
function diff_file()
|
||||||
|
{
|
||||||
|
diff -ud "$2" "$git_root/$3" > "$git_root/dev/${diff_prefix}_$1.patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
diff_file rst_to_contextlib2 \
|
||||||
|
"$cpython_dir/Doc/library/contextlib.rst" "docs/contextlib2.rst"
|
||||||
|
|
||||||
|
diff_file py_to_contextlib2 \
|
||||||
|
"$cpython_dir/Lib/contextlib.py" "contextlib2/__init__.py"
|
||||||
|
|
||||||
|
diff_file pyi_to_contextlib2 \
|
||||||
|
"$git_root/dev/typeshed_contextlib.pyi" "contextlib2/__init__.pyi"
|
||||||
|
|
||||||
|
diff_file test_to_contextlib2 \
|
||||||
|
"$cpython_dir/Lib/test/test_contextlib.py" "test/test_contextlib.py"
|
||||||
|
|
||||||
|
diff_file test_async_to_contextlib2 \
|
||||||
|
"$cpython_dir/Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py"
|
||||||
19
dev/sync_from_cpython.sh
Executable file
19
dev/sync_from_cpython.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
git_root="$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
cpython_dir="${1:-$git_root/../cpython}" # Folder with relevant CPython version
|
||||||
|
|
||||||
|
function sync_file()
|
||||||
|
{
|
||||||
|
cp -fv "$cpython_dir/$1" "$git_root/$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_file "Doc/library/contextlib.rst" "docs/contextlib2.rst"
|
||||||
|
sync_file "Lib/contextlib.py" "contextlib2/__init__.py"
|
||||||
|
sync_file "Lib/test/test_contextlib.py" "test/test_contextlib.py"
|
||||||
|
sync_file "Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Note: Update the 'contextlib2/__init__.pyi' stub as described in the file"
|
||||||
|
echo
|
||||||
209
dev/typeshed_contextlib.pyi
Normal file
209
dev/typeshed_contextlib.pyi
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
import abc
|
||||||
|
import sys
|
||||||
|
from _typeshed import FileDescriptorOrPath, Unused
|
||||||
|
from abc import abstractmethod
|
||||||
|
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable
|
||||||
|
from typing_extensions import ParamSpec, Self, TypeAlias
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"contextmanager",
|
||||||
|
"closing",
|
||||||
|
"AbstractContextManager",
|
||||||
|
"ContextDecorator",
|
||||||
|
"ExitStack",
|
||||||
|
"redirect_stdout",
|
||||||
|
"redirect_stderr",
|
||||||
|
"suppress",
|
||||||
|
"AbstractAsyncContextManager",
|
||||||
|
"AsyncExitStack",
|
||||||
|
"asynccontextmanager",
|
||||||
|
"nullcontext",
|
||||||
|
]
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
__all__ += ["aclosing"]
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
__all__ += ["chdir"]
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
_T_co = TypeVar("_T_co", covariant=True)
|
||||||
|
_T_io = TypeVar("_T_io", bound=IO[str] | None)
|
||||||
|
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None)
|
||||||
|
_F = TypeVar("_F", bound=Callable[..., Any])
|
||||||
|
_P = ParamSpec("_P")
|
||||||
|
|
||||||
|
_ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None]
|
||||||
|
_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc)
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class AbstractContextManager(Protocol[_T_co, _ExitT_co]):
|
||||||
|
def __enter__(self) -> _T_co: ...
|
||||||
|
@abstractmethod
|
||||||
|
def __exit__(
|
||||||
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
|
||||||
|
) -> _ExitT_co: ...
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]):
|
||||||
|
async def __aenter__(self) -> _T_co: ...
|
||||||
|
@abstractmethod
|
||||||
|
async def __aexit__(
|
||||||
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
|
||||||
|
) -> _ExitT_co: ...
|
||||||
|
|
||||||
|
class ContextDecorator:
|
||||||
|
def __call__(self, func: _F) -> _F: ...
|
||||||
|
|
||||||
|
class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator):
|
||||||
|
# __init__ and all instance attributes are actually inherited from _GeneratorContextManagerBase
|
||||||
|
# _GeneratorContextManagerBase is more trouble than it's worth to include in the stub; see #6676
|
||||||
|
def __init__(self, func: Callable[..., Iterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
|
||||||
|
gen: Generator[_T_co, Any, Any]
|
||||||
|
func: Callable[..., Generator[_T_co, Any, Any]]
|
||||||
|
args: tuple[Any, ...]
|
||||||
|
kwds: dict[str, Any]
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
def __exit__(
|
||||||
|
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
) -> bool | None: ...
|
||||||
|
else:
|
||||||
|
def __exit__(
|
||||||
|
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
) -> bool | None: ...
|
||||||
|
|
||||||
|
def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ...
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
|
||||||
|
|
||||||
|
class AsyncContextDecorator:
|
||||||
|
def __call__(self, func: _AF) -> _AF: ...
|
||||||
|
|
||||||
|
class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator):
|
||||||
|
# __init__ and these attributes are actually defined in the base class _GeneratorContextManagerBase,
|
||||||
|
# which is more trouble than it's worth to include in the stub (see #6676)
|
||||||
|
def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
|
||||||
|
gen: AsyncGenerator[_T_co, Any]
|
||||||
|
func: Callable[..., AsyncGenerator[_T_co, Any]]
|
||||||
|
args: tuple[Any, ...]
|
||||||
|
kwds: dict[str, Any]
|
||||||
|
async def __aexit__(
|
||||||
|
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
) -> bool | None: ...
|
||||||
|
|
||||||
|
else:
|
||||||
|
class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]):
|
||||||
|
def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
|
||||||
|
gen: AsyncGenerator[_T_co, Any]
|
||||||
|
func: Callable[..., AsyncGenerator[_T_co, Any]]
|
||||||
|
args: tuple[Any, ...]
|
||||||
|
kwds: dict[str, Any]
|
||||||
|
async def __aexit__(
|
||||||
|
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||||
|
) -> bool | None: ...
|
||||||
|
|
||||||
|
def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ...
|
||||||
|
|
||||||
|
class _SupportsClose(Protocol):
|
||||||
|
def close(self) -> object: ...
|
||||||
|
|
||||||
|
_SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose)
|
||||||
|
|
||||||
|
class closing(AbstractContextManager[_SupportsCloseT, None]):
|
||||||
|
def __init__(self, thing: _SupportsCloseT) -> None: ...
|
||||||
|
def __exit__(self, *exc_info: Unused) -> None: ...
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
class _SupportsAclose(Protocol):
|
||||||
|
def aclose(self) -> Awaitable[object]: ...
|
||||||
|
|
||||||
|
_SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose)
|
||||||
|
|
||||||
|
class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]):
|
||||||
|
def __init__(self, thing: _SupportsAcloseT) -> None: ...
|
||||||
|
async def __aexit__(self, *exc_info: Unused) -> None: ...
|
||||||
|
|
||||||
|
class suppress(AbstractContextManager[None, bool]):
|
||||||
|
def __init__(self, *exceptions: type[BaseException]) -> None: ...
|
||||||
|
def __exit__(
|
||||||
|
self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None
|
||||||
|
) -> bool: ...
|
||||||
|
|
||||||
|
class _RedirectStream(AbstractContextManager[_T_io, None]):
|
||||||
|
def __init__(self, new_target: _T_io) -> None: ...
|
||||||
|
def __exit__(
|
||||||
|
self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
class redirect_stdout(_RedirectStream[_T_io]): ...
|
||||||
|
class redirect_stderr(_RedirectStream[_T_io]): ...
|
||||||
|
|
||||||
|
# In reality this is a subclass of `AbstractContextManager`;
|
||||||
|
# see #7961 for why we don't do that in the stub
|
||||||
|
class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta):
|
||||||
|
def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ...
|
||||||
|
def push(self, exit: _CM_EF) -> _CM_EF: ...
|
||||||
|
def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ...
|
||||||
|
def pop_all(self) -> Self: ...
|
||||||
|
def close(self) -> None: ...
|
||||||
|
def __enter__(self) -> Self: ...
|
||||||
|
def __exit__(
|
||||||
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
|
||||||
|
) -> _ExitT_co: ...
|
||||||
|
|
||||||
|
_ExitCoroFunc: TypeAlias = Callable[
|
||||||
|
[type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None]
|
||||||
|
]
|
||||||
|
_ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc)
|
||||||
|
|
||||||
|
# In reality this is a subclass of `AbstractAsyncContextManager`;
|
||||||
|
# see #7961 for why we don't do that in the stub
|
||||||
|
class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta):
|
||||||
|
def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ...
|
||||||
|
async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ...
|
||||||
|
def push(self, exit: _CM_EF) -> _CM_EF: ...
|
||||||
|
def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ...
|
||||||
|
def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ...
|
||||||
|
def push_async_callback(
|
||||||
|
self, callback: Callable[_P, Awaitable[_T]], /, *args: _P.args, **kwds: _P.kwargs
|
||||||
|
) -> Callable[_P, Awaitable[_T]]: ...
|
||||||
|
def pop_all(self) -> Self: ...
|
||||||
|
async def aclose(self) -> None: ...
|
||||||
|
async def __aenter__(self) -> Self: ...
|
||||||
|
async def __aexit__(
|
||||||
|
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
|
||||||
|
) -> bool: ...
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]):
|
||||||
|
enter_result: _T
|
||||||
|
@overload
|
||||||
|
def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
|
||||||
|
@overload
|
||||||
|
def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
|
||||||
|
def __enter__(self) -> _T: ...
|
||||||
|
def __exit__(self, *exctype: Unused) -> None: ...
|
||||||
|
async def __aenter__(self) -> _T: ...
|
||||||
|
async def __aexit__(self, *exctype: Unused) -> None: ...
|
||||||
|
|
||||||
|
else:
|
||||||
|
class nullcontext(AbstractContextManager[_T, None]):
|
||||||
|
enter_result: _T
|
||||||
|
@overload
|
||||||
|
def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
|
||||||
|
@overload
|
||||||
|
def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
|
||||||
|
def __enter__(self) -> _T: ...
|
||||||
|
def __exit__(self, *exctype: Unused) -> None: ...
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
_T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath)
|
||||||
|
|
||||||
|
class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]):
|
||||||
|
path: _T_fd_or_any_path
|
||||||
|
def __init__(self, path: _T_fd_or_any_path) -> None: ...
|
||||||
|
def __enter__(self) -> None: ...
|
||||||
|
def __exit__(self, *excinfo: Unused) -> None: ...
|
||||||
21
docs/conf.py
21
docs/conf.py
|
|
@ -11,8 +11,6 @@
|
||||||
# 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.
|
||||||
|
|
@ -25,7 +23,10 @@ import sys, os
|
||||||
|
|
||||||
# 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 = ['sphinx.ext.intersphinx']
|
extensions = [
|
||||||
|
'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']
|
||||||
|
|
@ -41,7 +42,7 @@ master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'contextlib2'
|
project = u'contextlib2'
|
||||||
copyright = u'2011, Nick Coghlan'
|
copyright = u'2024, Alyssa 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
|
||||||
|
|
@ -92,7 +93,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 = 'default'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
# 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
|
||||||
|
|
@ -121,7 +122,7 @@ html_theme = 'default'
|
||||||
# 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 = ['_static']
|
html_static_path = []
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
@ -180,7 +181,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'Nick Coghlan', 'manual'),
|
u'Alyssa 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
|
||||||
|
|
@ -213,9 +214,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'Nick Coghlan'], 1)
|
[u'Alyssa Coghlan'], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python 3 standard library.
|
# Configuration for intersphinx: refer to the Python 3 standard library.
|
||||||
intersphinx_mapping = {'http://docs.python.org/py3k': None}
|
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||||
|
|
|
||||||
1033
docs/contextlib2.rst
Normal file
1033
docs/contextlib2.rst
Normal file
File diff suppressed because it is too large
Load diff
663
docs/index.rst
663
docs/index.rst
|
|
@ -15,659 +15,32 @@ 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`` statement.
|
involving the ``with`` and ``async with`` statements.
|
||||||
|
|
||||||
|
|
||||||
Additions Relative to the Standard Library
|
Additions Relative to the Standard Library
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
This module is primarily a backport of the Python 3.5 version of
|
This module is primarily a backport of the Python 3.12.3 version of
|
||||||
:mod:`contextlib` to earlier releases. However, it is also a proving ground
|
:mod:`contextlib` to earlier releases. (Note: as of the start of the Python 3.13
|
||||||
for new features not yet part of the standard library.
|
beta release cycle, there have been no subsequent changes to ``contextlib``)
|
||||||
|
|
||||||
There are currently no such features in the module.
|
The module makes use of positional-only argument syntax in several call
|
||||||
|
signatures, so the oldest supported Python version is Python 3.8.
|
||||||
|
|
||||||
Refer to the :mod:`contextlib` documentation for details of which
|
This module may also be used as a proving ground for new features not yet part
|
||||||
versions of Python 3 introduce the various APIs provided in this module.
|
of the standard library. There are currently no such features in the module.
|
||||||
|
|
||||||
|
Finally, this module contains some deprecated APIs which never graduated to
|
||||||
|
standard library inclusion. These interfaces are no longer documented, but may
|
||||||
|
still be present in the code (emitting ``DeprecationWarning`` if used).
|
||||||
|
|
||||||
API Reference
|
|
||||||
=============
|
|
||||||
|
|
||||||
Functions and classes provided:
|
Using the Module
|
||||||
|
|
||||||
.. 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
|
|
||||||
====================
|
====================
|
||||||
|
|
||||||
This section describes some examples and recipes for making effective use of
|
.. toctree::
|
||||||
the tools provided by :mod:`contextlib2`. Some of them may also work with
|
contextlib2.rst
|
||||||
: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
|
||||||
====================
|
====================
|
||||||
|
|
@ -683,7 +56,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 2.7 and 3.2+.
|
Supported Python versions are currently 3.8+.
|
||||||
|
|
||||||
.. _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
|
||||||
|
|
@ -693,11 +66,11 @@ Supported Python versions are currently 2.7 and 3.2+.
|
||||||
Development and Support
|
Development and Support
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
contextlib2 is developed and maintained on BitBucket_. Problems and suggested
|
contextlib2 is developed and maintained on GitHub_. Problems and suggested
|
||||||
improvements can be posted to the `issue tracker`_.
|
improvements can be posted to the `issue tracker`_.
|
||||||
|
|
||||||
.. _BitBucket: https://bitbucket.org/ncoghlan/contextlib2/overview
|
.. _GitHub: https://github.com/jazzband/contextlib2
|
||||||
.. _issue tracker: https://bitbucket.org/ncoghlan/contextlib2/issues?status=new&status=open
|
.. _issue tracker: https://github.com/jazzband/contextlib2/issues
|
||||||
|
|
||||||
|
|
||||||
.. include:: ../NEWS.rst
|
.. include:: ../NEWS.rst
|
||||||
|
|
|
||||||
1
docs/requirements.txt
Normal file
1
docs/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
sphinx-rtd-theme
|
||||||
42
setup.py
42
setup.py
|
|
@ -1,29 +1,45 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from distutils.core import setup
|
try:
|
||||||
|
from setuptools import setup
|
||||||
|
except ImportError:
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
# Technically, unittest2 is a dependency to run the tests on 2.7
|
# Note: The minimum Python version requirement is set on the basis of
|
||||||
# This file ignores that, since I don't want to depend on
|
# "if it's not tested, it's broken".
|
||||||
# setuptools just to get "tests_require" support
|
# Specifically, if a Python version is no longer available for testing
|
||||||
|
# in CI, then the minimum supported Python version will be increased.
|
||||||
|
# That way there's no risk of a release that breaks older Python versions.
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='contextlib2',
|
name='contextlib2',
|
||||||
version=open('VERSION.txt').read().strip(),
|
version=open('VERSION.txt').read().strip(),
|
||||||
py_modules=['contextlib2'],
|
python_requires='>=3.7',
|
||||||
|
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.md').read(),
|
long_description=open('README.rst').read(),
|
||||||
author='Nick Coghlan',
|
author='Alyssa Coghlan',
|
||||||
author_email='ncoghlan@gmail.com',
|
author_email='ncoghlan@gmail.com',
|
||||||
url='http://contextlib2.readthedocs.org',
|
url='https://github.com/jazzband/contextlib2',
|
||||||
|
project_urls= {
|
||||||
|
'Documentation': 'https://contextlib2.readthedocs.org',
|
||||||
|
'Source': 'https://github.com/jazzband/contextlib2.git',
|
||||||
|
'Issue Tracker': 'https://github.com/jazzband/contextlib2.git',
|
||||||
|
}
|
||||||
classifiers=[
|
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
|
||||||
'Programming Language :: Python :: 2',
|
# It definitely won't work on versions without native async support
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.7',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
|
'Programming Language :: Python :: 3.11',
|
||||||
|
'Programming Language :: Python :: 3.12',
|
||||||
],
|
],
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
1
test/__init__.py
Normal file
1
test/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# unittest test discovery requires an __init__.py file in the test directory
|
||||||
1
test/data/README.txt
Normal file
1
test/data/README.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
test_contextlib uses this folder for chdir tests
|
||||||
101
test/support/__init__.py
Normal file
101
test/support/__init__.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
"""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)
|
||||||
4
test/support/os_helper.py
Normal file
4
test/support/os_helper.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""Enough of the test.support.os_helper APIs to run the contextlib test suite"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
unlink = os.unlink
|
||||||
33
test/support/testcase.py
Normal file
33
test/support/testcase.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""Enough of the test.support.testcase APIs to run the contextlib test suite"""
|
||||||
|
from . import cl2_have_exception_groups
|
||||||
|
|
||||||
|
if not cl2_have_exception_groups:
|
||||||
|
# Placeholder to let the isinstance check below run on older versions
|
||||||
|
class ExceptionGroup(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ExceptionIsLikeMixin:
|
||||||
|
def assertExceptionIsLike(self, exc, template):
|
||||||
|
"""
|
||||||
|
Passes when the provided `exc` matches the structure of `template`.
|
||||||
|
Individual exceptions don't have to be the same objects or even pass
|
||||||
|
an equality test: they only need to be the same type and contain equal
|
||||||
|
`exc_obj.args`.
|
||||||
|
"""
|
||||||
|
if exc is None and template is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if template is None:
|
||||||
|
self.fail(f"unexpected exception: {exc}")
|
||||||
|
|
||||||
|
if exc is None:
|
||||||
|
self.fail(f"expected an exception like {template!r}, got None")
|
||||||
|
|
||||||
|
if not isinstance(exc, ExceptionGroup):
|
||||||
|
self.assertEqual(exc.__class__, template.__class__)
|
||||||
|
self.assertEqual(exc.args[0], template.args[0])
|
||||||
|
else:
|
||||||
|
self.assertEqual(exc.message, template.message)
|
||||||
|
self.assertEqual(len(exc.exceptions), len(template.exceptions))
|
||||||
|
for e, t in zip(exc.exceptions, template.exceptions):
|
||||||
|
self.assertExceptionIsLike(e, t)
|
||||||
1368
test/test_contextlib.py
Normal file
1368
test/test_contextlib.py
Normal file
File diff suppressed because it is too large
Load diff
778
test/test_contextlib_async.py
Normal file
778
test/test_contextlib_async.py
Normal file
|
|
@ -0,0 +1,778 @@
|
||||||
|
"""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
test/ziptestdata/README.txt
Normal file
1
test/ziptestdata/README.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
test_contextlib uses this folder for chdir tests
|
||||||
|
|
@ -1,835 +0,0 @@
|
||||||
"""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,11 +1,24 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py27, pypy, py34, py35, pypy3
|
# Python 3.8 is the first version with positional-only argument syntax support
|
||||||
|
envlist = py{38,39,3.10,3.11,3.12,py3}
|
||||||
|
skip_missing_interpreters = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = {envpython} test_contextlib2.py
|
commands =
|
||||||
|
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
|
||||||
|
|
||||||
[testenv:py27]
|
[gh-actions]
|
||||||
deps = unittest2
|
python =
|
||||||
|
3.8: py38
|
||||||
[testenv:pypy]
|
3.9: py39
|
||||||
deps = unittest2
|
3.10: py3.10
|
||||||
|
3.11: py3.11
|
||||||
|
3.12: py3.12
|
||||||
|
pypy-3.10: pypy3
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue