Compare commits

...

102 commits

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

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

Note: typechecking is still disabled in CI for now

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

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

Committed via https://github.com/asottile/all-repos
2021-11-10 22:14:24 +02:00
Jannis Leidel
0828b5a332
Merge pull request #47 from jazzband/jazzband/sync/default
Jazzband: Synced file(s) with jazzband/.github
2021-10-22 17:46:45 +02:00
jazzband-bot
1ea4e4dfe0 Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' 2021-10-21 14:33:39 +00:00
Thomas Grainger
3feaef8f91
Merge pull request #45 from jazzband/typeshed-resync 2021-08-13 11:00:17 +01:00
Sviatoslav Sydorenko
89c5189648
Merge pull request #46 from ppentchev/roam-universal-wheels
Do not build universal wheels.
2021-07-26 23:50:56 +03:00
Peter Pentchev
5373afc4ba Do not build universal wheels.
Only Python 3.x is supported, so universal wheels will only confuse
tools that examine the PyPI metadata about supported Python versions.
2021-07-24 17:08:22 +03:00
Thomas Grainger
3d2540dda1
Update contextlib2/_typeshed.py 2021-07-20 17:38:40 +01:00
Thomas Grainger
d78420a808
Update contextlib2/_typeshed.py 2021-07-20 17:38:32 +01:00
Thomas Grainger
a35252eefd
backport _typeshed.Self 2021-07-20 14:11:42 +01:00
Thomas Grainger
16db3dcf86
resync from typeshed 2021-07-16 08:33:19 +01:00
Nick Coghlan
2dc4c04492
Merge pull request #40 from graingert/widen-supports-aclose
sync pyi from typeshed
2021-06-30 22:24:55 +10:00
Thomas Grainger
908e2da622
add py3.10 async contextlib.nullcontext (pyi)
Fixes #41
2021-06-29 19:15:55 +01:00
Thomas Grainger
c907a9975e
widen _SupportsAclose
Fixes #39
2021-06-29 09:55:17 +01:00
Nick Coghlan
94174bafeb
Merge pull request #38 from ncoghlan/issue-37-mypy-stubtest
Issue #37: Use mypy.stubcheck to validate stub file
2021-06-27 16:15:18 +10:00
Nick Coghlan
fffe1d96be Fix mypy.stubtest invocation 2021-06-27 16:09:59 +10:00
Nick Coghlan
9a1453b673 Issue #37: Use mypy.stubcheck to validate stub file
* use mypy.stubcheck to ensure stub API matches module API
* sync latest typeshed stub with nullcontext API fix
* use allowlist to ignore deliberately omitted deprecated APIs
2021-06-27 16:04:15 +10:00
Nick Coghlan
190f6af5ef
Merge pull request #36 from ncoghlan/remove-lingering-py2-mentions
Remove lingering Python 2 mention
2021-06-26 22:51:54 +10:00
Nick Coghlan
181b9c212f Remove lingering Python 2 mention 2021-06-26 22:51:16 +10:00
Nick Coghlan
bed62be63b
Merge pull request #34 from ncoghlan/issue-33-add-type-hinting-support
Issue #33: convert to package and include typeshed type hints
2021-06-26 22:36:08 +10:00
Nick Coghlan
4b353b9815 Update NEWS file 2021-06-26 22:34:23 +10:00
Nick Coghlan
0fb391a30c Actually run a typecheck in CI 2021-06-26 22:12:55 +10:00
Nick Coghlan
841d0ae73a Include the pyi patch file 2021-06-26 21:49:03 +10:00
Nick Coghlan
3f6ee4d314 Merge remote-tracking branch 'origin/master' into issue-33-add-type-hinting-support 2021-06-26 21:45:51 +10:00
Nick Coghlan
77e93832f0
Merge pull request #35 from ncoghlan/issue-12-backport-py3.10-contextlib-docs
Issue #12: Sync API documentation from Python 3.10
2021-06-26 21:44:11 +10:00
Nick Coghlan
72c47e4629 Fix type hint filename, make API spec version independent 2021-06-26 21:43:44 +10:00
Nick Coghlan
802688462c Update README (including for docs backport) 2021-06-26 21:43:16 +10:00
Nick Coghlan
a858a8f4d6 Tweak relative paths for docs patch 2021-06-26 21:28:15 +10:00
Nick Coghlan
46244c827d Issue #12: Sync API documentation from Python 3.10 2021-06-26 21:24:10 +10:00
Nick Coghlan
e42cd73fe9 Issue #33: convert to package and include typeshed type hints 2021-06-26 20:43:11 +10:00
Nick Coghlan
b99ed09dfd
Merge pull request #32 from ncoghlan/issue-12-sync-module-from-python-3.10
Issue #12:  sync module from CPython 3.10
2021-06-26 18:45:29 +10:00
Nick Coghlan
032662d6e9 Fix test incompatibility with PyPy3 2021-06-26 18:30:55 +10:00
Nick Coghlan
4b39470cbb Sync test suite, add notes on sync process 2021-06-26 17:49:42 +10:00
Nick Coghlan
94a3b86586 Sync module, mostly keeping old test suite 2021-06-26 16:55:09 +10:00
Nick Coghlan
630f73a3a5 Add back ContextStack alias 2021-06-26 16:50:39 +10:00
Nick Coghlan
6a40a1ee80 Fix syntax and import compatibility 2021-06-26 16:49:00 +10:00
Nick Coghlan
cf0cece837 Add back refresh_cm 2021-06-26 16:40:28 +10:00
Nick Coghlan
17b0a93e5b Issue #12: sync contextlib from Python 3.10 2021-06-26 16:36:23 +10:00
Nick Coghlan
94f3881963
Merge pull request #30 from ncoghlan/issue-29-switch-to-calver-update-min-py-version
Issue #29: Switch to CalVer, require Python >= 3.6
2021-06-26 16:14:45 +10:00
Nick Coghlan
eed96067d7 Exception chaining is now always available 2021-06-26 16:04:26 +10:00
Nick Coghlan
cb66e76cd8 Remove no longer used Py2 compatibility code 2021-06-26 15:59:43 +10:00
Nick Coghlan
cef92f49f9 Accept 3.10 beta for testing 2021-06-26 15:47:45 +10:00
Nick Coghlan
dbcddef29f Ensure '3.10' is parsed correctly in YAML 2021-06-26 15:43:50 +10:00
Nick Coghlan
093bc69490 Fix versions in GitHub Actions 2021-06-26 15:42:03 +10:00
Nick Coghlan
a1ea615bb3 Issue #29: Switch to CalVer, require Python >= 3.6 2021-06-26 15:39:33 +10:00
Jannis Leidel
5f8498c19f
Merge pull request #26 from jazzband/github-actions
Migrate from Travis-CI to GitHub Actions
2020-11-30 12:51:12 +01:00
Jannis Leidel
34947502e5
Update .github/workflows/release.yml
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2020-11-30 12:19:07 +01:00
Jannis Leidel
07825fc9d9
Update README.rst
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2020-11-25 19:48:42 +01:00
Jannis Leidel
ca6d76fb9f
Use tag event instead of release. 2020-11-23 20:52:38 +01:00
Jannis Leidel
5eba33be65
Remove all Travis-CI cruft. 2020-11-23 16:45:50 +01:00
Jannis Leidel
90b643f5fb
Add release workflow. 2020-11-23 16:41:12 +01:00
Jannis Leidel
4a2baeceab
Cache pip 2020-11-23 16:38:15 +01:00
Jannis Leidel
f50a62ed77
No combine 2020-11-23 16:34:36 +01:00
Jannis Leidel
403bb51102
Combine coverage data 2020-11-23 16:31:16 +01:00
Jannis Leidel
cd80f6860d
Run tox in verbose mode 2020-11-23 16:26:48 +01:00
Jannis Leidel
72bde0ee1d
No 3.4 2020-11-23 16:25:24 +01:00
Jannis Leidel
70cba0264a
Use Codecov instead of coveralls. 2020-11-23 16:21:54 +01:00
Jannis Leidel
b06a025948
Write coverage file. 2020-11-23 16:06:51 +01:00
Jannis Leidel
178007ad39
Define gh-actions matrix. 2020-11-23 15:57:55 +01:00
Jannis Leidel
78ec3a1f4b
Debugging errors.. 2020-11-23 15:05:27 +01:00
Jannis Leidel
f8a3439209
Limit parallel runs. 2020-11-23 15:01:42 +01:00
Jannis Leidel
7eb119a73e
Remove cruft. 2020-11-23 14:59:47 +01:00
Jannis Leidel
bf72a830c2
Add test workflow. 2020-11-23 14:55:13 +01:00
Nick Coghlan
603052bd0c Update NEWS.rst for 0.6.0 release
Closes #24
2019-10-10 22:26:16 +10:00
Nick Coghlan
cc63b02723
Merge pull request #23 from jazzband/ncoghlan-prepare-0.6.0
Prepare a 0.6.0 release
2019-09-21 23:00:17 +10:00
Nick Coghlan
571c6f7a0f
Version bump 2019-09-21 22:42:18 +10:00
Nick Coghlan
dd12dc65c2
Merge pull request #22 from jayvdb/abc-doc
Docs and tests for abc and nullcontext
2019-09-21 22:41:26 +10:00
John Vandenberg
717cb47d34 Docs and tests for abc and nullcontext
Followup of https://github.com/jazzband/contextlib2/pull/21
2019-09-21 09:17:20 +07:00
Nick Coghlan
739bd253bd
Merge pull request #21 from jayvdb/abc
Add nullcontext and AbstractContextManager
2019-09-20 21:15:35 +10:00
John Vandenberg
a8e6733e21 Add AbstractContextManager
Closes https://github.com/jazzband/contextlib2/issues/16
2019-09-09 23:22:33 +07:00
John Vandenberg
656a4eba29 Add nullcontext
Related to https://github.com/jazzband/contextlib2/issues/16
2019-09-09 23:02:56 +07:00
Nick Coghlan
57206f1181
Merge pull request #20 from jazzband/pypi-auto-release
Update travis.yml for PyPI semiauto-deploy via Jazzband.
2019-08-12 21:42:33 +10:00
Jannis Leidel
dd2388f7e6
Update travis.yml for PyPI semiauto-deploy via Jazzband. 2019-08-09 09:45:36 +02:00
Nick Coghlan
5a5d456af1
Merge pull request #17 from likeon/drop-py26
- dropped python 2.6 from test matrix
- updated travis config
- pep8 compliance
- set python-requires in package metadata
2019-04-09 00:32:26 +10:00
likeon
17cb926d56 Removed CPython 2.6 from a list of currently tested versions in README
Added "Python-Requires" metadata to setup.py to prevent package installation on python versions below 2.7
2019-04-08 14:06:10 +02:00
Aleksandr Sterkhov
977da10ab0 Dropped support for Python 2.6 as it reached EOL in 2013 and not supported by both tox and setuptools anymore
Enabled back Travis CI testing in pypy3 env, using stable python 3.7
Updated setup.py according to all the changes
Fixed several formatting issues to better comply with PEP8
2019-03-26 23:39:03 +01:00
Nick Coghlan
68069e372b Merge pull request #15 from ncoghlan/update-test-matrix
Update test matrix
2017-04-25 13:47:18 +10:00
Nick Coghlan
3d2e7ab60e Fix version ref in comment 2017-04-25 13:28:55 +10:00
Nick Coghlan
57eb33db7b Don't test PyPy3 in Travis CI for now 2017-04-25 13:28:10 +10:00
Nick Coghlan
8728cae63b Update test matrix
- drop 3.3 compatibility testing
- CPython dev branch is now 3.7
- add 3.6 maintenance branch
- PyPy3 exception chaining bug has been fixed
2017-04-25 13:07:46 +10:00
Nick Coghlan
166b07a463 Merge pull request #14 from allanhaywood/issue13-setup-does-not-fallback-to-distutils
Fixing issue13: setup.py does not fallback to using distutils
2017-04-25 12:39:54 +10:00
Allan Haywood
44966d33d3 Fixing issue13: setup.py does not fallback to using distutils.core import setup
Added falling back to distutils.core
    Tested install on a machine with and without setuptools
2017-04-24 12:53:34 -07:00
41 changed files with 5212 additions and 1327 deletions

53
.github/workflows/release.yml vendored Normal file
View 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
View 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 }}

4
.gitignore vendored
View file

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

14
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,14 @@
repos:
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-check-blanket-noqa
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: check-yaml
ci:
autoupdate_schedule: quarterly

35
.readthedocs.yaml Normal file
View file

@ -0,0 +1,35 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# golang: "1.20"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt

View file

@ -1,30 +0,0 @@
language: python
matrix:
include:
- python: 2.6
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 coveralls && tox --notest
script: tox
after_success: coveralls
deploy:
provider: pypi
user: jazzband
distributions: sdist bdist_wheel
password:
secure: hb6wCAIduM7vMO4DNV3bQh52m9XD5nEyeMb5FN9F7MTjWwsc9jVEF8eGtE2OWM07LREVHtUgOcKW9w04JV2csQpZ6VuOsrRdjUdma13ubmNHuEEuG6vHX0iaC8D7ajNDOK2AA930QTXMR9baNF0FDxhogDvh/C9s8lyc/JFwUj4/P+1Qw1Ont/PEuilJcp3px6Gn3X2VUJXJXklCmQSYBQEfGG/jbpjd6PKxTAmAjgIXSRQFwZGU3wSt1SOy2UyXC5D4X9eFrXUz3iQBGJc9DvjBdKusBgeRP8SpSoPUluKtKjq4rHEA1NUWkXykb9npEdcEV3x0zSVPLrkr6TCQ0usWj/l0oMkirR/biQeKQkfwHzECQaXkM/lrxoBv+sGkKXi97Fm+fUiQFFYz6vFixm3DADUAu2CfCLFiYusdIUvbN8hF/Mj8SCFnZgRJHWgX9DtKXecCH8SXaECjU5ld2ogCtG3GJSLlLcvKW2twtZ+Gb8/SySIrQ3hTVyK57Yh12KQICPvO4Hii7wblj51L2crub2pefQolD5V9NMjopMuICiiOKXctnHOaC8NtTAadAVDK7t4t/wXEP6jXRZdO1oTm4pyetnyp0xiojknt8qhj66rlNIXX7jKPIChfgnRHc1ccRSHOuDmrIXgszpiZRuSvU7WkSodZwA850Hy28iY=
on:
tags: true
repo: jazzband/contextlib2
condition: "$TOXENV = py35" # only release once per test run for a Git tag

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View file

@ -1,3 +1,5 @@
[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](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).
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).

View file

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

View file

@ -1,2 +1,5 @@
include *.py *.txt *.rst *.md 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

View file

@ -1,11 +1,104 @@
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 (Nick Coghlan)
[Jazzband](https://jazzband.co/) project! This means that I (Alyssa Coghlan)
am no longer a single point of failure for backports of future contextlib
updates to earlier Python versions.

View file

@ -2,33 +2,44 @@
: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/jazzband/contextlib2/master.svg
:target: http://travis-ci.org/jazzband/contextlib2
.. image:: https://coveralls.io/repos/github/jazzband/contextlib2/badge.svg?branch=master
:target: https://coveralls.io/github/jazzband/contextlib2?branch=master
.. image:: https://landscape.io/github/jazzband/contextlib2/master/landscape.svg
:target: https://landscape.io/github/jazzband/contextlib2/
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/>`_::
@ -36,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.

View file

@ -1 +1 @@
0.5.4
24.6.0rc1

View file

@ -1,436 +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:
# Don't re-raise the passed in exception
if exc is value:
return False
# Likewise, avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a RuntimeError
# (see PEP 479).
if _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
View 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
View 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
View file

10
dev/mypy.allowlist Normal file
View 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__

View file

@ -0,0 +1,116 @@
--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/contextlib.py 2024-05-23 11:57:09.210023505 +1000
+++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.py 2024-05-23 17:05:06.549142813 +1000
@@ -5,7 +5,46 @@
import _collections_abc
from collections import deque
from functools import wraps
-from types import MethodType, GenericAlias
+from types import MethodType
+
+# Python 3.8 compatibility: GenericAlias may not be defined
+try:
+ from types import GenericAlias
+except ImportError:
+ # If the real GenericAlias type doesn't exist, __class_getitem__ won't be used,
+ # so the fallback placeholder doesn't need to provide any meaningful behaviour
+ # (typecheckers may still be unhappy, but for that problem the answer is
+ # "use a newer Python version with better typechecking support")
+ class GenericAlias:
+ pass
+
+# Python 3.10 and earlier compatibility: BaseExceptionGroup may not be defined
+try:
+ BaseExceptionGroup
+except NameError:
+ # If the real BaseExceptionGroup type doesn't exist, it will never actually
+ # be raised. This means the fallback placeholder doesn't need to provide
+ # any meaningful behaviour, it just needs to be compatible with 'issubclass'
+ class BaseExceptionGroup(BaseException):
+ pass
+
+# Python 3.9 and earlier compatibility: anext may not be defined
+try:
+ anext
+except NameError:
+ def anext(obj, /):
+ return obj.__anext__()
+
+# Python 3.11+ behaviour consistency: replace AttributeError with TypeError
+if sys.version_info >= (3, 11):
+ # enter_context() and enter_async_context() follow the change in the
+ # exception type raised by with statements and async with statements
+ _CL2_ERROR_TO_CONVERT = AttributeError
+else:
+ # On older versions, raise AttributeError without any changes
+ class _CL2_ERROR_TO_CONVERT(Exception):
+ pass
+
__all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",
"AbstractContextManager", "AbstractAsyncContextManager",
@@ -62,6 +101,24 @@
class ContextDecorator(object):
"A base class or mixin that enables context managers to work as decorators."
+ def refresh_cm(self):
+ """Returns the context manager used to actually wrap the call to the
+ decorated function.
+
+ The default implementation just returns *self*.
+
+ Overriding this method allows otherwise one-shot context managers
+ like _GeneratorContextManager to support use as decorators via
+ implicit recreation.
+
+ DEPRECATED: refresh_cm was never added to the standard library's
+ ContextDecorator API
+ """
+ import warnings # Only import if needed for the deprecation warning
+ warnings.warn("refresh_cm was never added to the standard library",
+ DeprecationWarning)
+ return self._recreate_cm()
+
def _recreate_cm(self):
"""Return a recreated instance of self.
@@ -520,7 +577,7 @@
try:
_enter = cls.__enter__
_exit = cls.__exit__
- except AttributeError:
+ except _CL2_ERROR_TO_CONVERT:
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol") from None
result = _enter(cm)
@@ -652,7 +709,7 @@
try:
_enter = cls.__aenter__
_exit = cls.__aexit__
- except AttributeError:
+ except _CL2_ERROR_TO_CONVERT:
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the asynchronous context manager protocol"
) from None
@@ -798,3 +855,22 @@
def __exit__(self, *excinfo):
os.chdir(self._old_cwd.pop())
+
+# Preserve backwards compatibility
+class ContextStack(ExitStack):
+ """(DEPRECATED) Backwards compatibility alias for ExitStack"""
+
+ def __init__(self):
+ import warnings # Only import if needed for the deprecation warning
+ warnings.warn("ContextStack has been renamed to ExitStack",
+ DeprecationWarning)
+ super(ContextStack, self).__init__()
+
+ def register_exit(self, callback):
+ return self.push(callback)
+
+ def register(self, callback, *args, **kwds):
+ return self.callback(callback, *args, **kwds)
+
+ def preserve(self):
+ return self.pop_all()

View file

@ -0,0 +1,112 @@
--- /home/ncoghlan/devel/contextlib2/dev/typeshed_contextlib.pyi 2024-05-23 12:40:10.170754997 +1000
+++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.pyi 2024-05-23 16:47:15.874656809 +1000
@@ -1,3 +1,20 @@
+# Type hints copied from the typeshed project under the Apache License 2.0
+# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE
+
+# For updates: https://github.com/python/typeshed/blob/main/stdlib/contextlib.pyi
+
+# Last updated: 2024-05-22
+# Updated from: https://github.com/python/typeshed/blob/aa2d33df211e1e4f70883388febf750ac524d2bb/stdlib/contextlib.pyi
+# Saved to: dev/typeshed_contextlib.pyi
+
+# contextlib2 API adaptation notes:
+# * the various 'if True:' guards replace sys.version checks in the original
+# typeshed file (those APIs are available on all supported versions)
+# * any commented out 'if True:' guards replace sys.version checks in the original
+# typeshed file where the affected APIs haven't been backported yet
+# * deliberately omitted APIs are listed in `dev/mypy.allowlist`
+# (e.g. deprecated experimental APIs that never graduated to the stdlib)
+
import abc
import sys
from _typeshed import FileDescriptorOrPath, Unused
@@ -22,10 +39,10 @@
"nullcontext",
]
-if sys.version_info >= (3, 10):
+if True:
__all__ += ["aclosing"]
-if sys.version_info >= (3, 11):
+if True:
__all__ += ["chdir"]
_T = TypeVar("_T")
@@ -65,18 +82,14 @@
func: Callable[..., Generator[_T_co, Any, Any]]
args: tuple[Any, ...]
kwds: dict[str, Any]
- if sys.version_info >= (3, 9):
+ if True:
def __exit__(
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> bool | None: ...
- else:
- def __exit__(
- self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
- ) -> bool | None: ...
def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ...
-if sys.version_info >= (3, 10):
+if True:
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
class AsyncContextDecorator:
@@ -94,17 +107,6 @@
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> bool | None: ...
-else:
- class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]):
- def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
- gen: AsyncGenerator[_T_co, Any]
- func: Callable[..., AsyncGenerator[_T_co, Any]]
- args: tuple[Any, ...]
- kwds: dict[str, Any]
- async def __aexit__(
- self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
- ) -> bool | None: ...
-
def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ...
class _SupportsClose(Protocol):
@@ -116,7 +118,7 @@
def __init__(self, thing: _SupportsCloseT) -> None: ...
def __exit__(self, *exc_info: Unused) -> None: ...
-if sys.version_info >= (3, 10):
+if True:
class _SupportsAclose(Protocol):
def aclose(self) -> Awaitable[object]: ...
@@ -177,7 +179,7 @@
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> bool: ...
-if sys.version_info >= (3, 10):
+if True:
class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]):
enter_result: _T
@overload
@@ -189,17 +191,7 @@
async def __aenter__(self) -> _T: ...
async def __aexit__(self, *exctype: Unused) -> None: ...
-else:
- class nullcontext(AbstractContextManager[_T, None]):
- enter_result: _T
- @overload
- def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
- @overload
- def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
- def __enter__(self) -> _T: ...
- def __exit__(self, *exctype: Unused) -> None: ...
-
-if sys.version_info >= (3, 11):
+if True:
_T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath)
class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]):

View file

@ -0,0 +1,453 @@
--- /home/ncoghlan/devel/contextlib2/../cpython/Doc/library/contextlib.rst 2024-05-20 12:53:59.936907756 +1000
+++ /home/ncoghlan/devel/contextlib2/docs/contextlib2.rst 2024-05-23 17:39:52.671083724 +1000
@@ -1,20 +1,5 @@
-:mod:`!contextlib` --- Utilities for :keyword:`!with`\ -statement contexts
-==========================================================================
-
-.. module:: contextlib
- :synopsis: Utilities for with-statement contexts.
-
-**Source code:** :source:`Lib/contextlib.py`
-
---------------
-
-This module provides utilities for common tasks involving the :keyword:`with`
-statement. For more information see also :ref:`typecontextmanager` and
-:ref:`context-managers`.
-
-
-Utilities
----------
+API Reference
+-------------
Functions and classes provided:
@@ -26,8 +11,8 @@
``self`` while :meth:`object.__exit__` is an abstract method which by default
returns ``None``. See also the definition of :ref:`typecontextmanager`.
- .. versionadded:: 3.6
-
+ .. versionadded:: 0.6.0
+ Part of the standard library in Python 3.6 and later
.. class:: AbstractAsyncContextManager
@@ -38,8 +23,8 @@
returns ``None``. See also the definition of
:ref:`async-context-managers`.
- .. versionadded:: 3.7
-
+ .. versionadded:: 21.6.0
+ Part of the standard library in Python 3.7 and later
.. decorator:: contextmanager
@@ -49,12 +34,12 @@
While many objects natively support use in with statements, sometimes a
resource needs to be managed that isn't a context manager in its own right,
- and doesn't implement a ``close()`` method for use with ``contextlib.closing``
+ and doesn't implement a ``close()`` method for use with ``contextlib2.closing``
An abstract example would be the following to ensure correct resource
management::
- from contextlib import contextmanager
+ from contextlib2 import contextmanager
@contextmanager
def managed_resource(*args, **kwds):
@@ -95,13 +80,10 @@
created by :func:`contextmanager` to meet the requirement that context
managers support multiple invocations in order to be used as decorators).
- .. versionchanged:: 3.2
- Use of :class:`ContextDecorator`.
-
.. decorator:: asynccontextmanager
- Similar to :func:`~contextlib.contextmanager`, but creates an
+ Similar to :func:`~contextlib2.contextmanager`, but creates an
:ref:`asynchronous context manager <async-context-managers>`.
This function is a :term:`decorator` that can be used to define a factory
@@ -112,7 +94,7 @@
A simple example::
- from contextlib import asynccontextmanager
+ from contextlib2 import asynccontextmanager
@asynccontextmanager
async def get_connection():
@@ -126,13 +108,16 @@
async with get_connection() as conn:
return conn.query('SELECT ...')
- .. versionadded:: 3.7
+ .. versionadded:: 21.6.0
+ Part of the standard library in Python 3.7 and later, enhanced in
+ Python 3.10 and later to allow created async context managers to be used
+ as async function decorators.
Context managers defined with :func:`asynccontextmanager` can be used
either as decorators or with :keyword:`async with` statements::
import time
- from contextlib import asynccontextmanager
+ from contextlib2 import asynccontextmanager
@asynccontextmanager
async def timeit():
@@ -151,17 +136,13 @@
created by :func:`asynccontextmanager` to meet the requirement that context
managers support multiple invocations in order to be used as decorators.
- .. versionchanged:: 3.10
- Async context managers created with :func:`asynccontextmanager` can
- be used as decorators.
-
.. function:: closing(thing)
Return a context manager that closes *thing* upon completion of the block. This
is basically equivalent to::
- from contextlib import contextmanager
+ from contextlib2 import contextmanager
@contextmanager
def closing(thing):
@@ -172,7 +153,7 @@
And lets you write code like this::
- from contextlib import closing
+ from contextlib2 import closing
from urllib.request import urlopen
with closing(urlopen('https://www.python.org')) as page:
@@ -196,7 +177,7 @@
Return an async context manager that calls the ``aclose()`` method of *thing*
upon completion of the block. This is basically equivalent to::
- from contextlib import asynccontextmanager
+ from contextlib2 import asynccontextmanager
@asynccontextmanager
async def aclosing(thing):
@@ -209,7 +190,7 @@
generators when they happen to exit early by :keyword:`break` or an
exception. For example::
- from contextlib import aclosing
+ from contextlib2 import aclosing
async with aclosing(my_generator()) as values:
async for value in values:
@@ -221,7 +202,8 @@
variables work as expected, and the exit code isn't run after the
lifetime of some task it depends on).
- .. versionadded:: 3.10
+ .. versionadded:: 21.6.0
+ Part of the standard library in Python 3.10 and later
.. _simplifying-support-for-single-optional-context-managers:
@@ -235,10 +217,10 @@
def myfunction(arg, ignore_exceptions=False):
if ignore_exceptions:
# Use suppress to ignore all exceptions.
- cm = contextlib.suppress(Exception)
+ cm = contextlib2.suppress(Exception)
else:
# Do not ignore any exceptions, cm has no effect.
- cm = contextlib.nullcontext()
+ cm = contextlib2.nullcontext()
with cm:
# Do something
@@ -269,11 +251,11 @@
async with cm as session:
# Send http requests with session
- .. versionadded:: 3.7
-
- .. versionchanged:: 3.10
- :term:`asynchronous context manager` support was added.
+ .. versionadded:: 0.6.0
+ Part of the standard library in Python 3.7 and later
+ .. versionchanged:: 21.6.0
+ Updated to Python 3.10 version with :term:`asynchronous context manager` support
.. function:: suppress(*exceptions)
@@ -290,7 +272,7 @@
For example::
- from contextlib import suppress
+ from contextlib2 import suppress
with suppress(FileNotFoundError):
os.remove('somefile.tmp')
@@ -314,13 +296,15 @@
If the code within the :keyword:`!with` block raises a
:exc:`BaseExceptionGroup`, suppressed exceptions are removed from the
- group. If any exceptions in the group are not suppressed, a group containing them is re-raised.
+ group. If any exceptions in the group are not suppressed, a group containing
+ them is re-raised.
- .. versionadded:: 3.4
+ .. versionadded:: 0.5
+ Part of the standard library in Python 3.4 and later
- .. versionchanged:: 3.12
- ``suppress`` now supports suppressing exceptions raised as
- part of an :exc:`BaseExceptionGroup`.
+ .. versionchanged:: 24.6.0
+ Updated to Python 3.12 version that supports suppressing exceptions raised
+ as part of a :exc:`BaseExceptionGroup`.
.. function:: redirect_stdout(new_target)
@@ -359,17 +343,19 @@
This context manager is :ref:`reentrant <reentrant-cms>`.
- .. versionadded:: 3.4
+ .. versionadded:: 0.5
+ Part of the standard library in Python 3.4 and later
.. function:: redirect_stderr(new_target)
- Similar to :func:`~contextlib.redirect_stdout` but redirecting
+ Similar to :func:`~contextlib2.redirect_stdout` but redirecting
:data:`sys.stderr` to another file or file-like object.
This context manager is :ref:`reentrant <reentrant-cms>`.
- .. versionadded:: 3.5
+ .. versionadded:: 0.5
+ Part of the standard library in Python 3.5 and later
.. function:: chdir(path)
@@ -386,7 +372,8 @@
This context manager is :ref:`reentrant <reentrant-cms>`.
- .. versionadded:: 3.11
+ .. versionadded:: 24.6.0
+ Part of the standard library in Python 3.11 and later
.. class:: ContextDecorator()
@@ -402,7 +389,7 @@
Example of ``ContextDecorator``::
- from contextlib import ContextDecorator
+ from contextlib2 import ContextDecorator
class mycontext(ContextDecorator):
def __enter__(self):
@@ -449,7 +436,7 @@
Existing context managers that already have a base class can be extended by
using ``ContextDecorator`` as a mixin class::
- from contextlib import ContextDecorator
+ from contextlib2 import ContextDecorator
class mycontext(ContextBaseClass, ContextDecorator):
def __enter__(self):
@@ -464,8 +451,6 @@
statements. If this is not the case, then the original construct with the
explicit :keyword:`!with` statement inside the function should be used.
- .. versionadded:: 3.2
-
.. class:: AsyncContextDecorator
@@ -474,7 +459,7 @@
Example of ``AsyncContextDecorator``::
from asyncio import run
- from contextlib import AsyncContextDecorator
+ from contextlib2 import AsyncContextDecorator
class mycontext(AsyncContextDecorator):
async def __aenter__(self):
@@ -505,7 +490,8 @@
The bit in the middle
Finishing
- .. versionadded:: 3.10
+ .. versionadded:: 21.6.0
+ Part of the standard library in Python 3.10 and later
.. class:: ExitStack()
@@ -547,7 +533,8 @@
foundation for higher level context managers that manipulate the exit
stack in application specific ways.
- .. versionadded:: 3.3
+ .. versionadded:: 0.4
+ Part of the standard library in Python 3.3 and later
.. method:: enter_context(cm)
@@ -558,9 +545,10 @@
These context managers may suppress exceptions just as they normally
would if used directly as part of a :keyword:`with` statement.
- .. versionchanged:: 3.11
- Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
- is not a context manager.
+ .. versionchanged:: 24.6.0
+ When running on Python 3.11 or later, raises :exc:`TypeError` instead
+ of :exc:`AttributeError` if *cm* is not a context manager. This aligns
+ with the behaviour of :keyword:`with` statements in Python 3.11+.
.. method:: push(exit)
@@ -627,14 +615,16 @@
The :meth:`~ExitStack.close` method is not implemented; :meth:`aclose` must be used
instead.
- .. coroutinemethod:: enter_async_context(cm)
+ .. method:: enter_async_context(cm)
+ :async:
Similar to :meth:`ExitStack.enter_context` but expects an asynchronous context
manager.
- .. versionchanged:: 3.11
- Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm*
- is not an asynchronous context manager.
+ .. versionchanged:: 24.6.0
+ When running on Python 3.11 or later, raises :exc:`TypeError` instead
+ of :exc:`AttributeError` if *cm* is not an asynchronous context manager.
+ This aligns with the behaviour of ``async with`` statements in Python 3.11+.
.. method:: push_async_exit(exit)
@@ -645,7 +635,8 @@
Similar to :meth:`ExitStack.callback` but expects a coroutine function.
- .. coroutinemethod:: aclose()
+ .. method:: aclose()
+ :async:
Similar to :meth:`ExitStack.close` but properly handles awaitables.
@@ -658,13 +649,15 @@
# the async with statement, even if attempts to open a connection
# later in the list raise an exception.
- .. versionadded:: 3.7
+ .. versionadded:: 21.6.0
+ Part of the standard library in Python 3.7 and later
+
Examples and Recipes
--------------------
This section describes some examples and recipes for making effective use of
-the tools provided by :mod:`contextlib`.
+the tools provided by :mod:`contextlib2`.
Supporting a variable number of context managers
@@ -728,7 +721,7 @@
acquisition and release functions, along with an optional validation function,
and maps them to the context management protocol::
- from contextlib import contextmanager, AbstractContextManager, ExitStack
+ from contextlib2 import contextmanager, AbstractContextManager, ExitStack
class ResourceManager(AbstractContextManager):
@@ -788,7 +781,7 @@
execution at the end of a ``with`` statement, and then later decide to skip
executing that callback::
- from contextlib import ExitStack
+ from contextlib2 import ExitStack
with ExitStack() as stack:
stack.callback(cleanup_resources)
@@ -802,7 +795,7 @@
If a particular application uses this pattern a lot, it can be simplified
even further by means of a small helper class::
- from contextlib import ExitStack
+ from contextlib2 import ExitStack
class Callback(ExitStack):
def __init__(self, callback, /, *args, **kwds):
@@ -822,7 +815,7 @@
:meth:`ExitStack.callback` to declare the resource cleanup in
advance::
- from contextlib import ExitStack
+ from contextlib2 import ExitStack
with ExitStack() as stack:
@stack.callback
@@ -849,7 +842,7 @@
inheriting from :class:`ContextDecorator` provides both capabilities in a
single definition::
- from contextlib import ContextDecorator
+ from contextlib2 import ContextDecorator
import logging
logging.basicConfig(level=logging.INFO)
@@ -911,7 +904,7 @@
context managers, and will complain about the underlying generator failing
to yield if an attempt is made to use them a second time::
- >>> from contextlib import contextmanager
+ >>> from contextlib2 import contextmanager
>>> @contextmanager
... def singleuse():
... print("Before")
@@ -946,7 +939,7 @@
:func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very
simple example of reentrant use::
- >>> from contextlib import redirect_stdout
+ >>> from contextlib2 import redirect_stdout
>>> from io import StringIO
>>> stream = StringIO()
>>> write_to_stream = redirect_stdout(stream)
@@ -992,7 +985,7 @@
when leaving any with statement, regardless of where those callbacks
were added::
- >>> from contextlib import ExitStack
+ >>> from contextlib2 import ExitStack
>>> stack = ExitStack()
>>> with stack:
... stack.callback(print, "Callback: from first context")
@@ -1026,7 +1019,7 @@
Using separate :class:`ExitStack` instances instead of reusing a single
instance avoids that problem::
- >>> from contextlib import ExitStack
+ >>> from contextlib2 import ExitStack
>>> with ExitStack() as outer_stack:
... outer_stack.callback(print, "Callback: from outer context")
... with ExitStack() as inner_stack:

View file

@ -0,0 +1,67 @@
--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib_async.py 2024-05-23 11:57:09.276022441 +1000
+++ /home/ncoghlan/devel/contextlib2/test/test_contextlib_async.py 2024-05-23 17:39:05.799797895 +1000
@@ -1,5 +1,7 @@
+"""Unit tests for asynchronous features of contextlib2.py"""
+
import asyncio
-from contextlib import (
+from contextlib2 import (
asynccontextmanager, AbstractAsyncContextManager,
AsyncExitStack, nullcontext, aclosing, contextmanager)
import functools
@@ -7,7 +9,7 @@
import unittest
import traceback
-from test.test_contextlib import TestBaseExitStack
+from .test_contextlib import TestBaseExitStack
support.requires_working_socket(module=True)
@@ -202,7 +204,8 @@
await ctx.__aexit__(TypeError, TypeError('foo'), None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
- self.assertFalse(ctx.gen.ag_suspended)
+ if support.cl2_async_gens_have_ag_suspended:
+ self.assertFalse(ctx.gen.ag_suspended)
@_async_test
async def test_contextmanager_trap_no_yield(self):
@@ -226,7 +229,8 @@
await ctx.__aexit__(None, None, None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
- self.assertFalse(ctx.gen.ag_suspended)
+ if support.cl2_async_gens_have_ag_suspended:
+ self.assertFalse(ctx.gen.ag_suspended)
@_async_test
async def test_contextmanager_non_normalised(self):
@@ -669,12 +673,13 @@
async def __aenter__(self):
pass
+ expected_error, expected_text = support.cl2_cm_api_exc_info_async()
async with self.exit_stack() as stack:
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
+ with self.assertRaisesRegex(expected_error, expected_text):
await stack.enter_async_context(LacksEnterAndExit())
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
+ with self.assertRaisesRegex(expected_error, expected_text):
await stack.enter_async_context(LacksEnter())
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
+ with self.assertRaisesRegex(expected_error, expected_text):
await stack.enter_async_context(LacksExit())
self.assertFalse(stack._exit_callbacks)
@@ -752,7 +757,8 @@
cm.__aenter__ = object()
cm.__aexit__ = object()
stack = self.exit_stack()
- with self.assertRaisesRegex(TypeError, 'asynchronous context manager'):
+ expected_error, expected_text = support.cl2_cm_api_exc_info_async()
+ with self.assertRaisesRegex(expected_error, expected_text):
await stack.enter_async_context(cm)
stack.push_async_exit(cm)
self.assertIs(stack._exit_callbacks[-1][1], cm)

View file

@ -0,0 +1,106 @@
--- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib.py 2024-05-23 11:57:09.276022441 +1000
+++ /home/ncoghlan/devel/contextlib2/test/test_contextlib.py 2024-05-23 17:38:37.295232213 +1000
@@ -1,4 +1,4 @@
-"""Unit tests for contextlib.py, and other context managers."""
+"""Unit tests for synchronous features of contextlib2.py"""
import io
import os
@@ -7,7 +7,7 @@
import threading
import traceback
import unittest
-from contextlib import * # Tests __all__
+from contextlib2 import * # Tests __all__
from test import support
from test.support import os_helper
from test.support.testcase import ExceptionIsLikeMixin
@@ -161,7 +161,8 @@
ctx.__exit__(TypeError, TypeError("foo"), None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
- self.assertFalse(ctx.gen.gi_suspended)
+ if support.cl2_gens_have_gi_suspended:
+ self.assertFalse(ctx.gen.gi_suspended)
def test_contextmanager_trap_no_yield(self):
@contextmanager
@@ -183,7 +184,8 @@
ctx.__exit__(None, None, None)
if support.check_impl_detail(cpython=True):
# The "gen" attribute is an implementation detail.
- self.assertFalse(ctx.gen.gi_suspended)
+ if support.cl2_gens_have_gi_suspended:
+ self.assertFalse(ctx.gen.gi_suspended)
def test_contextmanager_non_normalised(self):
@contextmanager
@@ -610,7 +612,8 @@
def __exit__(self, *exc):
pass
- with self.assertRaisesRegex(TypeError, 'the context manager'):
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__enter__")
+ with self.assertRaisesRegex(expected_error, expected_text):
with mycontext():
pass
@@ -622,7 +625,8 @@
def __uxit__(self, *exc):
pass
- with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'):
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__exit__")
+ with self.assertRaisesRegex(expected_error, expected_text):
with mycontext():
pass
@@ -790,12 +794,13 @@
def __enter__(self):
pass
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync()
with self.exit_stack() as stack:
- with self.assertRaisesRegex(TypeError, 'the context manager'):
+ with self.assertRaisesRegex(expected_error, expected_text):
stack.enter_context(LacksEnterAndExit())
- with self.assertRaisesRegex(TypeError, 'the context manager'):
+ with self.assertRaisesRegex(expected_error, expected_text):
stack.enter_context(LacksEnter())
- with self.assertRaisesRegex(TypeError, 'the context manager'):
+ with self.assertRaisesRegex(expected_error, expected_text):
stack.enter_context(LacksExit())
self.assertFalse(stack._exit_callbacks)
@@ -858,8 +863,11 @@
[('_exit_wrapper', 'callback(*args, **kwds)'),
('raise_exc', 'raise exc')]
- self.assertEqual(
- [(f.name, f.line) for f in ve_frames], expected)
+ # This check fails on PyPy 3.10
+ # It also fails on CPython 3.9 and earlier versions
+ if support.check_impl_detail(cpython=True) and support.cl2_check_traceback_details:
+ self.assertEqual(
+ [(f.name, f.line) for f in ve_frames], expected)
self.assertIsInstance(exc.__context__, ZeroDivisionError)
zde_frames = traceback.extract_tb(exc.__context__.__traceback__)
@@ -1093,7 +1101,8 @@
cm.__enter__ = object()
cm.__exit__ = object()
stack = self.exit_stack()
- with self.assertRaisesRegex(TypeError, 'the context manager'):
+ expected_error, expected_text = support.cl2_cm_api_exc_info_sync()
+ with self.assertRaisesRegex(expected_error, expected_text):
stack.enter_context(cm)
stack.push(cm)
self.assertIs(stack._exit_callbacks[-1][1], cm)
@@ -1264,6 +1273,7 @@
1/0
self.assertTrue(outer_continued)
+ @support.cl2_requires_exception_groups
def test_exception_groups(self):
eg_ve = lambda: ExceptionGroup(
"EG with ValueErrors only",

27
dev/save_diff_snapshot.sh Executable file
View file

@ -0,0 +1,27 @@
#!/bin/sh
git_root="$(git rev-parse --show-toplevel)"
cpython_dir="${1:-$git_root/../cpython}"
diff_prefix="py3_12" # Update based on the version being synced
function diff_file()
{
diff -ud "$2" "$git_root/$3" > "$git_root/dev/${diff_prefix}_$1.patch"
}
diff_file rst_to_contextlib2 \
"$cpython_dir/Doc/library/contextlib.rst" "docs/contextlib2.rst"
diff_file py_to_contextlib2 \
"$cpython_dir/Lib/contextlib.py" "contextlib2/__init__.py"
diff_file pyi_to_contextlib2 \
"$git_root/dev/typeshed_contextlib.pyi" "contextlib2/__init__.pyi"
diff_file test_to_contextlib2 \
"$cpython_dir/Lib/test/test_contextlib.py" "test/test_contextlib.py"
diff_file test_async_to_contextlib2 \
"$cpython_dir/Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py"

19
dev/sync_from_cpython.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
git_root="$(git rev-parse --show-toplevel)"
cpython_dir="${1:-$git_root/../cpython}" # Folder with relevant CPython version
function sync_file()
{
cp -fv "$cpython_dir/$1" "$git_root/$2"
}
sync_file "Doc/library/contextlib.rst" "docs/contextlib2.rst"
sync_file "Lib/contextlib.py" "contextlib2/__init__.py"
sync_file "Lib/test/test_contextlib.py" "test/test_contextlib.py"
sync_file "Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py"
echo
echo "Note: Update the 'contextlib2/__init__.pyi' stub as described in the file"
echo

209
dev/typeshed_contextlib.pyi Normal file
View file

@ -0,0 +1,209 @@
import abc
import sys
from _typeshed import FileDescriptorOrPath, Unused
from abc import abstractmethod
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator
from types import TracebackType
from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable
from typing_extensions import ParamSpec, Self, TypeAlias
__all__ = [
"contextmanager",
"closing",
"AbstractContextManager",
"ContextDecorator",
"ExitStack",
"redirect_stdout",
"redirect_stderr",
"suppress",
"AbstractAsyncContextManager",
"AsyncExitStack",
"asynccontextmanager",
"nullcontext",
]
if sys.version_info >= (3, 10):
__all__ += ["aclosing"]
if sys.version_info >= (3, 11):
__all__ += ["chdir"]
_T = TypeVar("_T")
_T_co = TypeVar("_T_co", covariant=True)
_T_io = TypeVar("_T_io", bound=IO[str] | None)
_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None)
_F = TypeVar("_F", bound=Callable[..., Any])
_P = ParamSpec("_P")
_ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None]
_CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc)
@runtime_checkable
class AbstractContextManager(Protocol[_T_co, _ExitT_co]):
def __enter__(self) -> _T_co: ...
@abstractmethod
def __exit__(
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> _ExitT_co: ...
@runtime_checkable
class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]):
async def __aenter__(self) -> _T_co: ...
@abstractmethod
async def __aexit__(
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> _ExitT_co: ...
class ContextDecorator:
def __call__(self, func: _F) -> _F: ...
class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator):
# __init__ and all instance attributes are actually inherited from _GeneratorContextManagerBase
# _GeneratorContextManagerBase is more trouble than it's worth to include in the stub; see #6676
def __init__(self, func: Callable[..., Iterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
gen: Generator[_T_co, Any, Any]
func: Callable[..., Generator[_T_co, Any, Any]]
args: tuple[Any, ...]
kwds: dict[str, Any]
if sys.version_info >= (3, 9):
def __exit__(
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> bool | None: ...
else:
def __exit__(
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> bool | None: ...
def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ...
if sys.version_info >= (3, 10):
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
class AsyncContextDecorator:
def __call__(self, func: _AF) -> _AF: ...
class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator):
# __init__ and these attributes are actually defined in the base class _GeneratorContextManagerBase,
# which is more trouble than it's worth to include in the stub (see #6676)
def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
gen: AsyncGenerator[_T_co, Any]
func: Callable[..., AsyncGenerator[_T_co, Any]]
args: tuple[Any, ...]
kwds: dict[str, Any]
async def __aexit__(
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> bool | None: ...
else:
class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]):
def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ...
gen: AsyncGenerator[_T_co, Any]
func: Callable[..., AsyncGenerator[_T_co, Any]]
args: tuple[Any, ...]
kwds: dict[str, Any]
async def __aexit__(
self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
) -> bool | None: ...
def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ...
class _SupportsClose(Protocol):
def close(self) -> object: ...
_SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose)
class closing(AbstractContextManager[_SupportsCloseT, None]):
def __init__(self, thing: _SupportsCloseT) -> None: ...
def __exit__(self, *exc_info: Unused) -> None: ...
if sys.version_info >= (3, 10):
class _SupportsAclose(Protocol):
def aclose(self) -> Awaitable[object]: ...
_SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose)
class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]):
def __init__(self, thing: _SupportsAcloseT) -> None: ...
async def __aexit__(self, *exc_info: Unused) -> None: ...
class suppress(AbstractContextManager[None, bool]):
def __init__(self, *exceptions: type[BaseException]) -> None: ...
def __exit__(
self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None
) -> bool: ...
class _RedirectStream(AbstractContextManager[_T_io, None]):
def __init__(self, new_target: _T_io) -> None: ...
def __exit__(
self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None
) -> None: ...
class redirect_stdout(_RedirectStream[_T_io]): ...
class redirect_stderr(_RedirectStream[_T_io]): ...
# In reality this is a subclass of `AbstractContextManager`;
# see #7961 for why we don't do that in the stub
class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta):
def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ...
def push(self, exit: _CM_EF) -> _CM_EF: ...
def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ...
def pop_all(self) -> Self: ...
def close(self) -> None: ...
def __enter__(self) -> Self: ...
def __exit__(
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> _ExitT_co: ...
_ExitCoroFunc: TypeAlias = Callable[
[type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None]
]
_ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc)
# In reality this is a subclass of `AbstractAsyncContextManager`;
# see #7961 for why we don't do that in the stub
class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta):
def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ...
async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ...
def push(self, exit: _CM_EF) -> _CM_EF: ...
def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ...
def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ...
def push_async_callback(
self, callback: Callable[_P, Awaitable[_T]], /, *args: _P.args, **kwds: _P.kwargs
) -> Callable[_P, Awaitable[_T]]: ...
def pop_all(self) -> Self: ...
async def aclose(self) -> None: ...
async def __aenter__(self) -> Self: ...
async def __aexit__(
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /
) -> bool: ...
if sys.version_info >= (3, 10):
class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]):
enter_result: _T
@overload
def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
@overload
def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
def __enter__(self) -> _T: ...
def __exit__(self, *exctype: Unused) -> None: ...
async def __aenter__(self) -> _T: ...
async def __aexit__(self, *exctype: Unused) -> None: ...
else:
class nullcontext(AbstractContextManager[_T, None]):
enter_result: _T
@overload
def __init__(self: nullcontext[None], enter_result: None = None) -> None: ...
@overload
def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780
def __enter__(self) -> _T: ...
def __exit__(self, *exctype: Unused) -> None: ...
if sys.version_info >= (3, 11):
_T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath)
class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]):
path: _T_fd_or_any_path
def __init__(self, path: _T_fd_or_any_path) -> None: ...
def __enter__(self) -> None: ...
def __exit__(self, *excinfo: Unused) -> None: ...

View file

@ -11,8 +11,6 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@ -25,7 +23,10 @@ import sys, os
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.intersphinx']
extensions = [
'sphinx.ext.intersphinx',
'sphinx_rtd_theme',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -41,7 +42,7 @@ master_doc = 'index'
# General information about the project.
project = u'contextlib2'
copyright = u'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

File diff suppressed because it is too large Load diff

View file

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

1
docs/requirements.txt Normal file
View file

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

View file

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

View file

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

@ -0,0 +1 @@
# unittest test discovery requires an __init__.py file in the test directory

1
test/data/README.txt Normal file
View file

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

101
test/support/__init__.py Normal file
View 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)

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

@ -0,0 +1,33 @@
"""Enough of the test.support.testcase APIs to run the contextlib test suite"""
from . import cl2_have_exception_groups
if not cl2_have_exception_groups:
# Placeholder to let the isinstance check below run on older versions
class ExceptionGroup(Exception):
pass
class ExceptionIsLikeMixin:
def assertExceptionIsLike(self, exc, template):
"""
Passes when the provided `exc` matches the structure of `template`.
Individual exceptions don't have to be the same objects or even pass
an equality test: they only need to be the same type and contain equal
`exc_obj.args`.
"""
if exc is None and template is None:
return
if template is None:
self.fail(f"unexpected exception: {exc}")
if exc is None:
self.fail(f"expected an exception like {template!r}, got None")
if not isinstance(exc, ExceptionGroup):
self.assertEqual(exc.__class__, template.__class__)
self.assertEqual(exc.args[0], template.args[0])
else:
self.assertEqual(exc.message, template.message)
self.assertEqual(len(exc.exceptions), len(template.exceptions))
for e, t in zip(exc.exceptions, template.exceptions):
self.assertExceptionIsLike(e, t)

762
test_contextlib2.py → test/test_contextlib.py Executable file → Normal file

File diff suppressed because it is too large Load diff

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

View file

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

24
tox.ini
View file

@ -1,18 +1,24 @@
[tox]
envlist = py{26,27,33,34,35,36,py,py3}
# Python 3.8 is the first version with positional-only argument syntax support
envlist = py{38,39,3.10,3.11,3.12,py3}
skip_missing_interpreters = True
[testenv]
commands =
coverage erase
coverage run test_contextlib2.py
coverage run -m unittest discover -t . -s test
coverage report
coverage xml
# mypy won't install on PyPy, so only run the typechecking on CPython
!pypy3: python -m mypy.stubtest --allowlist dev/mypy.allowlist contextlib2
deps =
coverage
py26: unittest2
py27: unittest2
pypy: unittest2
!pypy3: mypy
[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