mirror of
https://github.com/jazzband/contextlib2.git
synced 2026-03-17 06:00:23 +00:00
Compare commits
111 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 |
43 changed files with 6018 additions and 2058 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 }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -10,3 +10,8 @@ MANIFEST
|
|||
*~
|
||||
.coverage
|
||||
coverage.xml
|
||||
htmlcov/
|
||||
|
||||
# Patching output files
|
||||
*.orig
|
||||
*.rej
|
||||
|
|
|
|||
14
.pre-commit-config.yaml
Normal file
14
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
35
.readthedocs.yaml
Normal file
35
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the OS, Python version and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "20"
|
||||
# rust: "1.70"
|
||||
# golang: "1.20"
|
||||
|
||||
# Build documentation in the "docs/" directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
||||
# builder: "dirhtml"
|
||||
# Fail on all warnings to avoid broken references
|
||||
# fail_on_warning: true
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
# formats:
|
||||
# - pdf
|
||||
# - epub
|
||||
|
||||
# Optional but recommended, declare the Python requirements required
|
||||
# to build your documentation
|
||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
22
.travis.yml
22
.travis.yml
|
|
@ -1,22 +0,0 @@
|
|||
language: python
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.6 # these are just to make travis's UI a bit prettier
|
||||
env: TOXENV=py26
|
||||
- python: 2.7
|
||||
env: TOXENV=py27
|
||||
- python: 3.3
|
||||
env: TOXENV=py33
|
||||
- python: 3.4
|
||||
env: TOXENV=py34
|
||||
- python: 3.5
|
||||
env: TOXENV=py35
|
||||
- python: nightly
|
||||
env: TOXENV=py36
|
||||
- python: pypy
|
||||
env: TOXENV=pypy
|
||||
|
||||
install: pip install tox && tox --notest
|
||||
|
||||
script: tox
|
||||
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
|
||||
==========================
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
include *.py *.txt *.rst MANIFEST.in
|
||||
include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in
|
||||
recursive-include contextlib2 *.py *.pyi py.typed
|
||||
recursive-include docs *.rst *.py make.bat Makefile
|
||||
|
||||
recursive-include test *.py
|
||||
recursive-include dev *.patch *.allowlist *.sh
|
||||
|
|
|
|||
129
NEWS.rst
129
NEWS.rst
|
|
@ -1,6 +1,113 @@
|
|||
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)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
@ -53,8 +160,8 @@ Release History
|
|||
0.4.0 (2012-05-05)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Issue #8: Replace ContextStack with ExitStack (old ContextStack API
|
||||
retained for backwards compatibility)
|
||||
* (BitBucket) Issue #8: Replace ContextStack with ExitStack (old ContextStack
|
||||
API retained for backwards compatibility)
|
||||
|
||||
* Fall back to unittest2 if unittest is missing required functionality
|
||||
|
||||
|
|
@ -62,20 +169,20 @@ Release History
|
|||
0.3.1 (2012-01-17)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Issue #7: Add MANIFEST.in so PyPI package contains all relevant files
|
||||
(patch contributed by Doug Latornell)
|
||||
* (BitBucket) Issue #7: Add MANIFEST.in so PyPI package contains all relevant
|
||||
files (patch contributed by Doug Latornell)
|
||||
|
||||
|
||||
0.3 (2012-01-04)
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
* Issue #5: ContextStack.register no longer pointlessly returns the wrapped
|
||||
function
|
||||
* Issue #2: Add examples and recipes section to docs
|
||||
* Issue #3: ContextStack.register_exit() now accepts objects with __exit__
|
||||
attributes in addition to accepting exit callbacks directly
|
||||
* Issue #1: Add ContextStack.preserve() to move all registered callbacks to
|
||||
a new ContextStack object
|
||||
* (BitBucket) Issue #5: ContextStack.register no longer pointlessly returns the
|
||||
wrapped function
|
||||
* (BitBucket) Issue #2: Add examples and recipes section to docs
|
||||
* (BitBucket) Issue #3: ContextStack.register_exit() now accepts objects with
|
||||
__exit__ attributes in addition to accepting exit callbacks directly
|
||||
* (BitBucket) Issue #1: Add ContextStack.preserve() to move all registered
|
||||
callbacks to a new ContextStack object
|
||||
* Wrapped callbacks now expose __wrapped__ (for direct callbacks) or __self__
|
||||
(for context manager methods) attributes to aid in introspection
|
||||
* Moved version number to a VERSION.txt file (read by both docs and setup.py)
|
||||
|
|
|
|||
110
README.rst
110
README.rst
|
|
@ -1,30 +1,45 @@
|
|||
.. 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
|
||||
|
||||
.. image:: https://img.shields.io/travis/ncoghlan/contextlib2/master.svg
|
||||
:target: http://travis-ci.org/ncoghlan/contextlib2
|
||||
|
||||
.. image:: https://coveralls.io/repos/github/ncoghlan/contextlib2/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/ncoghlan/contextlib2?branch=master
|
||||
|
||||
.. image:: https://landscape.io/github/ncoghlan/contextlib2/master/landscape.svg
|
||||
:target: https://landscape.io/github/ncoghlan/contextlib2/
|
||||
:target: https://contextlib2.readthedocs.org/
|
||||
:alt: Latest Docs
|
||||
|
||||
contextlib2 is a backport of the `standard library's contextlib
|
||||
module <https://docs.python.org/3.5/library/contextlib.html>`_ to
|
||||
module <https://docs.python.org/3/library/contextlib.html>`_ to
|
||||
earlier Python versions.
|
||||
|
||||
It also serves as a real world proving ground for possible future
|
||||
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 ``unittest2`` for testing
|
||||
on Python 2.x.
|
||||
``contextlib2`` has no runtime dependencies, but requires ``setuptools`` and
|
||||
``wheel`` at build time to generate universal wheel archives.
|
||||
|
||||
Local testing is just a matter of running ``python test_contextlib2.py``.
|
||||
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/>`_::
|
||||
|
|
@ -32,21 +47,56 @@ You can test against multiple versions of Python with
|
|||
pip install tox
|
||||
tox
|
||||
|
||||
Versions currently tested in both tox and Travis CI are:
|
||||
Versions currently tested in both tox and GitHub Actions are:
|
||||
|
||||
* CPython 2.6
|
||||
* CPython 2.7
|
||||
* CPython 3.4
|
||||
* CPython 3.5
|
||||
* CPython 3.6 (CPython development branch)
|
||||
* PyPy
|
||||
* CPython 3.8
|
||||
* CPython 3.9
|
||||
* CPython 3.10
|
||||
* CPython 3.11
|
||||
* CPython 3.12
|
||||
* PyPy3 (specifically 3.10 in GitHub Actions)
|
||||
|
||||
tox also has a PyPy3 configuration, but it is not configured in Travis
|
||||
due to a
|
||||
`known incompatibility <https://bitbucket.org/pypy/pypy/issues/1903>`_.
|
||||
Updating to a new stdlib reference version
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To install several of the relevant runtimes on Fedora 23::
|
||||
As of Python 3.12.3, 4 files needed to be copied from the CPython reference
|
||||
implementation to contextlib2:
|
||||
|
||||
sudo dnf install python python3 pypy pypy3
|
||||
sudo dnf copr enable -y mstuchli/Python3.5
|
||||
sudo dnf install python35-python3
|
||||
* ``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.3
|
||||
24.6.0rc1
|
||||
|
|
|
|||
433
contextlib2.py
433
contextlib2.py
|
|
@ -1,433 +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(object):
|
||||
|
||||
_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(object):
|
||||
"""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[0] >= 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")
|
||||
|
||||
# Handle old-style classes if they exist
|
||||
try:
|
||||
from types import InstanceType
|
||||
except ImportError:
|
||||
# Python 3 doesn't have old-style classes
|
||||
_get_type = type
|
||||
else:
|
||||
# Need to handle old-style context managers on Python 2
|
||||
def _get_type(obj):
|
||||
obj_type = type(obj)
|
||||
if obj_type is InstanceType:
|
||||
return obj.__class__ # Old-style class
|
||||
return obj_type # New-style class
|
||||
|
||||
# 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 = _get_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 = _get_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
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
|
|
@ -25,7 +23,10 @@ import sys, os
|
|||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.intersphinx']
|
||||
extensions = [
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx_rtd_theme',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
|
@ -41,7 +42,7 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'contextlib2'
|
||||
copyright = u'2011, Nick Coghlan'
|
||||
copyright = u'2024, Alyssa Coghlan'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
|
|
@ -92,7 +93,7 @@ pygments_style = 'sphinx'
|
|||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
|
@ -121,7 +122,7 @@ html_theme = 'default'
|
|||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
|
|
@ -180,7 +181,7 @@ htmlhelp_basename = 'contextlib2doc'
|
|||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'contextlib2.tex', u'contextlib2 Documentation',
|
||||
u'Nick Coghlan', 'manual'),
|
||||
u'Alyssa Coghlan', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
|
@ -213,9 +214,9 @@ latex_documents = [
|
|||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'contextlib2', u'contextlib2 Documentation',
|
||||
[u'Nick Coghlan'], 1)
|
||||
[u'Alyssa Coghlan'], 1)
|
||||
]
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python 3 standard library.
|
||||
intersphinx_mapping = {'http://docs.python.org/py3k': None}
|
||||
# Configuration for intersphinx: refer to the Python 3 standard library.
|
||||
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
661
docs/index.rst
661
docs/index.rst
|
|
@ -15,659 +15,32 @@ also serves as a real world proving ground for potential future enhancements
|
|||
to that module.
|
||||
|
||||
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
|
||||
------------------------------------------
|
||||
|
||||
This module is primarily a backport of the Python 3.5 version of
|
||||
:mod:`contextlib` to earlier releases. However, it is also a proving ground
|
||||
for new features not yet part of the standard library.
|
||||
This module is primarily a backport of the Python 3.12.3 version of
|
||||
:mod:`contextlib` to earlier releases. (Note: as of the start of the Python 3.13
|
||||
beta release cycle, there have been no subsequent changes to ``contextlib``)
|
||||
|
||||
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
|
||||
versions of Python 3 introduce the various APIs provided in this module.
|
||||
This module may also be used as a proving ground for new features not yet part
|
||||
of the standard library. There are currently no such features in the module.
|
||||
|
||||
Finally, this module contains some deprecated APIs which never graduated to
|
||||
standard library inclusion. These interfaces are no longer documented, but may
|
||||
still be present in the code (emitting ``DeprecationWarning`` if used).
|
||||
|
||||
API Reference
|
||||
=============
|
||||
|
||||
Functions and classes provided:
|
||||
|
||||
.. decorator:: contextmanager
|
||||
|
||||
This function is a :term:`decorator` that can be used to define a factory
|
||||
function for :keyword:`with` statement context managers, without needing to
|
||||
create a class or separate :meth:`__enter__` and :meth:`__exit__` methods.
|
||||
|
||||
A simple example (this is not recommended as a real way of generating HTML!)::
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def tag(name):
|
||||
print("<%s>" % name)
|
||||
yield
|
||||
print("</%s>" % name)
|
||||
|
||||
>>> with tag("h1"):
|
||||
... print("foo")
|
||||
...
|
||||
<h1>
|
||||
foo
|
||||
</h1>
|
||||
|
||||
The function being decorated must return a :term:`generator`-iterator when
|
||||
called. This iterator must yield exactly one value, which will be bound to
|
||||
the targets in the :keyword:`with` statement's :keyword:`as` clause, if any.
|
||||
|
||||
At the point where the generator yields, the block nested in the :keyword:`with`
|
||||
statement is executed. The generator is then resumed after the block is exited.
|
||||
If an unhandled exception occurs in the block, it is reraised inside the
|
||||
generator at the point where the yield occurred. Thus, you can use a
|
||||
:keyword:`try`...\ :keyword:`except`...\ :keyword:`finally` statement to trap
|
||||
the error (if any), or ensure that some cleanup takes place. If an exception is
|
||||
trapped merely in order to log it or to perform some action (rather than to
|
||||
suppress it entirely), the generator must reraise that exception. Otherwise the
|
||||
generator context manager will indicate to the :keyword:`with` statement that
|
||||
the exception has been handled, and execution will resume with the statement
|
||||
immediately following the :keyword:`with` statement.
|
||||
|
||||
:func:`contextmanager` uses :class:`ContextDecorator` so the context managers
|
||||
it creates can be used as decorators as well as in :keyword:`with` statements.
|
||||
When used as a decorator, a new generator instance is implicitly created on
|
||||
each function call (this allows the otherwise "one-shot" context managers
|
||||
created by :func:`contextmanager` to meet the requirement that context
|
||||
managers support multiple invocations in order to be used as decorators).
|
||||
|
||||
|
||||
.. function:: closing(thing)
|
||||
|
||||
Return a context manager that closes *thing* upon completion of the block. This
|
||||
is basically equivalent to::
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def closing(thing):
|
||||
try:
|
||||
yield thing
|
||||
finally:
|
||||
thing.close()
|
||||
|
||||
And lets you write code like this::
|
||||
|
||||
from contextlib import closing
|
||||
from urllib.request import urlopen
|
||||
|
||||
with closing(urlopen('http://www.python.org')) as page:
|
||||
for line in page:
|
||||
print(line)
|
||||
|
||||
without needing to explicitly close ``page``. Even if an error occurs,
|
||||
``page.close()`` will be called when the :keyword:`with` block is exited.
|
||||
|
||||
|
||||
.. function:: suppress(*exceptions)
|
||||
|
||||
Return a context manager that suppresses any of the specified exceptions
|
||||
if they occur in the body of a with statement and then resumes execution
|
||||
with the first statement following the end of the with statement.
|
||||
|
||||
As with any other mechanism that completely suppresses exceptions, this
|
||||
context manager should be used only to cover very specific errors where
|
||||
silently continuing with program execution is known to be the right
|
||||
thing to do.
|
||||
|
||||
For example::
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove('somefile.tmp')
|
||||
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove('someotherfile.tmp')
|
||||
|
||||
This code is equivalent to::
|
||||
|
||||
try:
|
||||
os.remove('somefile.tmp')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.remove('someotherfile.tmp')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
Part of the standard library in Python 3.4 and later
|
||||
|
||||
|
||||
.. function:: redirect_stdout(new_target)
|
||||
|
||||
Context manager for temporarily redirecting :data:`sys.stdout` to
|
||||
another file or file-like object.
|
||||
|
||||
This tool adds flexibility to existing functions or classes whose output
|
||||
is hardwired to stdout.
|
||||
|
||||
For example, the output of :func:`help` normally is sent to *sys.stdout*.
|
||||
You can capture that output in a string by redirecting the output to a
|
||||
:class:`io.StringIO` object::
|
||||
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
help(pow)
|
||||
s = f.getvalue()
|
||||
|
||||
To send the output of :func:`help` to a file on disk, redirect the output
|
||||
to a regular file::
|
||||
|
||||
with open('help.txt', 'w') as f:
|
||||
with redirect_stdout(f):
|
||||
help(pow)
|
||||
|
||||
To send the output of :func:`help` to *sys.stderr*::
|
||||
|
||||
with redirect_stdout(sys.stderr):
|
||||
help(pow)
|
||||
|
||||
Note that the global side effect on :data:`sys.stdout` means that this
|
||||
context manager is not suitable for use in library code and most threaded
|
||||
applications. It also has no effect on the output of subprocesses.
|
||||
However, it is still a useful approach for many utility scripts.
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
Part of the standard library in Python 3.4 and later
|
||||
|
||||
|
||||
.. function:: redirect_stderr(new_target)
|
||||
|
||||
Similar to :func:`redirect_stdout`, but redirecting :data:`sys.stderr` to
|
||||
another file or file-like object.
|
||||
|
||||
This context manager is :ref:`reentrant <reentrant-cms>`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
Part of the standard library in Python 3.5 and later
|
||||
|
||||
|
||||
.. class:: ContextDecorator()
|
||||
|
||||
A base class that enables a context manager to also be used as a decorator.
|
||||
|
||||
Context managers inheriting from ``ContextDecorator`` have to implement
|
||||
``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional
|
||||
exception handling even when used as a decorator.
|
||||
|
||||
``ContextDecorator`` is used by :func:`contextmanager`, so you get this
|
||||
functionality automatically.
|
||||
|
||||
Example of ``ContextDecorator``::
|
||||
|
||||
from contextlib import ContextDecorator
|
||||
|
||||
class mycontext(ContextDecorator):
|
||||
def __enter__(self):
|
||||
print('Starting')
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
print('Finishing')
|
||||
return False
|
||||
|
||||
>>> @mycontext()
|
||||
... def function():
|
||||
... print('The bit in the middle')
|
||||
...
|
||||
>>> function()
|
||||
Starting
|
||||
The bit in the middle
|
||||
Finishing
|
||||
|
||||
>>> with mycontext():
|
||||
... print('The bit in the middle')
|
||||
...
|
||||
Starting
|
||||
The bit in the middle
|
||||
Finishing
|
||||
|
||||
This change is just syntactic sugar for any construct of the following form::
|
||||
|
||||
def f():
|
||||
with cm():
|
||||
# Do stuff
|
||||
|
||||
``ContextDecorator`` lets you instead write::
|
||||
|
||||
@cm()
|
||||
def f():
|
||||
# Do stuff
|
||||
|
||||
It makes it clear that the ``cm`` applies to the whole function, rather than
|
||||
just a piece of it (and saving an indentation level is nice, too).
|
||||
|
||||
Existing context managers that already have a base class can be extended by
|
||||
using ``ContextDecorator`` as a mixin class::
|
||||
|
||||
from contextlib import ContextDecorator
|
||||
|
||||
class mycontext(ContextBaseClass, ContextDecorator):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
.. note::
|
||||
As the decorated function must be able to be called multiple times, the
|
||||
underlying context manager must support use in multiple :keyword:`with`
|
||||
statements. If this is not the case, then the original construct with the
|
||||
explicit :keyword:`with` statement inside the function should be used.
|
||||
|
||||
|
||||
.. class:: ExitStack()
|
||||
|
||||
A context manager that is designed to make it easy to programmatically
|
||||
combine other context managers and cleanup functions, especially those
|
||||
that are optional or otherwise driven by input data.
|
||||
|
||||
For example, a set of files may easily be handled in a single with
|
||||
statement as follows::
|
||||
|
||||
with ExitStack() as stack:
|
||||
files = [stack.enter_context(open(fname)) for fname in filenames]
|
||||
# All opened files will automatically be closed at the end of
|
||||
# the with statement, even if attempts to open files later
|
||||
# in the list raise an exception
|
||||
|
||||
Each instance maintains a stack of registered callbacks that are called in
|
||||
reverse order when the instance is closed (either explicitly or implicitly
|
||||
at the end of a :keyword:`with` statement). Note that callbacks are *not*
|
||||
invoked implicitly when the context stack instance is garbage collected.
|
||||
|
||||
This stack model is used so that context managers that acquire their
|
||||
resources in their ``__init__`` method (such as file objects) can be
|
||||
handled correctly.
|
||||
|
||||
Since registered callbacks are invoked in the reverse order of
|
||||
registration, this ends up behaving as if multiple nested :keyword:`with`
|
||||
statements had been used with the registered set of callbacks. This even
|
||||
extends to exception handling - if an inner callback suppresses or replaces
|
||||
an exception, then outer callbacks will be passed arguments based on that
|
||||
updated state.
|
||||
|
||||
This is a relatively low level API that takes care of the details of
|
||||
correctly unwinding the stack of exit callbacks. It provides a suitable
|
||||
foundation for higher level context managers that manipulate the exit
|
||||
stack in application specific ways.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
Part of the standard library in Python 3.3 and later
|
||||
|
||||
.. method:: enter_context(cm)
|
||||
|
||||
Enters a new context manager and adds its :meth:`__exit__` method to
|
||||
the callback stack. The return value is the result of the context
|
||||
manager's own :meth:`__enter__` method.
|
||||
|
||||
These context managers may suppress exceptions just as they normally
|
||||
would if used directly as part of a :keyword:`with` statement.
|
||||
|
||||
.. method:: push(exit)
|
||||
|
||||
Adds a context manager's :meth:`__exit__` method to the callback stack.
|
||||
|
||||
As ``__enter__`` is *not* invoked, this method can be used to cover
|
||||
part of an :meth:`__enter__` implementation with a context manager's own
|
||||
:meth:`__exit__` method.
|
||||
|
||||
If passed an object that is not a context manager, this method assumes
|
||||
it is a callback with the same signature as a context manager's
|
||||
:meth:`__exit__` method and adds it directly to the callback stack.
|
||||
|
||||
By returning true values, these callbacks can suppress exceptions the
|
||||
same way context manager :meth:`__exit__` methods can.
|
||||
|
||||
The passed in object is returned from the function, allowing this
|
||||
method to be used as a function decorator.
|
||||
|
||||
.. method:: callback(callback, *args, **kwds)
|
||||
|
||||
Accepts an arbitrary callback function and arguments and adds it to
|
||||
the callback stack.
|
||||
|
||||
Unlike the other methods, callbacks added this way cannot suppress
|
||||
exceptions (as they are never passed the exception details).
|
||||
|
||||
The passed in callback is returned from the function, allowing this
|
||||
method to be used as a function decorator.
|
||||
|
||||
.. method:: pop_all()
|
||||
|
||||
Transfers the callback stack to a fresh :class:`ExitStack` instance
|
||||
and returns it. No callbacks are invoked by this operation - instead,
|
||||
they will now be invoked when the new stack is closed (either
|
||||
explicitly or implicitly at the end of a :keyword:`with` statement).
|
||||
|
||||
For example, a group of files can be opened as an "all or nothing"
|
||||
operation as follows::
|
||||
|
||||
with ExitStack() as stack:
|
||||
files = [stack.enter_context(open(fname)) for fname in filenames]
|
||||
# Hold onto the close method, but don't call it yet.
|
||||
close_files = stack.pop_all().close
|
||||
# If opening any file fails, all previously opened files will be
|
||||
# closed automatically. If all files are opened successfully,
|
||||
# they will remain open even after the with statement ends.
|
||||
# close_files() can then be invoked explicitly to close them all.
|
||||
|
||||
.. method:: close()
|
||||
|
||||
Immediately unwinds the callback stack, invoking callbacks in the
|
||||
reverse order of registration. For any context managers and exit
|
||||
callbacks registered, the arguments passed in will indicate that no
|
||||
exception occurred.
|
||||
|
||||
|
||||
Examples and Recipes
|
||||
Using the Module
|
||||
====================
|
||||
|
||||
This section describes some examples and recipes for making effective use of
|
||||
the tools provided by :mod:`contextlib2`. Some of them may also work with
|
||||
:mod:`contextlib` in sufficiently recent versions of Python. When this is the
|
||||
case, it is noted at the end of the example.
|
||||
|
||||
|
||||
Cleaning up in an ``__enter__`` implementation
|
||||
----------------------------------------------
|
||||
|
||||
As noted in the documentation of :meth:`ExitStack.push`, this
|
||||
method can be useful in cleaning up an already allocated resource if later
|
||||
steps in the :meth:`__enter__` implementation fail.
|
||||
|
||||
Here's an example of doing this for a context manager that accepts resource
|
||||
acquisition and release functions, along with an optional validation function,
|
||||
and maps them to the context management protocol::
|
||||
|
||||
from contextlib2 import ExitStack
|
||||
|
||||
class ResourceManager(object):
|
||||
|
||||
def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
|
||||
self.acquire_resource = acquire_resource
|
||||
self.release_resource = release_resource
|
||||
self.check_resource_ok = check_resource_ok
|
||||
|
||||
def __enter__(self):
|
||||
resource = self.acquire_resource()
|
||||
if self.check_resource_ok is not None:
|
||||
with ExitStack() as stack:
|
||||
stack.push(self)
|
||||
if not self.check_resource_ok(resource):
|
||||
msg = "Failed validation for {!r}"
|
||||
raise RuntimeError(msg.format(resource))
|
||||
# The validation check passed and didn't raise an exception
|
||||
# Accordingly, we want to keep the resource, and pass it
|
||||
# back to our caller
|
||||
stack.pop_all()
|
||||
return resource
|
||||
|
||||
def __exit__(self, *exc_details):
|
||||
# We don't need to duplicate any of our resource release logic
|
||||
self.release_resource()
|
||||
|
||||
This example will also work with :mod:`contextlib` in Python 3.3 or later.
|
||||
|
||||
|
||||
Replacing any use of ``try-finally`` and flag variables
|
||||
-------------------------------------------------------
|
||||
|
||||
A pattern you will sometimes see is a ``try-finally`` statement with a flag
|
||||
variable to indicate whether or not the body of the ``finally`` clause should
|
||||
be executed. In its simplest form (that can't already be handled just by
|
||||
using an ``except`` clause instead), it looks something like this::
|
||||
|
||||
cleanup_needed = True
|
||||
try:
|
||||
result = perform_operation()
|
||||
if result:
|
||||
cleanup_needed = False
|
||||
finally:
|
||||
if cleanup_needed:
|
||||
cleanup_resources()
|
||||
|
||||
As with any ``try`` statement based code, this can cause problems for
|
||||
development and review, because the setup code and the cleanup code can end
|
||||
up being separated by arbitrarily long sections of code.
|
||||
|
||||
:class:`ExitStack` makes it possible to instead register a callback for
|
||||
execution at the end of a ``with`` statement, and then later decide to skip
|
||||
executing that callback::
|
||||
|
||||
from contextlib2 import ExitStack
|
||||
|
||||
with ExitStack() as stack:
|
||||
stack.callback(cleanup_resources)
|
||||
result = perform_operation()
|
||||
if result:
|
||||
stack.pop_all()
|
||||
|
||||
This allows the intended cleanup up behaviour to be made explicit up front,
|
||||
rather than requiring a separate flag variable.
|
||||
|
||||
If you find yourself using this pattern a lot, it can be simplified even
|
||||
further by means of a small helper class::
|
||||
|
||||
from contextlib2 import ExitStack
|
||||
|
||||
class Callback(ExitStack):
|
||||
def __init__(self, callback, *args, **kwds):
|
||||
super(Callback, self).__init__()
|
||||
self.callback(callback, *args, **kwds)
|
||||
|
||||
def cancel(self):
|
||||
self.pop_all()
|
||||
|
||||
with Callback(cleanup_resources) as cb:
|
||||
result = perform_operation()
|
||||
if result:
|
||||
cb.cancel()
|
||||
|
||||
If the resource cleanup isn't already neatly bundled into a standalone
|
||||
function, then it is still possible to use the decorator form of
|
||||
:meth:`ExitStack.callback` to declare the resource cleanup in
|
||||
advance::
|
||||
|
||||
from contextlib2 import ExitStack
|
||||
|
||||
with ExitStack() as stack:
|
||||
@stack.callback
|
||||
def cleanup_resources():
|
||||
...
|
||||
result = perform_operation()
|
||||
if result:
|
||||
stack.pop_all()
|
||||
|
||||
Due to the way the decorator protocol works, a callback function
|
||||
declared this way cannot take any parameters. Instead, any resources to
|
||||
be released must be accessed as closure variables.
|
||||
|
||||
This example will also work with :mod:`contextlib` in Python 3.3 or later.
|
||||
|
||||
|
||||
Using a context manager as a function decorator
|
||||
-----------------------------------------------
|
||||
|
||||
:class:`ContextDecorator` makes it possible to use a context manager in
|
||||
both an ordinary ``with`` statement and also as a function decorator. The
|
||||
:meth:`ContextDecorator.refresh_cm` method even makes it possible to use
|
||||
otherwise single use context managers (such as those created by
|
||||
:func:`contextmanager`) that way.
|
||||
|
||||
For example, it is sometimes useful to wrap functions or groups of statements
|
||||
with a logger that can track the time of entry and time of exit. Rather than
|
||||
writing both a function decorator and a context manager for the task,
|
||||
:func:`contextmanager` provides both capabilities in a single
|
||||
definition::
|
||||
|
||||
from contextlib2 import contextmanager
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@contextmanager
|
||||
def track_entry_and_exit(name):
|
||||
logging.info('Entering: {}'.format(name))
|
||||
yield
|
||||
logging.info('Exiting: {}'.format(name))
|
||||
|
||||
This can be used as both a context manager::
|
||||
|
||||
with track_entry_and_exit('widget loader'):
|
||||
print('Some time consuming activity goes here')
|
||||
load_widget()
|
||||
|
||||
And also as a function decorator::
|
||||
|
||||
@track_entry_and_exit('widget loader')
|
||||
def activity():
|
||||
print('Some time consuming activity goes here')
|
||||
load_widget()
|
||||
|
||||
Note that there is one additional limitation when using context managers
|
||||
as function decorators: there's no way to access the return value of
|
||||
:meth:`__enter__`. If that value is needed, then it is still necessary to use
|
||||
an explicit ``with`` statement.
|
||||
|
||||
This example will also work with :mod:`contextlib` in Python 3.2.1 or later.
|
||||
|
||||
|
||||
Context Management Concepts
|
||||
===========================
|
||||
|
||||
.. _single-use-reusable-and-reentrant-cms:
|
||||
|
||||
Single use, reusable and reentrant context managers
|
||||
---------------------------------------------------
|
||||
|
||||
Most context managers are written in a way that means they can only be
|
||||
used effectively in a :keyword:`with` statement once. These single use
|
||||
context managers must be created afresh each time they're used -
|
||||
attempting to use them a second time will trigger an exception or
|
||||
otherwise not work correctly.
|
||||
|
||||
This common limitation means that it is generally advisable to create
|
||||
context managers directly in the header of the :keyword:`with` statement
|
||||
where they are used (as shown in all of the usage examples above).
|
||||
|
||||
Files are an example of effectively single use context managers, since
|
||||
the first :keyword:`with` statement will close the file, preventing any
|
||||
further IO operations using that file object.
|
||||
|
||||
Context managers created using :func:`contextmanager` are also single use
|
||||
context managers, and will complain about the underlying generator failing
|
||||
to yield if an attempt is made to use them a second time::
|
||||
|
||||
>>> from contextlib import contextmanager
|
||||
>>> @contextmanager
|
||||
... def singleuse():
|
||||
... print("Before")
|
||||
... yield
|
||||
... print("After")
|
||||
...
|
||||
>>> cm = singleuse()
|
||||
>>> with cm:
|
||||
... pass
|
||||
...
|
||||
Before
|
||||
After
|
||||
>>> with cm:
|
||||
... pass
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
RuntimeError: generator didn't yield
|
||||
|
||||
|
||||
.. _reentrant-cms:
|
||||
|
||||
Reentrant context managers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
More sophisticated context managers may be "reentrant". These context
|
||||
managers can not only be used in multiple :keyword:`with` statements,
|
||||
but may also be used *inside* a :keyword:`with` statement that is already
|
||||
using the same context manager.
|
||||
|
||||
:class:`threading.RLock` is an example of a reentrant context manager, as is
|
||||
:func:`suppress`. Here's a toy example of reentrant use (real world
|
||||
examples of reentrancy are more likely to occur with objects like recursive
|
||||
locks and are likely to be far more complicated than this example)::
|
||||
|
||||
>>> from contextlib import suppress
|
||||
>>> ignore_raised_exception = suppress(ZeroDivisionError)
|
||||
>>> with ignore_raised_exception:
|
||||
... with ignore_raised_exception:
|
||||
... 1/0
|
||||
... print("This line runs")
|
||||
... 1/0
|
||||
... print("This is skipped")
|
||||
...
|
||||
This line runs
|
||||
>>> # The second exception is also suppressed
|
||||
|
||||
|
||||
.. _reusable-cms:
|
||||
|
||||
Reusable context managers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Distinct from both single use and reentrant context managers are "reusable"
|
||||
context managers (or, to be completely explicit, "reusable, but not
|
||||
reentrant" context managers, since reentrant context managers are also
|
||||
reusable). These context managers support being used multiple times, but
|
||||
will fail (or otherwise not work correctly) if the specific context manager
|
||||
instance has already been used in a containing with statement.
|
||||
|
||||
An example of a reusable context manager is :func:`redirect_stdout`::
|
||||
|
||||
>>> from contextlib import redirect_stdout
|
||||
>>> from io import StringIO
|
||||
>>> f = StringIO()
|
||||
>>> collect_output = redirect_stdout(f)
|
||||
>>> with collect_output:
|
||||
... print("Collected")
|
||||
...
|
||||
>>> print("Not collected")
|
||||
Not collected
|
||||
>>> with collect_output:
|
||||
... print("Also collected")
|
||||
...
|
||||
>>> print(f.getvalue())
|
||||
Collected
|
||||
Also collected
|
||||
|
||||
However, this context manager is not reentrant, so attempting to reuse it
|
||||
within a containing with statement fails:
|
||||
|
||||
>>> with collect_output:
|
||||
... # Nested reuse is not permitted
|
||||
... with collect_output:
|
||||
... pass
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
RuntimeError: Cannot reenter <...>
|
||||
|
||||
.. toctree::
|
||||
contextlib2.rst
|
||||
|
||||
Obtaining the Module
|
||||
====================
|
||||
|
|
@ -683,7 +56,7 @@ PyPI page`_.
|
|||
There are no operating system or distribution specific versions of this
|
||||
module - it is a pure Python module that should work on all platforms.
|
||||
|
||||
Supported Python versions are currently 2.7 and 3.2+.
|
||||
Supported Python versions are currently 3.8+.
|
||||
|
||||
.. _Python Package Index: http://pypi.python.org
|
||||
.. _pip: http://www.pip-installer.org
|
||||
|
|
@ -696,8 +69,8 @@ Development and Support
|
|||
contextlib2 is developed and maintained on GitHub_. Problems and suggested
|
||||
improvements can be posted to the `issue tracker`_.
|
||||
|
||||
.. _GitHub: https://github.com/ncoghlan/contextlib2
|
||||
.. _issue tracker: https://github.com/ncoghlan/contextlib2/issues
|
||||
.. _GitHub: https://github.com/jazzband/contextlib2
|
||||
.. _issue tracker: https://github.com/jazzband/contextlib2/issues
|
||||
|
||||
|
||||
.. include:: ../NEWS.rst
|
||||
|
|
|
|||
1
docs/requirements.txt
Normal file
1
docs/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
sphinx-rtd-theme
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
[bdist_wheel]
|
||||
universal=1
|
||||
38
setup.py
38
setup.py
|
|
@ -1,27 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
from setuptools import setup
|
||||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
# Note: The minimum Python version requirement is set on the basis of
|
||||
# "if it's not tested, it's broken".
|
||||
# Specifically, if a Python version is no longer available for testing
|
||||
# in CI, then the minimum supported Python version will be increased.
|
||||
# That way there's no risk of a release that breaks older Python versions.
|
||||
|
||||
setup(
|
||||
name='contextlib2',
|
||||
version=open('VERSION.txt').read().strip(),
|
||||
py_modules=['contextlib2'],
|
||||
python_requires='>=3.7',
|
||||
packages=['contextlib2'],
|
||||
include_package_data=True,
|
||||
license='PSF License',
|
||||
description='Backports and enhancements for the contextlib module',
|
||||
long_description=open('README.rst').read(),
|
||||
author='Nick Coghlan',
|
||||
author='Alyssa Coghlan',
|
||||
author_email='ncoghlan@gmail.com',
|
||||
url='http://contextlib2.readthedocs.org',
|
||||
url='https://github.com/jazzband/contextlib2',
|
||||
project_urls= {
|
||||
'Documentation': 'https://contextlib2.readthedocs.org',
|
||||
'Source': 'https://github.com/jazzband/contextlib2.git',
|
||||
'Issue Tracker': 'https://github.com/jazzband/contextlib2.git',
|
||||
}
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'License :: OSI Approved :: Python Software Foundation License',
|
||||
# These are the Python versions tested, it may work on others
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
# It definitely won't work on versions without native async support
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
],
|
||||
|
||||
)
|
||||
|
|
|
|||
1
test/__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,878 +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)
|
||||
|
||||
def test_default_class_semantics(self):
|
||||
# For Python 2.x, this ensures compatibility with old-style classes
|
||||
# For Python 3.x, it just reruns some of the other tests
|
||||
class DefaultCM:
|
||||
def __enter__(self):
|
||||
result.append("Enter")
|
||||
def __exit__(self, *exc_details):
|
||||
result.append("Exit")
|
||||
class DefaultCallable:
|
||||
def __call__(self, *exc_details):
|
||||
result.append("Callback")
|
||||
|
||||
result = []
|
||||
cm = DefaultCM()
|
||||
cb = DefaultCallable()
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||
stack.push(cb)
|
||||
stack.push(cm)
|
||||
self.assertIs(stack._exit_callbacks[-1].__self__, cm)
|
||||
result.append("Running")
|
||||
stack.callback(cb)
|
||||
self.assertIs(stack._exit_callbacks[-1].__wrapped__, cb)
|
||||
self.assertEqual(result, ["Enter", "Running",
|
||||
"Callback", "Exit",
|
||||
"Callback", "Exit",
|
||||
])
|
||||
|
||||
with ExitStack():
|
||||
pass
|
||||
|
||||
|
||||
|
||||
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")
|
||||
|
||||
def test_cm_is_exitstack_compatible(self):
|
||||
with ExitStack() as stack:
|
||||
# This shouldn't raise an exception.
|
||||
stack.enter_context(self.redirect_stream(io.StringIO()))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def test_cm_is_exitstack_compatible(self):
|
||||
with ExitStack() as stack:
|
||||
# This shouldn't raise an exception.
|
||||
stack.enter_context(suppress())
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
unittest.main()
|
||||
33
tox.ini
33
tox.ini
|
|
@ -1,19 +1,24 @@
|
|||
[tox]
|
||||
envlist = py26, py27, pypy, py33, py34, py35, py36, 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]
|
||||
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:py26]
|
||||
deps = unittest2
|
||||
|
||||
[testenv:py27]
|
||||
deps = unittest2
|
||||
|
||||
[testenv:pypy]
|
||||
deps = unittest2
|
||||
|
||||
[testenv:pypy3]
|
||||
# Known incompatibility: https://bitbucket.org/pypy/pypy/issues/1903
|
||||
ignore_outcome = True
|
||||
[gh-actions]
|
||||
python =
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py3.10
|
||||
3.11: py3.11
|
||||
3.12: py3.12
|
||||
pypy-3.10: pypy3
|
||||
|
|
|
|||
Loading…
Reference in a new issue