├── contextlib2 ├── py.typed ├── __init__.pyi └── __init__.py ├── VERSION.txt ├── docs ├── requirements.txt ├── index.rst ├── Makefile ├── make.bat ├── conf.py └── contextlib2.rst ├── test ├── data │ └── README.txt ├── ziptestdata │ └── README.txt ├── __init__.py ├── support │ ├── os_helper.py │ ├── testcase.py │ └── __init__.py └── test_contextlib_async.py ├── .coveragerc ├── .gitignore ├── MANIFEST.in ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── dev ├── mypy.allowlist ├── sync_from_cpython.sh ├── save_diff_snapshot.sh ├── py3_12_test_async_to_contextlib2.patch ├── py3_12_test_to_contextlib2.patch ├── py3_12_py_to_contextlib2.patch ├── py3_12_pyi_to_contextlib2.patch ├── typeshed_contextlib.pyi └── py3_12_rst_to_contextlib2.patch ├── tox.ini ├── .readthedocs.yaml ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── setup.py ├── CODE_OF_CONDUCT.md ├── README.rst ├── LICENSE.txt └── NEWS.rst /contextlib2/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 24.6.0rc1 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme 2 | -------------------------------------------------------------------------------- /test/data/README.txt: -------------------------------------------------------------------------------- 1 | test_contextlib uses this folder for chdir tests 2 | -------------------------------------------------------------------------------- /test/ziptestdata/README.txt: -------------------------------------------------------------------------------- 1 | test_contextlib uses this folder for chdir tests 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = contextlib2 3 | branch = 1 4 | 5 | [report] 6 | omit = *test* 7 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # unittest test discovery requires an __init__.py file in the test directory 2 | -------------------------------------------------------------------------------- /test/support/os_helper.py: -------------------------------------------------------------------------------- 1 | """Enough of the test.support.os_helper APIs to run the contextlib test suite""" 2 | import os 3 | 4 | unlink = os.unlink 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | docs/_build/ 4 | pip.egg-info/ 5 | MANIFEST 6 | .tox 7 | *.egg 8 | *.egg-info 9 | *.py[cod] 10 | *~ 11 | .coverage 12 | coverage.xml 13 | htmlcov/ 14 | 15 | # Patching output files 16 | *.orig 17 | *.rej 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py *.cfg *.txt *.rst *.md *.ini MANIFEST.in 2 | recursive-include contextlib2 *.py *.pyi py.typed 3 | recursive-include docs *.rst *.py make.bat Makefile 4 | recursive-include test *.py 5 | recursive-include dev *.patch *.allowlist *.sh 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree 4 | to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) 5 | and follow the [guidelines](https://jazzband.co/about/guidelines). 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pygrep-hooks 3 | rev: v1.10.0 4 | hooks: 5 | - id: python-check-blanket-noqa 6 | 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v6.0.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: check-yaml 12 | 13 | ci: 14 | autoupdate_schedule: quarterly 15 | -------------------------------------------------------------------------------- /dev/mypy.allowlist: -------------------------------------------------------------------------------- 1 | # Deprecated APIs that never graduated to the standard library 2 | contextlib2.ContextDecorator.refresh_cm 3 | 4 | # stubcheck no longer complains about this one for some reason 5 | # (but it does complain about the unused allowlist entry) 6 | # contextlib2.ContextStack 7 | 8 | # mypy seems to be confused by the GenericAlias compatibility hack 9 | contextlib2.AbstractAsyncContextManager.__class_getitem__ 10 | contextlib2.AbstractContextManager.__class_getitem__ 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Python 3.8 is the first version with positional-only argument syntax support 3 | envlist = py{38,39,3.10,3.11,3.12,py3} 4 | skip_missing_interpreters = True 5 | 6 | [testenv] 7 | commands = 8 | coverage run -m unittest discover -t . -s test 9 | coverage report 10 | coverage xml 11 | # mypy won't install on PyPy, so only run the typechecking on CPython 12 | !pypy3: python -m mypy.stubtest --allowlist dev/mypy.allowlist contextlib2 13 | deps = 14 | coverage 15 | !pypy3: mypy 16 | 17 | [gh-actions] 18 | python = 19 | 3.8: py38 20 | 3.9: py39 21 | 3.10: py3.10 22 | 3.11: py3.11 23 | 3.12: py3.12 24 | pypy-3.10: pypy3 25 | -------------------------------------------------------------------------------- /dev/sync_from_cpython.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git_root="$(git rev-parse --show-toplevel)" 4 | 5 | cpython_dir="${1:-$git_root/../cpython}" # Folder with relevant CPython version 6 | 7 | function sync_file() 8 | { 9 | cp -fv "$cpython_dir/$1" "$git_root/$2" 10 | } 11 | 12 | sync_file "Doc/library/contextlib.rst" "docs/contextlib2.rst" 13 | sync_file "Lib/contextlib.py" "contextlib2/__init__.py" 14 | sync_file "Lib/test/test_contextlib.py" "test/test_contextlib.py" 15 | sync_file "Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py" 16 | 17 | echo 18 | echo "Note: Update the 'contextlib2/__init__.pyi' stub as described in the file" 19 | echo 20 | -------------------------------------------------------------------------------- /dev/save_diff_snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git_root="$(git rev-parse --show-toplevel)" 4 | 5 | cpython_dir="${1:-$git_root/../cpython}" 6 | 7 | diff_prefix="py3_12" # Update based on the version being synced 8 | 9 | function diff_file() 10 | { 11 | diff -ud "$2" "$git_root/$3" > "$git_root/dev/${diff_prefix}_$1.patch" 12 | } 13 | 14 | diff_file rst_to_contextlib2 \ 15 | "$cpython_dir/Doc/library/contextlib.rst" "docs/contextlib2.rst" 16 | 17 | diff_file py_to_contextlib2 \ 18 | "$cpython_dir/Lib/contextlib.py" "contextlib2/__init__.py" 19 | 20 | diff_file pyi_to_contextlib2 \ 21 | "$git_root/dev/typeshed_contextlib.pyi" "contextlib2/__init__.pyi" 22 | 23 | diff_file test_to_contextlib2 \ 24 | "$cpython_dir/Lib/test/test_contextlib.py" "test/test_contextlib.py" 25 | 26 | diff_file test_async_to_contextlib2 \ 27 | "$cpython_dir/Lib/test/test_contextlib_async.py" "test/test_contextlib_async.py" 28 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: docs/requirements.txt 36 | -------------------------------------------------------------------------------- /test/support/testcase.py: -------------------------------------------------------------------------------- 1 | """Enough of the test.support.testcase APIs to run the contextlib test suite""" 2 | from . import cl2_have_exception_groups 3 | 4 | if not cl2_have_exception_groups: 5 | # Placeholder to let the isinstance check below run on older versions 6 | class ExceptionGroup(Exception): 7 | pass 8 | 9 | class ExceptionIsLikeMixin: 10 | def assertExceptionIsLike(self, exc, template): 11 | """ 12 | Passes when the provided `exc` matches the structure of `template`. 13 | Individual exceptions don't have to be the same objects or even pass 14 | an equality test: they only need to be the same type and contain equal 15 | `exc_obj.args`. 16 | """ 17 | if exc is None and template is None: 18 | return 19 | 20 | if template is None: 21 | self.fail(f"unexpected exception: {exc}") 22 | 23 | if exc is None: 24 | self.fail(f"expected an exception like {template!r}, got None") 25 | 26 | if not isinstance(exc, ExceptionGroup): 27 | self.assertEqual(exc.__class__, template.__class__) 28 | self.assertEqual(exc.args[0], template.args[0]) 29 | else: 30 | self.assertEqual(exc.message, template.message) 31 | self.assertEqual(len(exc.exceptions), len(template.exceptions)) 32 | for e, t in zip(exc.exceptions, template.exceptions): 33 | self.assertExceptionIsLike(e, t) 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/contextlib2' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.9 22 | 23 | - name: Get pip cache dir 24 | id: pip-cache 25 | run: | 26 | echo "::set-output name=dir::$(pip cache dir)" 27 | 28 | - name: Cache 29 | uses: actions/cache@v2 30 | with: 31 | path: ${{ steps.pip-cache.outputs.dir }} 32 | key: release-${{ hashFiles('**/tox.ini') }} 33 | restore-keys: | 34 | release- 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install -U pip 39 | python -m pip install -U setuptools twine wheel 40 | 41 | - name: Build package 42 | run: | 43 | python setup.py --version 44 | python setup.py sdist --format=gztar bdist_wheel 45 | twine check dist/* 46 | 47 | - name: Upload packages to Jazzband 48 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 49 | uses: pypa/gh-action-pypi-publish@master 50 | with: 51 | user: jazzband 52 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 53 | repository_url: https://jazzband.co/projects/contextlib2/upload 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "**" 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | max-parallel: 5 16 | matrix: 17 | python-version: [3.8, 3.9, '3.10', 3.11, 3.12, 'pypy-3.10'] 18 | 19 | # Check https://github.com/actions/action-versions/tree/main/config/actions 20 | # for latest versions if the standard actions start emitting warnings 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Get pip cache dir 31 | id: pip-cache 32 | run: | 33 | echo "::set-output name=dir::$(pip cache dir)" 34 | 35 | - name: Cache 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ steps.pip-cache.outputs.dir }} 39 | key: 40 | ${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }} 41 | restore-keys: | 42 | ${{ matrix.os }}-${{ matrix.python-version }}-v1- 43 | 44 | - name: Install dependencies 45 | run: | 46 | python -m pip install --upgrade pip 47 | python -m pip install --upgrade tox tox-gh-actions 48 | 49 | - name: Tox tests 50 | run: | 51 | tox -v 52 | 53 | - name: Upload coverage 54 | uses: codecov/codecov-action@v1 55 | with: 56 | name: Python ${{ matrix.python-version }} 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | from setuptools import setup 4 | except ImportError: 5 | from distutils.core import setup 6 | 7 | # Note: The minimum Python version requirement is set on the basis of 8 | # "if it's not tested, it's broken". 9 | # Specifically, if a Python version is no longer available for testing 10 | # in CI, then the minimum supported Python version will be increased. 11 | # That way there's no risk of a release that breaks older Python versions. 12 | 13 | setup( 14 | name='contextlib2', 15 | version=open('VERSION.txt').read().strip(), 16 | python_requires='>=3.7', 17 | packages=['contextlib2'], 18 | include_package_data=True, 19 | license='PSF License', 20 | description='Backports and enhancements for the contextlib module', 21 | long_description=open('README.rst').read(), 22 | author='Alyssa Coghlan', 23 | author_email='ncoghlan@gmail.com', 24 | url='https://github.com/jazzband/contextlib2', 25 | project_urls= { 26 | 'Documentation': 'https://contextlib2.readthedocs.org', 27 | 'Source': 'https://github.com/jazzband/contextlib2.git', 28 | 'Issue Tracker': 'https://github.com/jazzband/contextlib2.git', 29 | } 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'License :: OSI Approved :: Apache Software License', 33 | 'License :: OSI Approved :: Python Software Foundation License', 34 | # These are the Python versions tested, it may work on others 35 | # It definitely won't work on versions without native async support 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.7', 38 | 'Programming Language :: Python :: 3.8', 39 | 'Programming Language :: Python :: 3.9', 40 | 'Programming Language :: Python :: 3.10', 41 | 'Programming Language :: Python :: 3.11', 42 | 'Programming Language :: Python :: 3.12', 43 | ], 44 | 45 | ) 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. contextlib2 documentation master file, created by 2 | sphinx-quickstart on Tue Dec 13 20:29:56 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | contextlib2 --- Updated utilities for context management 7 | ======================================================== 8 | 9 | .. module:: contextlib2 10 | :synopsis: Backports and future enhancements for the contextlib module 11 | 12 | This module provides backports of features in the latest version of the 13 | standard library's :mod:`contextlib` module to earlier Python versions. It 14 | also serves as a real world proving ground for potential future enhancements 15 | to that module. 16 | 17 | Like :mod:`contextlib`, this module provides utilities for common tasks 18 | involving the ``with`` and ``async with`` statements. 19 | 20 | 21 | Additions Relative to the Standard Library 22 | ------------------------------------------ 23 | 24 | This module is primarily a backport of the Python 3.12.3 version of 25 | :mod:`contextlib` to earlier releases. (Note: as of the start of the Python 3.13 26 | beta release cycle, there have been no subsequent changes to ``contextlib``) 27 | 28 | The module makes use of positional-only argument syntax in several call 29 | signatures, so the oldest supported Python version is Python 3.8. 30 | 31 | This module may also be used as a proving ground for new features not yet part 32 | of the standard library. There are currently no such features in the module. 33 | 34 | Finally, this module contains some deprecated APIs which never graduated to 35 | standard library inclusion. These interfaces are no longer documented, but may 36 | still be present in the code (emitting ``DeprecationWarning`` if used). 37 | 38 | 39 | Using the Module 40 | ==================== 41 | 42 | .. toctree:: 43 | contextlib2.rst 44 | 45 | Obtaining the Module 46 | ==================== 47 | 48 | This module can be installed directly from the `Python Package Index`_ with 49 | pip_:: 50 | 51 | pip install contextlib2 52 | 53 | Alternatively, you can download and unpack it manually from the `contextlib2 54 | PyPI page`_. 55 | 56 | There are no operating system or distribution specific versions of this 57 | module - it is a pure Python module that should work on all platforms. 58 | 59 | Supported Python versions are currently 3.8+. 60 | 61 | .. _Python Package Index: http://pypi.python.org 62 | .. _pip: http://www.pip-installer.org 63 | .. _contextlib2 pypi page: http://pypi.python.org/pypi/contextlib2 64 | 65 | 66 | Development and Support 67 | ----------------------- 68 | 69 | contextlib2 is developed and maintained on GitHub_. Problems and suggested 70 | improvements can be posted to the `issue tracker`_. 71 | 72 | .. _GitHub: https://github.com/jazzband/contextlib2 73 | .. _issue tracker: https://github.com/jazzband/contextlib2/issues 74 | 75 | 76 | .. include:: ../NEWS.rst 77 | 78 | 79 | Indices and tables 80 | ================== 81 | 82 | * :ref:`genindex` 83 | * :ref:`search` 84 | -------------------------------------------------------------------------------- /dev/py3_12_test_async_to_contextlib2.patch: -------------------------------------------------------------------------------- 1 | --- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib_async.py 2024-05-23 11:57:09.276022441 +1000 2 | +++ /home/ncoghlan/devel/contextlib2/test/test_contextlib_async.py 2024-05-23 17:39:05.799797895 +1000 3 | @@ -1,5 +1,7 @@ 4 | +"""Unit tests for asynchronous features of contextlib2.py""" 5 | + 6 | import asyncio 7 | -from contextlib import ( 8 | +from contextlib2 import ( 9 | asynccontextmanager, AbstractAsyncContextManager, 10 | AsyncExitStack, nullcontext, aclosing, contextmanager) 11 | import functools 12 | @@ -7,7 +9,7 @@ 13 | import unittest 14 | import traceback 15 | 16 | -from test.test_contextlib import TestBaseExitStack 17 | +from .test_contextlib import TestBaseExitStack 18 | 19 | support.requires_working_socket(module=True) 20 | 21 | @@ -202,7 +204,8 @@ 22 | await ctx.__aexit__(TypeError, TypeError('foo'), None) 23 | if support.check_impl_detail(cpython=True): 24 | # The "gen" attribute is an implementation detail. 25 | - self.assertFalse(ctx.gen.ag_suspended) 26 | + if support.cl2_async_gens_have_ag_suspended: 27 | + self.assertFalse(ctx.gen.ag_suspended) 28 | 29 | @_async_test 30 | async def test_contextmanager_trap_no_yield(self): 31 | @@ -226,7 +229,8 @@ 32 | await ctx.__aexit__(None, None, None) 33 | if support.check_impl_detail(cpython=True): 34 | # The "gen" attribute is an implementation detail. 35 | - self.assertFalse(ctx.gen.ag_suspended) 36 | + if support.cl2_async_gens_have_ag_suspended: 37 | + self.assertFalse(ctx.gen.ag_suspended) 38 | 39 | @_async_test 40 | async def test_contextmanager_non_normalised(self): 41 | @@ -669,12 +673,13 @@ 42 | async def __aenter__(self): 43 | pass 44 | 45 | + expected_error, expected_text = support.cl2_cm_api_exc_info_async() 46 | async with self.exit_stack() as stack: 47 | - with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): 48 | + with self.assertRaisesRegex(expected_error, expected_text): 49 | await stack.enter_async_context(LacksEnterAndExit()) 50 | - with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): 51 | + with self.assertRaisesRegex(expected_error, expected_text): 52 | await stack.enter_async_context(LacksEnter()) 53 | - with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): 54 | + with self.assertRaisesRegex(expected_error, expected_text): 55 | await stack.enter_async_context(LacksExit()) 56 | self.assertFalse(stack._exit_callbacks) 57 | 58 | @@ -752,7 +757,8 @@ 59 | cm.__aenter__ = object() 60 | cm.__aexit__ = object() 61 | stack = self.exit_stack() 62 | - with self.assertRaisesRegex(TypeError, 'asynchronous context manager'): 63 | + expected_error, expected_text = support.cl2_cm_api_exc_info_async() 64 | + with self.assertRaisesRegex(expected_error, expected_text): 65 | await stack.enter_async_context(cm) 66 | stack.push_async_exit(cm) 67 | self.assertIs(stack._exit_callbacks[-1][1], cm) 68 | -------------------------------------------------------------------------------- /test/support/__init__.py: -------------------------------------------------------------------------------- 1 | """Enough of the test.support APIs to run the contextlib test suite""" 2 | import sys 3 | import unittest 4 | 5 | # Extra contextlib2 helpers checking CPython version-dependent details 6 | _py_ver = sys.version_info 7 | 8 | cl2_gens_have_gi_suspended = (_py_ver >= (3, 11)) 9 | cl2_async_gens_have_ag_suspended = (_py_ver >= (3, 12)) 10 | 11 | cl2_have_exception_groups = (_py_ver >= (3, 11)) 12 | cl2_requires_exception_groups = unittest.skipIf(not cl2_have_exception_groups, 13 | "Test requires exception groups") 14 | 15 | cl2_check_traceback_details = (_py_ver >= (3, 10)) 16 | 17 | # CM protocol checking switched to TypeError in Python 3.11 18 | cl2_cm_api_exc_type = TypeError if (_py_ver >= (3, 11)) else AttributeError 19 | if cl2_cm_api_exc_type is AttributeError: 20 | cl2_cm_api_exc_text_sync = { 21 | "": "has no attribute", 22 | "__enter__": "__enter__", 23 | "__exit__": "__exit__", 24 | } 25 | cl2_cm_api_exc_text_async = cl2_cm_api_exc_text_sync 26 | else: 27 | cl2_cm_api_exc_text_sync = { 28 | "": "the context manager", 29 | "__enter__": "the context manager", 30 | "__exit__": "the context manager.*__exit__", 31 | } 32 | cl2_cm_api_exc_text_async = { 33 | "": "asynchronous context manager", 34 | "__enter__": "asynchronous context manager", 35 | "__exit__": "asynchronous context manager.*__exit__", 36 | } 37 | 38 | def cl2_cm_api_exc_info_sync(check_context="", /): 39 | return cl2_cm_api_exc_type, cl2_cm_api_exc_text_sync[check_context] 40 | 41 | def cl2_cm_api_exc_info_async(check_context="", /): 42 | return cl2_cm_api_exc_type, cl2_cm_api_exc_text_async[check_context] 43 | 44 | # Some tests check docstring details 45 | requires_docstrings = unittest.skipIf(sys.flags.optimize >= 2, 46 | "Test requires docstrings") 47 | 48 | # Some tests check CPython implementation details 49 | def _parse_guards(guards): 50 | # Returns a tuple ({platform_name: run_me}, default_value) 51 | if not guards: 52 | return ({'cpython': True}, False) 53 | is_true = list(guards.values())[0] 54 | assert list(guards.values()) == [is_true] * len(guards) # all True or all False 55 | return (guards, not is_true) 56 | 57 | # Use the following check to guard CPython's implementation-specific tests -- 58 | # or to run them only on the implementation(s) guarded by the arguments. 59 | def check_impl_detail(**guards): 60 | """This function returns True or False depending on the host platform. 61 | Examples: 62 | if check_impl_detail(): # only on CPython (default) 63 | if check_impl_detail(jython=True): # only on Jython 64 | if check_impl_detail(cpython=False): # everywhere except on CPython 65 | """ 66 | guards, default = _parse_guards(guards) 67 | return guards.get(sys.implementation.name, default) 68 | 69 | # Early reference release tests force gc collection 70 | def gc_collect(): 71 | """Force as many objects as possible to be collected. 72 | 73 | In non-CPython implementations of Python, this is needed because timely 74 | deallocation is not guaranteed by the garbage collector. (Even in CPython 75 | this can be the case in case of reference cycles.) This means that __del__ 76 | methods may be called later than expected and weakrefs may remain alive for 77 | longer than expected. This function tries its best to force all garbage 78 | objects to disappear. 79 | """ 80 | import gc 81 | gc.collect() 82 | gc.collect() 83 | gc.collect() 84 | 85 | # test_contextlib_async includes some socket-based tests 86 | # Emscripten's socket emulation and WASI sockets have limitations. 87 | is_emscripten = sys.platform == "emscripten" 88 | is_wasi = sys.platform == "wasi" 89 | has_socket_support = not is_emscripten and not is_wasi 90 | 91 | def requires_working_socket(*, module=False): 92 | """Skip tests or modules that require working sockets 93 | 94 | Can be used as a function/class decorator or to skip an entire module. 95 | """ 96 | msg = "requires socket support" 97 | if module: 98 | if not has_socket_support: 99 | raise unittest.SkipTest(msg) 100 | else: 101 | return unittest.skipUnless(has_socket_support, msg) 102 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://jazzband.co/static/img/badge.svg 2 | :target: https://jazzband.co/ 3 | :alt: Jazzband 4 | 5 | .. image:: https://github.com/jazzband/contextlib2/workflows/Test/badge.svg 6 | :target: https://github.com/jazzband/contextlib2/actions 7 | :alt: Tests 8 | 9 | .. image:: https://codecov.io/gh/jazzband/contextlib2/branch/master/graph/badge.svg 10 | :target: https://codecov.io/gh/jazzband/contextlib2 11 | :alt: Coverage 12 | 13 | .. image:: https://readthedocs.org/projects/contextlib2/badge/?version=latest 14 | :target: https://contextlib2.readthedocs.org/ 15 | :alt: Latest Docs 16 | 17 | contextlib2 is a backport of the `standard library's contextlib 18 | module `_ to 19 | earlier Python versions. 20 | 21 | It also sometimes serves as a real world proving ground for possible future 22 | enhancements to the standard library version. 23 | 24 | Licensing 25 | --------- 26 | 27 | As a backport of Python standard library software, the implementation, test 28 | suite and other supporting files for this project are distributed under the 29 | Python Software License used for the CPython reference implementation. 30 | 31 | The one exception is the included type hints file, which comes from the 32 | ``typeshed`` project, and is hence distributed under the Apache License 2.0. 33 | 34 | Development 35 | ----------- 36 | 37 | ``contextlib2`` has no runtime dependencies, but requires ``setuptools`` and 38 | ``wheel`` at build time to generate universal wheel archives. 39 | 40 | Local testing is a matter of running:: 41 | 42 | python3 -m unittest discover -t . -s test 43 | 44 | You can test against multiple versions of Python with 45 | `tox `_:: 46 | 47 | pip install tox 48 | tox 49 | 50 | Versions currently tested in both tox and GitHub Actions are: 51 | 52 | * CPython 3.8 53 | * CPython 3.9 54 | * CPython 3.10 55 | * CPython 3.11 56 | * CPython 3.12 57 | * PyPy3 (specifically 3.10 in GitHub Actions) 58 | 59 | Updating to a new stdlib reference version 60 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 61 | 62 | As of Python 3.12.3, 4 files needed to be copied from the CPython reference 63 | implementation to contextlib2: 64 | 65 | * ``Doc/library/contextlib.rst`` -> ``docs/contextlib2.rst`` 66 | * ``Lib/contextlib.py`` -> ``contextlib2/__init__.py`` 67 | * ``Lib/test/test_contextlib.py`` -> ``test/test_contextlib.py`` 68 | * ``Lib/test/test_contextlib_async.py`` -> ``test/test_contextlib_async.py`` 69 | 70 | The corresponding version of ``contextlib2/__init__.pyi`` also needs to be 71 | retrieved from the ``typeshed`` project:: 72 | 73 | wget https://raw.githubusercontent.com/python/typeshed/master/stdlib/contextlib.pyi 74 | 75 | The following patch files are saved in the ``dev`` directory: 76 | 77 | * changes to ``contextlib2/__init__.py`` to get it to run on the older 78 | versions (and to add back in the deprecated APIs that never graduated to 79 | the standard library version) 80 | * changes to ``test/test_contextlib.py`` and ``test/test_contextlib_async.py`` 81 | to get them to run on the older versions 82 | * changes to ``contextlib2/__init__.pyi`` to make the Python version 83 | guards unconditional (since the ``contextlib2`` API is the same on all 84 | supported versions) 85 | * changes to ``docs/contextlib2.rst`` to use ``contextlib2`` version 86 | numbers in the version added/changed notes and to integrate the module 87 | documentation with the rest of the project documentation 88 | 89 | When the upstream changes between releases are minor, these patch files may be 90 | used directly to reapply the ``contextlib2`` specific changes after syncing a 91 | new version. Even when the patches do not apply cleanly, they're still a useful 92 | guide as to the changes that are needed to restore compatibility with older 93 | Python versions and make any other ``contextlib2`` specific updates. 94 | 95 | The test directory is laid out so that the test suite's imports from 96 | ``test.support`` work the same way as they do in the main CPython test suite. 97 | These files are selective copies rather than complete ones as the ``contextlib`` 98 | tests only need a tiny fraction of the features available in the real 99 | ``test.support`` module. 100 | 101 | The ``dev/sync_from_cpython.sh`` and ``dev/save_diff_snapshot.sh`` scripts 102 | automate some of the steps in the sync process. 103 | -------------------------------------------------------------------------------- /dev/py3_12_test_to_contextlib2.patch: -------------------------------------------------------------------------------- 1 | --- /home/ncoghlan/devel/contextlib2/../cpython/Lib/test/test_contextlib.py 2024-05-23 11:57:09.276022441 +1000 2 | +++ /home/ncoghlan/devel/contextlib2/test/test_contextlib.py 2024-05-23 17:38:37.295232213 +1000 3 | @@ -1,4 +1,4 @@ 4 | -"""Unit tests for contextlib.py, and other context managers.""" 5 | +"""Unit tests for synchronous features of contextlib2.py""" 6 | 7 | import io 8 | import os 9 | @@ -7,7 +7,7 @@ 10 | import threading 11 | import traceback 12 | import unittest 13 | -from contextlib import * # Tests __all__ 14 | +from contextlib2 import * # Tests __all__ 15 | from test import support 16 | from test.support import os_helper 17 | from test.support.testcase import ExceptionIsLikeMixin 18 | @@ -161,7 +161,8 @@ 19 | ctx.__exit__(TypeError, TypeError("foo"), None) 20 | if support.check_impl_detail(cpython=True): 21 | # The "gen" attribute is an implementation detail. 22 | - self.assertFalse(ctx.gen.gi_suspended) 23 | + if support.cl2_gens_have_gi_suspended: 24 | + self.assertFalse(ctx.gen.gi_suspended) 25 | 26 | def test_contextmanager_trap_no_yield(self): 27 | @contextmanager 28 | @@ -183,7 +184,8 @@ 29 | ctx.__exit__(None, None, None) 30 | if support.check_impl_detail(cpython=True): 31 | # The "gen" attribute is an implementation detail. 32 | - self.assertFalse(ctx.gen.gi_suspended) 33 | + if support.cl2_gens_have_gi_suspended: 34 | + self.assertFalse(ctx.gen.gi_suspended) 35 | 36 | def test_contextmanager_non_normalised(self): 37 | @contextmanager 38 | @@ -610,7 +612,8 @@ 39 | def __exit__(self, *exc): 40 | pass 41 | 42 | - with self.assertRaisesRegex(TypeError, 'the context manager'): 43 | + expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__enter__") 44 | + with self.assertRaisesRegex(expected_error, expected_text): 45 | with mycontext(): 46 | pass 47 | 48 | @@ -622,7 +625,8 @@ 49 | def __uxit__(self, *exc): 50 | pass 51 | 52 | - with self.assertRaisesRegex(TypeError, 'the context manager.*__exit__'): 53 | + expected_error, expected_text = support.cl2_cm_api_exc_info_sync("__exit__") 54 | + with self.assertRaisesRegex(expected_error, expected_text): 55 | with mycontext(): 56 | pass 57 | 58 | @@ -790,12 +794,13 @@ 59 | def __enter__(self): 60 | pass 61 | 62 | + expected_error, expected_text = support.cl2_cm_api_exc_info_sync() 63 | with self.exit_stack() as stack: 64 | - with self.assertRaisesRegex(TypeError, 'the context manager'): 65 | + with self.assertRaisesRegex(expected_error, expected_text): 66 | stack.enter_context(LacksEnterAndExit()) 67 | - with self.assertRaisesRegex(TypeError, 'the context manager'): 68 | + with self.assertRaisesRegex(expected_error, expected_text): 69 | stack.enter_context(LacksEnter()) 70 | - with self.assertRaisesRegex(TypeError, 'the context manager'): 71 | + with self.assertRaisesRegex(expected_error, expected_text): 72 | stack.enter_context(LacksExit()) 73 | self.assertFalse(stack._exit_callbacks) 74 | 75 | @@ -858,8 +863,11 @@ 76 | [('_exit_wrapper', 'callback(*args, **kwds)'), 77 | ('raise_exc', 'raise exc')] 78 | 79 | - self.assertEqual( 80 | - [(f.name, f.line) for f in ve_frames], expected) 81 | + # This check fails on PyPy 3.10 82 | + # It also fails on CPython 3.9 and earlier versions 83 | + if support.check_impl_detail(cpython=True) and support.cl2_check_traceback_details: 84 | + self.assertEqual( 85 | + [(f.name, f.line) for f in ve_frames], expected) 86 | 87 | self.assertIsInstance(exc.__context__, ZeroDivisionError) 88 | zde_frames = traceback.extract_tb(exc.__context__.__traceback__) 89 | @@ -1093,7 +1101,8 @@ 90 | cm.__enter__ = object() 91 | cm.__exit__ = object() 92 | stack = self.exit_stack() 93 | - with self.assertRaisesRegex(TypeError, 'the context manager'): 94 | + expected_error, expected_text = support.cl2_cm_api_exc_info_sync() 95 | + with self.assertRaisesRegex(expected_error, expected_text): 96 | stack.enter_context(cm) 97 | stack.push(cm) 98 | self.assertIs(stack._exit_callbacks[-1][1], cm) 99 | @@ -1264,6 +1273,7 @@ 100 | 1/0 101 | self.assertTrue(outer_continued) 102 | 103 | + @support.cl2_requires_exception_groups 104 | def test_exception_groups(self): 105 | eg_ve = lambda: ExceptionGroup( 106 | "EG with ValueErrors only", 107 | -------------------------------------------------------------------------------- /dev/py3_12_py_to_contextlib2.patch: -------------------------------------------------------------------------------- 1 | --- /home/ncoghlan/devel/contextlib2/../cpython/Lib/contextlib.py 2024-05-23 11:57:09.210023505 +1000 2 | +++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.py 2024-05-23 17:05:06.549142813 +1000 3 | @@ -5,7 +5,46 @@ 4 | import _collections_abc 5 | from collections import deque 6 | from functools import wraps 7 | -from types import MethodType, GenericAlias 8 | +from types import MethodType 9 | + 10 | +# Python 3.8 compatibility: GenericAlias may not be defined 11 | +try: 12 | + from types import GenericAlias 13 | +except ImportError: 14 | + # If the real GenericAlias type doesn't exist, __class_getitem__ won't be used, 15 | + # so the fallback placeholder doesn't need to provide any meaningful behaviour 16 | + # (typecheckers may still be unhappy, but for that problem the answer is 17 | + # "use a newer Python version with better typechecking support") 18 | + class GenericAlias: 19 | + pass 20 | + 21 | +# Python 3.10 and earlier compatibility: BaseExceptionGroup may not be defined 22 | +try: 23 | + BaseExceptionGroup 24 | +except NameError: 25 | + # If the real BaseExceptionGroup type doesn't exist, it will never actually 26 | + # be raised. This means the fallback placeholder doesn't need to provide 27 | + # any meaningful behaviour, it just needs to be compatible with 'issubclass' 28 | + class BaseExceptionGroup(BaseException): 29 | + pass 30 | + 31 | +# Python 3.9 and earlier compatibility: anext may not be defined 32 | +try: 33 | + anext 34 | +except NameError: 35 | + def anext(obj, /): 36 | + return obj.__anext__() 37 | + 38 | +# Python 3.11+ behaviour consistency: replace AttributeError with TypeError 39 | +if sys.version_info >= (3, 11): 40 | + # enter_context() and enter_async_context() follow the change in the 41 | + # exception type raised by with statements and async with statements 42 | + _CL2_ERROR_TO_CONVERT = AttributeError 43 | +else: 44 | + # On older versions, raise AttributeError without any changes 45 | + class _CL2_ERROR_TO_CONVERT(Exception): 46 | + pass 47 | + 48 | 49 | __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", 50 | "AbstractContextManager", "AbstractAsyncContextManager", 51 | @@ -62,6 +101,24 @@ 52 | class ContextDecorator(object): 53 | "A base class or mixin that enables context managers to work as decorators." 54 | 55 | + def refresh_cm(self): 56 | + """Returns the context manager used to actually wrap the call to the 57 | + decorated function. 58 | + 59 | + The default implementation just returns *self*. 60 | + 61 | + Overriding this method allows otherwise one-shot context managers 62 | + like _GeneratorContextManager to support use as decorators via 63 | + implicit recreation. 64 | + 65 | + DEPRECATED: refresh_cm was never added to the standard library's 66 | + ContextDecorator API 67 | + """ 68 | + import warnings # Only import if needed for the deprecation warning 69 | + warnings.warn("refresh_cm was never added to the standard library", 70 | + DeprecationWarning) 71 | + return self._recreate_cm() 72 | + 73 | def _recreate_cm(self): 74 | """Return a recreated instance of self. 75 | 76 | @@ -520,7 +577,7 @@ 77 | try: 78 | _enter = cls.__enter__ 79 | _exit = cls.__exit__ 80 | - except AttributeError: 81 | + except _CL2_ERROR_TO_CONVERT: 82 | raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " 83 | f"not support the context manager protocol") from None 84 | result = _enter(cm) 85 | @@ -652,7 +709,7 @@ 86 | try: 87 | _enter = cls.__aenter__ 88 | _exit = cls.__aexit__ 89 | - except AttributeError: 90 | + except _CL2_ERROR_TO_CONVERT: 91 | raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " 92 | f"not support the asynchronous context manager protocol" 93 | ) from None 94 | @@ -798,3 +855,22 @@ 95 | 96 | def __exit__(self, *excinfo): 97 | os.chdir(self._old_cwd.pop()) 98 | + 99 | +# Preserve backwards compatibility 100 | +class ContextStack(ExitStack): 101 | + """(DEPRECATED) Backwards compatibility alias for ExitStack""" 102 | + 103 | + def __init__(self): 104 | + import warnings # Only import if needed for the deprecation warning 105 | + warnings.warn("ContextStack has been renamed to ExitStack", 106 | + DeprecationWarning) 107 | + super(ContextStack, self).__init__() 108 | + 109 | + def register_exit(self, callback): 110 | + return self.push(callback) 111 | + 112 | + def register(self, callback, *args, **kwds): 113 | + return self.callback(callback, *args, **kwds) 114 | + 115 | + def preserve(self): 116 | + return self.pop_all() 117 | -------------------------------------------------------------------------------- /dev/py3_12_pyi_to_contextlib2.patch: -------------------------------------------------------------------------------- 1 | --- /home/ncoghlan/devel/contextlib2/dev/typeshed_contextlib.pyi 2024-05-23 12:40:10.170754997 +1000 2 | +++ /home/ncoghlan/devel/contextlib2/contextlib2/__init__.pyi 2024-05-23 16:47:15.874656809 +1000 3 | @@ -1,3 +1,20 @@ 4 | +# Type hints copied from the typeshed project under the Apache License 2.0 5 | +# https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE 6 | + 7 | +# For updates: https://github.com/python/typeshed/blob/main/stdlib/contextlib.pyi 8 | + 9 | +# Last updated: 2024-05-22 10 | +# Updated from: https://github.com/python/typeshed/blob/aa2d33df211e1e4f70883388febf750ac524d2bb/stdlib/contextlib.pyi 11 | +# Saved to: dev/typeshed_contextlib.pyi 12 | + 13 | +# contextlib2 API adaptation notes: 14 | +# * the various 'if True:' guards replace sys.version checks in the original 15 | +# typeshed file (those APIs are available on all supported versions) 16 | +# * any commented out 'if True:' guards replace sys.version checks in the original 17 | +# typeshed file where the affected APIs haven't been backported yet 18 | +# * deliberately omitted APIs are listed in `dev/mypy.allowlist` 19 | +# (e.g. deprecated experimental APIs that never graduated to the stdlib) 20 | + 21 | import abc 22 | import sys 23 | from _typeshed import FileDescriptorOrPath, Unused 24 | @@ -22,10 +39,10 @@ 25 | "nullcontext", 26 | ] 27 | 28 | -if sys.version_info >= (3, 10): 29 | +if True: 30 | __all__ += ["aclosing"] 31 | 32 | -if sys.version_info >= (3, 11): 33 | +if True: 34 | __all__ += ["chdir"] 35 | 36 | _T = TypeVar("_T") 37 | @@ -65,18 +82,14 @@ 38 | func: Callable[..., Generator[_T_co, Any, Any]] 39 | args: tuple[Any, ...] 40 | kwds: dict[str, Any] 41 | - if sys.version_info >= (3, 9): 42 | + if True: 43 | def __exit__( 44 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 45 | ) -> bool | None: ... 46 | - else: 47 | - def __exit__( 48 | - self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 49 | - ) -> bool | None: ... 50 | 51 | def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... 52 | 53 | -if sys.version_info >= (3, 10): 54 | +if True: 55 | _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) 56 | 57 | class AsyncContextDecorator: 58 | @@ -94,17 +107,6 @@ 59 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 60 | ) -> bool | None: ... 61 | 62 | -else: 63 | - class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]): 64 | - def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... 65 | - gen: AsyncGenerator[_T_co, Any] 66 | - func: Callable[..., AsyncGenerator[_T_co, Any]] 67 | - args: tuple[Any, ...] 68 | - kwds: dict[str, Any] 69 | - async def __aexit__( 70 | - self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 71 | - ) -> bool | None: ... 72 | - 73 | def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... 74 | 75 | class _SupportsClose(Protocol): 76 | @@ -116,7 +118,7 @@ 77 | def __init__(self, thing: _SupportsCloseT) -> None: ... 78 | def __exit__(self, *exc_info: Unused) -> None: ... 79 | 80 | -if sys.version_info >= (3, 10): 81 | +if True: 82 | class _SupportsAclose(Protocol): 83 | def aclose(self) -> Awaitable[object]: ... 84 | 85 | @@ -177,7 +179,7 @@ 86 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 87 | ) -> bool: ... 88 | 89 | -if sys.version_info >= (3, 10): 90 | +if True: 91 | class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): 92 | enter_result: _T 93 | @overload 94 | @@ -189,17 +191,7 @@ 95 | async def __aenter__(self) -> _T: ... 96 | async def __aexit__(self, *exctype: Unused) -> None: ... 97 | 98 | -else: 99 | - class nullcontext(AbstractContextManager[_T, None]): 100 | - enter_result: _T 101 | - @overload 102 | - def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... 103 | - @overload 104 | - def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 105 | - def __enter__(self) -> _T: ... 106 | - def __exit__(self, *exctype: Unused) -> None: ... 107 | - 108 | -if sys.version_info >= (3, 11): 109 | +if True: 110 | _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) 111 | 112 | class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): 113 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/contextlib2.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/contextlib2.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/contextlib2" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/contextlib2" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\contextlib2.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\contextlib2.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Note: The type hints included in this package come from the typeshed project, 2 | and are hence distributed under the Apache License 2.0 rather than under the 3 | Python Software License that covers the module implementation and test suite. 4 | 5 | A. HISTORY OF THE SOFTWARE 6 | ========================== 7 | 8 | contextlib2 is a derivative of the contextlib module distributed by the PSF 9 | as part of the Python standard library. According, it is itself redistributed 10 | under the PSF license (reproduced in full below). As the contextlib module 11 | was added only in Python 2.5, the licenses for earlier Python versions are 12 | not applicable and have not been included. 13 | 14 | Python was created in the early 1990s by Guido van Rossum at Stichting 15 | Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands 16 | as a successor of a language called ABC. Guido remains Python's 17 | principal author, although it includes many contributions from others. 18 | 19 | In 1995, Guido continued his work on Python at the Corporation for 20 | National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) 21 | in Reston, Virginia where he released several versions of the 22 | software. 23 | 24 | In May 2000, Guido and the Python core development team moved to 25 | BeOpen.com to form the BeOpen PythonLabs team. In October of the same 26 | year, the PythonLabs team moved to Digital Creations (now Zope 27 | Corporation, see http://www.zope.com). In 2001, the Python Software 28 | Foundation (PSF, see http://www.python.org/psf/) was formed, a 29 | non-profit organization created specifically to own Python-related 30 | Intellectual Property. Zope Corporation is a sponsoring member of 31 | the PSF. 32 | 33 | All Python releases are Open Source (see http://www.opensource.org for 34 | the Open Source Definition). Historically, most, but not all, Python 35 | releases have also been GPL-compatible; the table below summarizes 36 | the various releases that included the contextlib module. 37 | 38 | Release Derived Year Owner GPL- 39 | from compatible? (1) 40 | 41 | 2.5 2.4 2006 PSF yes 42 | 2.5.1 2.5 2007 PSF yes 43 | 2.5.2 2.5.1 2008 PSF yes 44 | 2.5.3 2.5.2 2008 PSF yes 45 | 2.6 2.5 2008 PSF yes 46 | 2.6.1 2.6 2008 PSF yes 47 | 2.6.2 2.6.1 2009 PSF yes 48 | 2.6.3 2.6.2 2009 PSF yes 49 | 2.6.4 2.6.3 2009 PSF yes 50 | 2.6.5 2.6.4 2010 PSF yes 51 | 3.0 2.6 2008 PSF yes 52 | 3.0.1 3.0 2009 PSF yes 53 | 3.1 3.0.1 2009 PSF yes 54 | 3.1.1 3.1 2009 PSF yes 55 | 3.1.2 3.1.1 2010 PSF yes 56 | 3.1.3 3.1.2 2010 PSF yes 57 | 3.1.4 3.1.3 2011 PSF yes 58 | 3.2 3.1 2011 PSF yes 59 | 3.2.1 3.2 2011 PSF yes 60 | 3.2.2 3.2.1 2011 PSF yes 61 | 3.3 3.2 2012 PSF yes 62 | 63 | Footnotes: 64 | 65 | (1) GPL-compatible doesn't mean that we're distributing Python under 66 | the GPL. All Python licenses, unlike the GPL, let you distribute 67 | a modified version without making your changes open source. The 68 | GPL-compatible licenses make it possible to combine Python with 69 | other software that is released under the GPL; the others don't. 70 | 71 | Thanks to the many outside volunteers who have worked under Guido's 72 | direction to make these releases possible. 73 | 74 | 75 | B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON 76 | =============================================================== 77 | 78 | PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 79 | -------------------------------------------- 80 | 81 | 1. This LICENSE AGREEMENT is between the Python Software Foundation 82 | ("PSF"), and the Individual or Organization ("Licensee") accessing and 83 | otherwise using this software ("Python") in source or binary form and 84 | its associated documentation. 85 | 86 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby 87 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 88 | analyze, test, perform and/or display publicly, prepare derivative works, 89 | distribute, and otherwise use Python alone or in any derivative version, 90 | provided, however, that PSF's License Agreement and PSF's notice of copyright, 91 | i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 92 | 2011 Python Software Foundation; All Rights Reserved" are retained in Python 93 | alone or in any derivative version prepared by Licensee. 94 | 95 | 3. In the event Licensee prepares a derivative work that is based on 96 | or incorporates Python or any part thereof, and wants to make 97 | the derivative work available to others as provided herein, then 98 | Licensee hereby agrees to include in any such work a brief summary of 99 | the changes made to Python. 100 | 101 | 4. PSF is making Python available to Licensee on an "AS IS" 102 | basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR 103 | IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND 104 | DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS 105 | FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT 106 | INFRINGE ANY THIRD PARTY RIGHTS. 107 | 108 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 109 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS 110 | A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, 111 | OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 112 | 113 | 6. This License Agreement will automatically terminate upon a material 114 | breach of its terms and conditions. 115 | 116 | 7. Nothing in this License Agreement shall be deemed to create any 117 | relationship of agency, partnership, or joint venture between PSF and 118 | Licensee. This License Agreement does not grant permission to use PSF 119 | trademarks or trade name in a trademark sense to endorse or promote 120 | products or services of Licensee, or any third party. 121 | 122 | 8. By copying, installing or otherwise using Python, Licensee 123 | agrees to be bound by the terms and conditions of this License 124 | Agreement. 125 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # contextlib2 documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Dec 13 20:29:56 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | # sys.path.insert(0, os.path.abspath('..')) 18 | 19 | # -- General configuration ----------------------------------------------------- 20 | 21 | # If your documentation needs a minimal Sphinx version, state it here. 22 | #needs_sphinx = '1.0' 23 | 24 | # Add any Sphinx extension module names here, as strings. They can be extensions 25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 26 | extensions = [ 27 | 'sphinx.ext.intersphinx', 28 | 'sphinx_rtd_theme', 29 | ] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'contextlib2' 45 | copyright = u'2024, Alyssa Coghlan' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | with open('../VERSION.txt') as f: 53 | version = f.read().strip() 54 | # The full version, including alpha/beta/rc tags. 55 | release = version 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | add_module_names = False 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'sphinx_rtd_theme' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = [] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'contextlib2doc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | # The paper size ('letter' or 'a4'). 175 | #latex_paper_size = 'letter' 176 | 177 | # The font size ('10pt', '11pt' or '12pt'). 178 | #latex_font_size = '10pt' 179 | 180 | # Grouping the document tree into LaTeX files. List of tuples 181 | # (source start file, target name, title, author, documentclass [howto/manual]). 182 | latex_documents = [ 183 | ('index', 'contextlib2.tex', u'contextlib2 Documentation', 184 | u'Alyssa Coghlan', 'manual'), 185 | ] 186 | 187 | # The name of an image file (relative to this directory) to place at the top of 188 | # the title page. 189 | #latex_logo = None 190 | 191 | # For "manual" documents, if this is true, then toplevel headings are parts, 192 | # not chapters. 193 | #latex_use_parts = False 194 | 195 | # If true, show page references after internal links. 196 | #latex_show_pagerefs = False 197 | 198 | # If true, show URL addresses after external links. 199 | #latex_show_urls = False 200 | 201 | # Additional stuff for the LaTeX preamble. 202 | #latex_preamble = '' 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'contextlib2', u'contextlib2 Documentation', 217 | [u'Alyssa Coghlan'], 1) 218 | ] 219 | 220 | 221 | # Configuration for intersphinx: refer to the Python 3 standard library. 222 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 223 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- 1 | Release History 2 | --------------- 3 | 4 | 24.6.0 (2024-06-??) 5 | ^^^^^^^^^^^^^^^^^^^ 6 | 7 | * To allow the use of positional-only argument syntax, the minimum supported 8 | Python version is now Python 3.8. 9 | * Synchronised with the Python 3.12.3 (and 3.13.0) version of contextlib 10 | (`#12 `__), making the 11 | following new features available on Python 3.8+: 12 | 13 | * :class:`chdir` (added in Python 3.11) 14 | * :func:`suppress` filters the contents of ``BaseExceptionGroup`` (Python 3.12) 15 | * improved handling of :class:`StopIteration` subclasses (Python 3.11) 16 | * The exception thrown by :meth:`ExitStack.enter_context` and 17 | :meth:`AsyncExitStack.enter_async_context` when the given object does not 18 | implement the relevant context management protocol is now version-dependent 19 | (:class:`TypeError` on 3.11+, :class:`AttributeError` on earlier versions). 20 | This provides consistency with the ``with`` and ``async with`` behaviour on 21 | the corresponding versions. 22 | * No longer needed object references are now released more promptly 23 | * Update ``mypy stubtest`` to work with recent mypy versions (mypy 1.8.0 tested) 24 | (`#54 `__) 25 | * The ``dev/mypy.allowlist`` file needed for the ``mypy stubtest`` step in the 26 | ``tox`` test configuration is now included in the published sdist 27 | (`#53 `__) 28 | * Type hints have been updated to include ``nullcontext`` (3.10 API added in 29 | 21.6.0) (`#41 `__) 30 | * Test suite updated to pass on Python 3.11 and 3.12 (21.6.0 works on these 31 | versions, the test suite just failed due to no longer valid assumptions) 32 | (`#51 `__) 33 | * Updates to the default compatibility testing matrix: 34 | 35 | * Added: CPython 3.11, CPython 3.12 36 | * Dropped: CPython 3.6, CPython 3.7 37 | 38 | 21.6.0 (2021-06-27) 39 | ^^^^^^^^^^^^^^^^^^^ 40 | 41 | * License update: due to the inclusion of type hints from the ``typeshed`` 42 | project, the ``contextlib2`` project is now under a combination of the 43 | Python Software License (existing license) and the Apache License 2.0 44 | (``typeshed`` license) 45 | * Switched to calendar based versioning using a "year"-"month"-"serial" scheme, 46 | rather than continuing with pre-1.0 semantic versioning 47 | * Due to the inclusion of asynchronous features from Python 3.7+, the 48 | minimum supported Python version is now Python 3.6 49 | (`#29 `__) 50 | * Synchronised with the Python 3.10 version of contextlib 51 | (`#12 `__), making the 52 | following new features available on Python 3.6+: 53 | 54 | * ``asyncontextmanager`` (added in Python 3.7, enhanced in Python 3.10) 55 | * ``aclosing`` (added in Python 3.10) 56 | * ``AbstractAsyncContextManager`` (added in Python 3.7) 57 | * ``AsyncContextDecorator`` (added in Python 3.10) 58 | * ``AsyncExitStack`` (added in Python 3.7) 59 | * async support in ``nullcontext`` (Python 3.10) 60 | 61 | * ``contextlib2`` now includes an adapted copy of the ``contextlib`` 62 | type hints from ``typeshed`` (the adaptation removes the Python version 63 | dependencies from the API definition) 64 | (`#33 `__) 65 | * to incorporate the type hints stub file and the ``py.typed`` marker file, 66 | ``contextlib2`` is now installed as a package rather than as a module 67 | * Updates to the default compatibility testing matrix: 68 | 69 | * Added: CPython 3.9, CPython 3.10 70 | * Dropped: CPython 2.7, CPython 3.5, PyPy2 71 | 72 | 0.6.0.post1 (2019-10-10) 73 | ^^^^^^^^^^^^^^^^^^^^^^^^ 74 | 75 | * Issue `#24 `__: 76 | Correctly update NEWS.rst for the 0.6.0 release. 77 | 78 | 0.6.0 (2019-09-21) 79 | ^^^^^^^^^^^^^^^^^^ 80 | 81 | * Issue `#16 `__: 82 | Backport `AbstractContextManager` from Python 3.6 and `nullcontext` 83 | from Python 3.7 (patch by John Vandenberg) 84 | 85 | 0.5.5 (2017-04-25) 86 | ^^^^^^^^^^^^^^^^^^ 87 | 88 | * Issue `#13 `__: 89 | ``setup.py`` now falls back to plain ``distutils`` if ``setuptools`` is not 90 | available (patch by Allan Harwood) 91 | 92 | * Updates to the default compatibility testing matrix: 93 | 94 | * Added: PyPy3, CPython 3.6 (maintenance), CPython 3.7 (development) 95 | * Dropped: CPython 3.3 96 | 97 | 0.5.4 (2016-07-31) 98 | ^^^^^^^^^^^^^^^^^^ 99 | 100 | * Thanks to the welcome efforts of Jannis Leidel, contextlib2 is now a 101 | [Jazzband](https://jazzband.co/) project! This means that I (Alyssa Coghlan) 102 | am no longer a single point of failure for backports of future contextlib 103 | updates to earlier Python versions. 104 | 105 | * Issue `#7 `__: Backported 106 | fix for CPython issue `#27122 `__, 107 | preventing a potential infinite loop on Python 3.5 when handling 108 | ``RuntimeError`` (CPython updates by Gregory P. Smith & Serhiy Storchaka) 109 | 110 | 111 | 0.5.3 (2016-05-02) 112 | ^^^^^^^^^^^^^^^^^^ 113 | 114 | * ``ExitStack`` now correctly handles context managers implemented as old-style 115 | classes in Python 2.x (such as ``codecs.StreamReader`` and 116 | ``codecs.StreamWriter``) 117 | 118 | * ``setup.py`` has been migrated to setuptools and configured to emit a 119 | universal wheel file by default 120 | 121 | 0.5.2 (2016-05-02) 122 | ^^^^^^^^^^^^^^^^^^ 123 | 124 | * development migrated from BitBucket to GitHub 125 | 126 | * ``redirect_stream``, ``redirect_stdout``, ``redirect_stderr`` and ``suppress`` 127 | now explicitly inherit from ``object``, ensuring compatibility with 128 | ``ExitStack`` when run under Python 2.x (patch contributed by Devin 129 | Jeanpierre). 130 | 131 | * ``MANIFEST.in`` is now included in the published sdist, ensuring the archive 132 | can be precisely recreated even without access to the original source repo 133 | (patch contributed by Guy Rozendorn) 134 | 135 | 136 | 0.5.1 (2016-01-13) 137 | ^^^^^^^^^^^^^^^^^^ 138 | 139 | * Python 2.6 compatilibity restored (patch contributed by Armin Ronacher) 140 | 141 | * README converted back to reStructured Text formatting 142 | 143 | 144 | 0.5.0 (2016-01-12) 145 | ^^^^^^^^^^^^^^^^^^ 146 | 147 | * Updated to include all features from the Python 3.4 and 3.5 releases of 148 | contextlib (also includes some ``ExitStack`` enhancements made following 149 | the integration into the standard library for Python 3.3) 150 | 151 | * The legacy ``ContextStack`` and ``ContextDecorator.refresh_cm`` APIs are 152 | no longer documented and emit ``DeprecationWarning`` when used 153 | 154 | * Python 2.6, 3.2 and 3.3 have been dropped from compatibility testing 155 | 156 | * tox is now supported for local version compatibility testing (patch by 157 | Marc Abramowitz) 158 | 159 | 160 | 0.4.0 (2012-05-05) 161 | ^^^^^^^^^^^^^^^^^^ 162 | 163 | * (BitBucket) Issue #8: Replace ContextStack with ExitStack (old ContextStack 164 | API retained for backwards compatibility) 165 | 166 | * Fall back to unittest2 if unittest is missing required functionality 167 | 168 | 169 | 0.3.1 (2012-01-17) 170 | ^^^^^^^^^^^^^^^^^^ 171 | 172 | * (BitBucket) Issue #7: Add MANIFEST.in so PyPI package contains all relevant 173 | files (patch contributed by Doug Latornell) 174 | 175 | 176 | 0.3 (2012-01-04) 177 | ^^^^^^^^^^^^^^^^ 178 | 179 | * (BitBucket) Issue #5: ContextStack.register no longer pointlessly returns the 180 | wrapped function 181 | * (BitBucket) Issue #2: Add examples and recipes section to docs 182 | * (BitBucket) Issue #3: ContextStack.register_exit() now accepts objects with 183 | __exit__ attributes in addition to accepting exit callbacks directly 184 | * (BitBucket) Issue #1: Add ContextStack.preserve() to move all registered 185 | callbacks to a new ContextStack object 186 | * Wrapped callbacks now expose __wrapped__ (for direct callbacks) or __self__ 187 | (for context manager methods) attributes to aid in introspection 188 | * Moved version number to a VERSION.txt file (read by both docs and setup.py) 189 | * Added NEWS.rst (and incorporated into documentation) 190 | 191 | 192 | 0.2 (2011-12-15) 193 | ^^^^^^^^^^^^^^^^ 194 | 195 | * Renamed CleanupManager to ContextStack (hopefully before anyone started 196 | using the module for anything, since I didn't alias the old name at all) 197 | 198 | 199 | 0.1 (2011-12-13) 200 | ^^^^^^^^^^^^^^^^ 201 | 202 | * Initial release as a backport module 203 | * Added CleanupManager (based on a `Python feature request`_) 204 | * Added ContextDecorator.refresh_cm() (based on a `Python tracker issue`_) 205 | 206 | .. _Python feature request: http://bugs.python.org/issue13585 207 | .. _Python tracker issue: http://bugs.python.org/issue11647 208 | -------------------------------------------------------------------------------- /contextlib2/__init__.pyi: -------------------------------------------------------------------------------- 1 | # Type hints copied from the typeshed project under the Apache License 2.0 2 | # https://github.com/python/typeshed/blob/64c85cdd449ccaff90b546676220c9ecfa6e697f/LICENSE 3 | 4 | # For updates: https://github.com/python/typeshed/blob/main/stdlib/contextlib.pyi 5 | 6 | # Last updated: 2024-05-22 7 | # Updated from: https://github.com/python/typeshed/blob/aa2d33df211e1e4f70883388febf750ac524d2bb/stdlib/contextlib.pyi 8 | # Saved to: dev/typeshed_contextlib.pyi 9 | 10 | # contextlib2 API adaptation notes: 11 | # * the various 'if True:' guards replace sys.version checks in the original 12 | # typeshed file (those APIs are available on all supported versions) 13 | # * any commented out 'if True:' guards replace sys.version checks in the original 14 | # typeshed file where the affected APIs haven't been backported yet 15 | # * deliberately omitted APIs are listed in `dev/mypy.allowlist` 16 | # (e.g. deprecated experimental APIs that never graduated to the stdlib) 17 | 18 | import abc 19 | import sys 20 | from _typeshed import FileDescriptorOrPath, Unused 21 | from abc import abstractmethod 22 | from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator 23 | from types import TracebackType 24 | from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable 25 | from typing_extensions import ParamSpec, Self, TypeAlias 26 | 27 | __all__ = [ 28 | "contextmanager", 29 | "closing", 30 | "AbstractContextManager", 31 | "ContextDecorator", 32 | "ExitStack", 33 | "redirect_stdout", 34 | "redirect_stderr", 35 | "suppress", 36 | "AbstractAsyncContextManager", 37 | "AsyncExitStack", 38 | "asynccontextmanager", 39 | "nullcontext", 40 | ] 41 | 42 | if True: 43 | __all__ += ["aclosing"] 44 | 45 | if True: 46 | __all__ += ["chdir"] 47 | 48 | _T = TypeVar("_T") 49 | _T_co = TypeVar("_T_co", covariant=True) 50 | _T_io = TypeVar("_T_io", bound=IO[str] | None) 51 | _ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None) 52 | _F = TypeVar("_F", bound=Callable[..., Any]) 53 | _P = ParamSpec("_P") 54 | 55 | _ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None] 56 | _CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc) 57 | 58 | @runtime_checkable 59 | class AbstractContextManager(Protocol[_T_co, _ExitT_co]): 60 | def __enter__(self) -> _T_co: ... 61 | @abstractmethod 62 | def __exit__( 63 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 64 | ) -> _ExitT_co: ... 65 | 66 | @runtime_checkable 67 | class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): 68 | async def __aenter__(self) -> _T_co: ... 69 | @abstractmethod 70 | async def __aexit__( 71 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 72 | ) -> _ExitT_co: ... 73 | 74 | class ContextDecorator: 75 | def __call__(self, func: _F) -> _F: ... 76 | 77 | class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator): 78 | # __init__ and all instance attributes are actually inherited from _GeneratorContextManagerBase 79 | # _GeneratorContextManagerBase is more trouble than it's worth to include in the stub; see #6676 80 | def __init__(self, func: Callable[..., Iterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... 81 | gen: Generator[_T_co, Any, Any] 82 | func: Callable[..., Generator[_T_co, Any, Any]] 83 | args: tuple[Any, ...] 84 | kwds: dict[str, Any] 85 | if True: 86 | def __exit__( 87 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 88 | ) -> bool | None: ... 89 | 90 | def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... 91 | 92 | if True: 93 | _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) 94 | 95 | class AsyncContextDecorator: 96 | def __call__(self, func: _AF) -> _AF: ... 97 | 98 | class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator): 99 | # __init__ and these attributes are actually defined in the base class _GeneratorContextManagerBase, 100 | # which is more trouble than it's worth to include in the stub (see #6676) 101 | def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... 102 | gen: AsyncGenerator[_T_co, Any] 103 | func: Callable[..., AsyncGenerator[_T_co, Any]] 104 | args: tuple[Any, ...] 105 | kwds: dict[str, Any] 106 | async def __aexit__( 107 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 108 | ) -> bool | None: ... 109 | 110 | def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... 111 | 112 | class _SupportsClose(Protocol): 113 | def close(self) -> object: ... 114 | 115 | _SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) 116 | 117 | class closing(AbstractContextManager[_SupportsCloseT, None]): 118 | def __init__(self, thing: _SupportsCloseT) -> None: ... 119 | def __exit__(self, *exc_info: Unused) -> None: ... 120 | 121 | if True: 122 | class _SupportsAclose(Protocol): 123 | def aclose(self) -> Awaitable[object]: ... 124 | 125 | _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) 126 | 127 | class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): 128 | def __init__(self, thing: _SupportsAcloseT) -> None: ... 129 | async def __aexit__(self, *exc_info: Unused) -> None: ... 130 | 131 | class suppress(AbstractContextManager[None, bool]): 132 | def __init__(self, *exceptions: type[BaseException]) -> None: ... 133 | def __exit__( 134 | self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None 135 | ) -> bool: ... 136 | 137 | class _RedirectStream(AbstractContextManager[_T_io, None]): 138 | def __init__(self, new_target: _T_io) -> None: ... 139 | def __exit__( 140 | self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None 141 | ) -> None: ... 142 | 143 | class redirect_stdout(_RedirectStream[_T_io]): ... 144 | class redirect_stderr(_RedirectStream[_T_io]): ... 145 | 146 | # In reality this is a subclass of `AbstractContextManager`; 147 | # see #7961 for why we don't do that in the stub 148 | class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): 149 | def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... 150 | def push(self, exit: _CM_EF) -> _CM_EF: ... 151 | def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... 152 | def pop_all(self) -> Self: ... 153 | def close(self) -> None: ... 154 | def __enter__(self) -> Self: ... 155 | def __exit__( 156 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 157 | ) -> _ExitT_co: ... 158 | 159 | _ExitCoroFunc: TypeAlias = Callable[ 160 | [type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None] 161 | ] 162 | _ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc) 163 | 164 | # In reality this is a subclass of `AbstractAsyncContextManager`; 165 | # see #7961 for why we don't do that in the stub 166 | class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): 167 | def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... 168 | async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... 169 | def push(self, exit: _CM_EF) -> _CM_EF: ... 170 | def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... 171 | def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... 172 | def push_async_callback( 173 | self, callback: Callable[_P, Awaitable[_T]], /, *args: _P.args, **kwds: _P.kwargs 174 | ) -> Callable[_P, Awaitable[_T]]: ... 175 | def pop_all(self) -> Self: ... 176 | async def aclose(self) -> None: ... 177 | async def __aenter__(self) -> Self: ... 178 | async def __aexit__( 179 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 180 | ) -> bool: ... 181 | 182 | if True: 183 | class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): 184 | enter_result: _T 185 | @overload 186 | def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... 187 | @overload 188 | def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 189 | def __enter__(self) -> _T: ... 190 | def __exit__(self, *exctype: Unused) -> None: ... 191 | async def __aenter__(self) -> _T: ... 192 | async def __aexit__(self, *exctype: Unused) -> None: ... 193 | 194 | if True: 195 | _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) 196 | 197 | class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): 198 | path: _T_fd_or_any_path 199 | def __init__(self, path: _T_fd_or_any_path) -> None: ... 200 | def __enter__(self) -> None: ... 201 | def __exit__(self, *excinfo: Unused) -> None: ... 202 | -------------------------------------------------------------------------------- /dev/typeshed_contextlib.pyi: -------------------------------------------------------------------------------- 1 | import abc 2 | import sys 3 | from _typeshed import FileDescriptorOrPath, Unused 4 | from abc import abstractmethod 5 | from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Generator, Iterator 6 | from types import TracebackType 7 | from typing import IO, Any, Generic, Protocol, TypeVar, overload, runtime_checkable 8 | from typing_extensions import ParamSpec, Self, TypeAlias 9 | 10 | __all__ = [ 11 | "contextmanager", 12 | "closing", 13 | "AbstractContextManager", 14 | "ContextDecorator", 15 | "ExitStack", 16 | "redirect_stdout", 17 | "redirect_stderr", 18 | "suppress", 19 | "AbstractAsyncContextManager", 20 | "AsyncExitStack", 21 | "asynccontextmanager", 22 | "nullcontext", 23 | ] 24 | 25 | if sys.version_info >= (3, 10): 26 | __all__ += ["aclosing"] 27 | 28 | if sys.version_info >= (3, 11): 29 | __all__ += ["chdir"] 30 | 31 | _T = TypeVar("_T") 32 | _T_co = TypeVar("_T_co", covariant=True) 33 | _T_io = TypeVar("_T_io", bound=IO[str] | None) 34 | _ExitT_co = TypeVar("_ExitT_co", covariant=True, bound=bool | None, default=bool | None) 35 | _F = TypeVar("_F", bound=Callable[..., Any]) 36 | _P = ParamSpec("_P") 37 | 38 | _ExitFunc: TypeAlias = Callable[[type[BaseException] | None, BaseException | None, TracebackType | None], bool | None] 39 | _CM_EF = TypeVar("_CM_EF", bound=AbstractContextManager[Any, Any] | _ExitFunc) 40 | 41 | @runtime_checkable 42 | class AbstractContextManager(Protocol[_T_co, _ExitT_co]): 43 | def __enter__(self) -> _T_co: ... 44 | @abstractmethod 45 | def __exit__( 46 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 47 | ) -> _ExitT_co: ... 48 | 49 | @runtime_checkable 50 | class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]): 51 | async def __aenter__(self) -> _T_co: ... 52 | @abstractmethod 53 | async def __aexit__( 54 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 55 | ) -> _ExitT_co: ... 56 | 57 | class ContextDecorator: 58 | def __call__(self, func: _F) -> _F: ... 59 | 60 | class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator): 61 | # __init__ and all instance attributes are actually inherited from _GeneratorContextManagerBase 62 | # _GeneratorContextManagerBase is more trouble than it's worth to include in the stub; see #6676 63 | def __init__(self, func: Callable[..., Iterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... 64 | gen: Generator[_T_co, Any, Any] 65 | func: Callable[..., Generator[_T_co, Any, Any]] 66 | args: tuple[Any, ...] 67 | kwds: dict[str, Any] 68 | if sys.version_info >= (3, 9): 69 | def __exit__( 70 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 71 | ) -> bool | None: ... 72 | else: 73 | def __exit__( 74 | self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 75 | ) -> bool | None: ... 76 | 77 | def contextmanager(func: Callable[_P, Iterator[_T_co]]) -> Callable[_P, _GeneratorContextManager[_T_co]]: ... 78 | 79 | if sys.version_info >= (3, 10): 80 | _AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]]) 81 | 82 | class AsyncContextDecorator: 83 | def __call__(self, func: _AF) -> _AF: ... 84 | 85 | class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator): 86 | # __init__ and these attributes are actually defined in the base class _GeneratorContextManagerBase, 87 | # which is more trouble than it's worth to include in the stub (see #6676) 88 | def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... 89 | gen: AsyncGenerator[_T_co, Any] 90 | func: Callable[..., AsyncGenerator[_T_co, Any]] 91 | args: tuple[Any, ...] 92 | kwds: dict[str, Any] 93 | async def __aexit__( 94 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 95 | ) -> bool | None: ... 96 | 97 | else: 98 | class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None]): 99 | def __init__(self, func: Callable[..., AsyncIterator[_T_co]], args: tuple[Any, ...], kwds: dict[str, Any]) -> None: ... 100 | gen: AsyncGenerator[_T_co, Any] 101 | func: Callable[..., AsyncGenerator[_T_co, Any]] 102 | args: tuple[Any, ...] 103 | kwds: dict[str, Any] 104 | async def __aexit__( 105 | self, typ: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None 106 | ) -> bool | None: ... 107 | 108 | def asynccontextmanager(func: Callable[_P, AsyncIterator[_T_co]]) -> Callable[_P, _AsyncGeneratorContextManager[_T_co]]: ... 109 | 110 | class _SupportsClose(Protocol): 111 | def close(self) -> object: ... 112 | 113 | _SupportsCloseT = TypeVar("_SupportsCloseT", bound=_SupportsClose) 114 | 115 | class closing(AbstractContextManager[_SupportsCloseT, None]): 116 | def __init__(self, thing: _SupportsCloseT) -> None: ... 117 | def __exit__(self, *exc_info: Unused) -> None: ... 118 | 119 | if sys.version_info >= (3, 10): 120 | class _SupportsAclose(Protocol): 121 | def aclose(self) -> Awaitable[object]: ... 122 | 123 | _SupportsAcloseT = TypeVar("_SupportsAcloseT", bound=_SupportsAclose) 124 | 125 | class aclosing(AbstractAsyncContextManager[_SupportsAcloseT, None]): 126 | def __init__(self, thing: _SupportsAcloseT) -> None: ... 127 | async def __aexit__(self, *exc_info: Unused) -> None: ... 128 | 129 | class suppress(AbstractContextManager[None, bool]): 130 | def __init__(self, *exceptions: type[BaseException]) -> None: ... 131 | def __exit__( 132 | self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None 133 | ) -> bool: ... 134 | 135 | class _RedirectStream(AbstractContextManager[_T_io, None]): 136 | def __init__(self, new_target: _T_io) -> None: ... 137 | def __exit__( 138 | self, exctype: type[BaseException] | None, excinst: BaseException | None, exctb: TracebackType | None 139 | ) -> None: ... 140 | 141 | class redirect_stdout(_RedirectStream[_T_io]): ... 142 | class redirect_stderr(_RedirectStream[_T_io]): ... 143 | 144 | # In reality this is a subclass of `AbstractContextManager`; 145 | # see #7961 for why we don't do that in the stub 146 | class ExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): 147 | def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... 148 | def push(self, exit: _CM_EF) -> _CM_EF: ... 149 | def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... 150 | def pop_all(self) -> Self: ... 151 | def close(self) -> None: ... 152 | def __enter__(self) -> Self: ... 153 | def __exit__( 154 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 155 | ) -> _ExitT_co: ... 156 | 157 | _ExitCoroFunc: TypeAlias = Callable[ 158 | [type[BaseException] | None, BaseException | None, TracebackType | None], Awaitable[bool | None] 159 | ] 160 | _ACM_EF = TypeVar("_ACM_EF", bound=AbstractAsyncContextManager[Any, Any] | _ExitCoroFunc) 161 | 162 | # In reality this is a subclass of `AbstractAsyncContextManager`; 163 | # see #7961 for why we don't do that in the stub 164 | class AsyncExitStack(Generic[_ExitT_co], metaclass=abc.ABCMeta): 165 | def enter_context(self, cm: AbstractContextManager[_T, _ExitT_co]) -> _T: ... 166 | async def enter_async_context(self, cm: AbstractAsyncContextManager[_T, _ExitT_co]) -> _T: ... 167 | def push(self, exit: _CM_EF) -> _CM_EF: ... 168 | def push_async_exit(self, exit: _ACM_EF) -> _ACM_EF: ... 169 | def callback(self, callback: Callable[_P, _T], /, *args: _P.args, **kwds: _P.kwargs) -> Callable[_P, _T]: ... 170 | def push_async_callback( 171 | self, callback: Callable[_P, Awaitable[_T]], /, *args: _P.args, **kwds: _P.kwargs 172 | ) -> Callable[_P, Awaitable[_T]]: ... 173 | def pop_all(self) -> Self: ... 174 | async def aclose(self) -> None: ... 175 | async def __aenter__(self) -> Self: ... 176 | async def __aexit__( 177 | self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, / 178 | ) -> bool: ... 179 | 180 | if sys.version_info >= (3, 10): 181 | class nullcontext(AbstractContextManager[_T, None], AbstractAsyncContextManager[_T, None]): 182 | enter_result: _T 183 | @overload 184 | def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... 185 | @overload 186 | def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 187 | def __enter__(self) -> _T: ... 188 | def __exit__(self, *exctype: Unused) -> None: ... 189 | async def __aenter__(self) -> _T: ... 190 | async def __aexit__(self, *exctype: Unused) -> None: ... 191 | 192 | else: 193 | class nullcontext(AbstractContextManager[_T, None]): 194 | enter_result: _T 195 | @overload 196 | def __init__(self: nullcontext[None], enter_result: None = None) -> None: ... 197 | @overload 198 | def __init__(self: nullcontext[_T], enter_result: _T) -> None: ... # pyright: ignore[reportInvalidTypeVarUse] #11780 199 | def __enter__(self) -> _T: ... 200 | def __exit__(self, *exctype: Unused) -> None: ... 201 | 202 | if sys.version_info >= (3, 11): 203 | _T_fd_or_any_path = TypeVar("_T_fd_or_any_path", bound=FileDescriptorOrPath) 204 | 205 | class chdir(AbstractContextManager[None, None], Generic[_T_fd_or_any_path]): 206 | path: _T_fd_or_any_path 207 | def __init__(self, path: _T_fd_or_any_path) -> None: ... 208 | def __enter__(self) -> None: ... 209 | def __exit__(self, *excinfo: Unused) -> None: ... 210 | -------------------------------------------------------------------------------- /dev/py3_12_rst_to_contextlib2.patch: -------------------------------------------------------------------------------- 1 | --- /home/ncoghlan/devel/contextlib2/../cpython/Doc/library/contextlib.rst 2024-05-20 12:53:59.936907756 +1000 2 | +++ /home/ncoghlan/devel/contextlib2/docs/contextlib2.rst 2024-05-23 17:39:52.671083724 +1000 3 | @@ -1,20 +1,5 @@ 4 | -:mod:`!contextlib` --- Utilities for :keyword:`!with`\ -statement contexts 5 | -========================================================================== 6 | - 7 | -.. module:: contextlib 8 | - :synopsis: Utilities for with-statement contexts. 9 | - 10 | -**Source code:** :source:`Lib/contextlib.py` 11 | - 12 | --------------- 13 | - 14 | -This module provides utilities for common tasks involving the :keyword:`with` 15 | -statement. For more information see also :ref:`typecontextmanager` and 16 | -:ref:`context-managers`. 17 | - 18 | - 19 | -Utilities 20 | ---------- 21 | +API Reference 22 | +------------- 23 | 24 | Functions and classes provided: 25 | 26 | @@ -26,8 +11,8 @@ 27 | ``self`` while :meth:`object.__exit__` is an abstract method which by default 28 | returns ``None``. See also the definition of :ref:`typecontextmanager`. 29 | 30 | - .. versionadded:: 3.6 31 | - 32 | + .. versionadded:: 0.6.0 33 | + Part of the standard library in Python 3.6 and later 34 | 35 | .. class:: AbstractAsyncContextManager 36 | 37 | @@ -38,8 +23,8 @@ 38 | returns ``None``. See also the definition of 39 | :ref:`async-context-managers`. 40 | 41 | - .. versionadded:: 3.7 42 | - 43 | + .. versionadded:: 21.6.0 44 | + Part of the standard library in Python 3.7 and later 45 | 46 | .. decorator:: contextmanager 47 | 48 | @@ -49,12 +34,12 @@ 49 | 50 | While many objects natively support use in with statements, sometimes a 51 | resource needs to be managed that isn't a context manager in its own right, 52 | - and doesn't implement a ``close()`` method for use with ``contextlib.closing`` 53 | + and doesn't implement a ``close()`` method for use with ``contextlib2.closing`` 54 | 55 | An abstract example would be the following to ensure correct resource 56 | management:: 57 | 58 | - from contextlib import contextmanager 59 | + from contextlib2 import contextmanager 60 | 61 | @contextmanager 62 | def managed_resource(*args, **kwds): 63 | @@ -95,13 +80,10 @@ 64 | created by :func:`contextmanager` to meet the requirement that context 65 | managers support multiple invocations in order to be used as decorators). 66 | 67 | - .. versionchanged:: 3.2 68 | - Use of :class:`ContextDecorator`. 69 | - 70 | 71 | .. decorator:: asynccontextmanager 72 | 73 | - Similar to :func:`~contextlib.contextmanager`, but creates an 74 | + Similar to :func:`~contextlib2.contextmanager`, but creates an 75 | :ref:`asynchronous context manager `. 76 | 77 | This function is a :term:`decorator` that can be used to define a factory 78 | @@ -112,7 +94,7 @@ 79 | 80 | A simple example:: 81 | 82 | - from contextlib import asynccontextmanager 83 | + from contextlib2 import asynccontextmanager 84 | 85 | @asynccontextmanager 86 | async def get_connection(): 87 | @@ -126,13 +108,16 @@ 88 | async with get_connection() as conn: 89 | return conn.query('SELECT ...') 90 | 91 | - .. versionadded:: 3.7 92 | + .. versionadded:: 21.6.0 93 | + Part of the standard library in Python 3.7 and later, enhanced in 94 | + Python 3.10 and later to allow created async context managers to be used 95 | + as async function decorators. 96 | 97 | Context managers defined with :func:`asynccontextmanager` can be used 98 | either as decorators or with :keyword:`async with` statements:: 99 | 100 | import time 101 | - from contextlib import asynccontextmanager 102 | + from contextlib2 import asynccontextmanager 103 | 104 | @asynccontextmanager 105 | async def timeit(): 106 | @@ -151,17 +136,13 @@ 107 | created by :func:`asynccontextmanager` to meet the requirement that context 108 | managers support multiple invocations in order to be used as decorators. 109 | 110 | - .. versionchanged:: 3.10 111 | - Async context managers created with :func:`asynccontextmanager` can 112 | - be used as decorators. 113 | - 114 | 115 | .. function:: closing(thing) 116 | 117 | Return a context manager that closes *thing* upon completion of the block. This 118 | is basically equivalent to:: 119 | 120 | - from contextlib import contextmanager 121 | + from contextlib2 import contextmanager 122 | 123 | @contextmanager 124 | def closing(thing): 125 | @@ -172,7 +153,7 @@ 126 | 127 | And lets you write code like this:: 128 | 129 | - from contextlib import closing 130 | + from contextlib2 import closing 131 | from urllib.request import urlopen 132 | 133 | with closing(urlopen('https://www.python.org')) as page: 134 | @@ -196,7 +177,7 @@ 135 | Return an async context manager that calls the ``aclose()`` method of *thing* 136 | upon completion of the block. This is basically equivalent to:: 137 | 138 | - from contextlib import asynccontextmanager 139 | + from contextlib2 import asynccontextmanager 140 | 141 | @asynccontextmanager 142 | async def aclosing(thing): 143 | @@ -209,7 +190,7 @@ 144 | generators when they happen to exit early by :keyword:`break` or an 145 | exception. For example:: 146 | 147 | - from contextlib import aclosing 148 | + from contextlib2 import aclosing 149 | 150 | async with aclosing(my_generator()) as values: 151 | async for value in values: 152 | @@ -221,7 +202,8 @@ 153 | variables work as expected, and the exit code isn't run after the 154 | lifetime of some task it depends on). 155 | 156 | - .. versionadded:: 3.10 157 | + .. versionadded:: 21.6.0 158 | + Part of the standard library in Python 3.10 and later 159 | 160 | 161 | .. _simplifying-support-for-single-optional-context-managers: 162 | @@ -235,10 +217,10 @@ 163 | def myfunction(arg, ignore_exceptions=False): 164 | if ignore_exceptions: 165 | # Use suppress to ignore all exceptions. 166 | - cm = contextlib.suppress(Exception) 167 | + cm = contextlib2.suppress(Exception) 168 | else: 169 | # Do not ignore any exceptions, cm has no effect. 170 | - cm = contextlib.nullcontext() 171 | + cm = contextlib2.nullcontext() 172 | with cm: 173 | # Do something 174 | 175 | @@ -269,11 +251,11 @@ 176 | async with cm as session: 177 | # Send http requests with session 178 | 179 | - .. versionadded:: 3.7 180 | - 181 | - .. versionchanged:: 3.10 182 | - :term:`asynchronous context manager` support was added. 183 | + .. versionadded:: 0.6.0 184 | + Part of the standard library in Python 3.7 and later 185 | 186 | + .. versionchanged:: 21.6.0 187 | + Updated to Python 3.10 version with :term:`asynchronous context manager` support 188 | 189 | 190 | .. function:: suppress(*exceptions) 191 | @@ -290,7 +272,7 @@ 192 | 193 | For example:: 194 | 195 | - from contextlib import suppress 196 | + from contextlib2 import suppress 197 | 198 | with suppress(FileNotFoundError): 199 | os.remove('somefile.tmp') 200 | @@ -314,13 +296,15 @@ 201 | 202 | If the code within the :keyword:`!with` block raises a 203 | :exc:`BaseExceptionGroup`, suppressed exceptions are removed from the 204 | - group. If any exceptions in the group are not suppressed, a group containing them is re-raised. 205 | + group. If any exceptions in the group are not suppressed, a group containing 206 | + them is re-raised. 207 | 208 | - .. versionadded:: 3.4 209 | + .. versionadded:: 0.5 210 | + Part of the standard library in Python 3.4 and later 211 | 212 | - .. versionchanged:: 3.12 213 | - ``suppress`` now supports suppressing exceptions raised as 214 | - part of an :exc:`BaseExceptionGroup`. 215 | + .. versionchanged:: 24.6.0 216 | + Updated to Python 3.12 version that supports suppressing exceptions raised 217 | + as part of a :exc:`BaseExceptionGroup`. 218 | 219 | .. function:: redirect_stdout(new_target) 220 | 221 | @@ -359,17 +343,19 @@ 222 | 223 | This context manager is :ref:`reentrant `. 224 | 225 | - .. versionadded:: 3.4 226 | + .. versionadded:: 0.5 227 | + Part of the standard library in Python 3.4 and later 228 | 229 | 230 | .. function:: redirect_stderr(new_target) 231 | 232 | - Similar to :func:`~contextlib.redirect_stdout` but redirecting 233 | + Similar to :func:`~contextlib2.redirect_stdout` but redirecting 234 | :data:`sys.stderr` to another file or file-like object. 235 | 236 | This context manager is :ref:`reentrant `. 237 | 238 | - .. versionadded:: 3.5 239 | + .. versionadded:: 0.5 240 | + Part of the standard library in Python 3.5 and later 241 | 242 | 243 | .. function:: chdir(path) 244 | @@ -386,7 +372,8 @@ 245 | 246 | This context manager is :ref:`reentrant `. 247 | 248 | - .. versionadded:: 3.11 249 | + .. versionadded:: 24.6.0 250 | + Part of the standard library in Python 3.11 and later 251 | 252 | 253 | .. class:: ContextDecorator() 254 | @@ -402,7 +389,7 @@ 255 | 256 | Example of ``ContextDecorator``:: 257 | 258 | - from contextlib import ContextDecorator 259 | + from contextlib2 import ContextDecorator 260 | 261 | class mycontext(ContextDecorator): 262 | def __enter__(self): 263 | @@ -449,7 +436,7 @@ 264 | Existing context managers that already have a base class can be extended by 265 | using ``ContextDecorator`` as a mixin class:: 266 | 267 | - from contextlib import ContextDecorator 268 | + from contextlib2 import ContextDecorator 269 | 270 | class mycontext(ContextBaseClass, ContextDecorator): 271 | def __enter__(self): 272 | @@ -464,8 +451,6 @@ 273 | statements. If this is not the case, then the original construct with the 274 | explicit :keyword:`!with` statement inside the function should be used. 275 | 276 | - .. versionadded:: 3.2 277 | - 278 | 279 | .. class:: AsyncContextDecorator 280 | 281 | @@ -474,7 +459,7 @@ 282 | Example of ``AsyncContextDecorator``:: 283 | 284 | from asyncio import run 285 | - from contextlib import AsyncContextDecorator 286 | + from contextlib2 import AsyncContextDecorator 287 | 288 | class mycontext(AsyncContextDecorator): 289 | async def __aenter__(self): 290 | @@ -505,7 +490,8 @@ 291 | The bit in the middle 292 | Finishing 293 | 294 | - .. versionadded:: 3.10 295 | + .. versionadded:: 21.6.0 296 | + Part of the standard library in Python 3.10 and later 297 | 298 | 299 | .. class:: ExitStack() 300 | @@ -547,7 +533,8 @@ 301 | foundation for higher level context managers that manipulate the exit 302 | stack in application specific ways. 303 | 304 | - .. versionadded:: 3.3 305 | + .. versionadded:: 0.4 306 | + Part of the standard library in Python 3.3 and later 307 | 308 | .. method:: enter_context(cm) 309 | 310 | @@ -558,9 +545,10 @@ 311 | These context managers may suppress exceptions just as they normally 312 | would if used directly as part of a :keyword:`with` statement. 313 | 314 | - .. versionchanged:: 3.11 315 | - Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* 316 | - is not a context manager. 317 | + .. versionchanged:: 24.6.0 318 | + When running on Python 3.11 or later, raises :exc:`TypeError` instead 319 | + of :exc:`AttributeError` if *cm* is not a context manager. This aligns 320 | + with the behaviour of :keyword:`with` statements in Python 3.11+. 321 | 322 | .. method:: push(exit) 323 | 324 | @@ -627,14 +615,16 @@ 325 | The :meth:`~ExitStack.close` method is not implemented; :meth:`aclose` must be used 326 | instead. 327 | 328 | - .. coroutinemethod:: enter_async_context(cm) 329 | + .. method:: enter_async_context(cm) 330 | + :async: 331 | 332 | Similar to :meth:`ExitStack.enter_context` but expects an asynchronous context 333 | manager. 334 | 335 | - .. versionchanged:: 3.11 336 | - Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* 337 | - is not an asynchronous context manager. 338 | + .. versionchanged:: 24.6.0 339 | + When running on Python 3.11 or later, raises :exc:`TypeError` instead 340 | + of :exc:`AttributeError` if *cm* is not an asynchronous context manager. 341 | + This aligns with the behaviour of ``async with`` statements in Python 3.11+. 342 | 343 | .. method:: push_async_exit(exit) 344 | 345 | @@ -645,7 +635,8 @@ 346 | 347 | Similar to :meth:`ExitStack.callback` but expects a coroutine function. 348 | 349 | - .. coroutinemethod:: aclose() 350 | + .. method:: aclose() 351 | + :async: 352 | 353 | Similar to :meth:`ExitStack.close` but properly handles awaitables. 354 | 355 | @@ -658,13 +649,15 @@ 356 | # the async with statement, even if attempts to open a connection 357 | # later in the list raise an exception. 358 | 359 | - .. versionadded:: 3.7 360 | + .. versionadded:: 21.6.0 361 | + Part of the standard library in Python 3.7 and later 362 | + 363 | 364 | Examples and Recipes 365 | -------------------- 366 | 367 | This section describes some examples and recipes for making effective use of 368 | -the tools provided by :mod:`contextlib`. 369 | +the tools provided by :mod:`contextlib2`. 370 | 371 | 372 | Supporting a variable number of context managers 373 | @@ -728,7 +721,7 @@ 374 | acquisition and release functions, along with an optional validation function, 375 | and maps them to the context management protocol:: 376 | 377 | - from contextlib import contextmanager, AbstractContextManager, ExitStack 378 | + from contextlib2 import contextmanager, AbstractContextManager, ExitStack 379 | 380 | class ResourceManager(AbstractContextManager): 381 | 382 | @@ -788,7 +781,7 @@ 383 | execution at the end of a ``with`` statement, and then later decide to skip 384 | executing that callback:: 385 | 386 | - from contextlib import ExitStack 387 | + from contextlib2 import ExitStack 388 | 389 | with ExitStack() as stack: 390 | stack.callback(cleanup_resources) 391 | @@ -802,7 +795,7 @@ 392 | If a particular application uses this pattern a lot, it can be simplified 393 | even further by means of a small helper class:: 394 | 395 | - from contextlib import ExitStack 396 | + from contextlib2 import ExitStack 397 | 398 | class Callback(ExitStack): 399 | def __init__(self, callback, /, *args, **kwds): 400 | @@ -822,7 +815,7 @@ 401 | :meth:`ExitStack.callback` to declare the resource cleanup in 402 | advance:: 403 | 404 | - from contextlib import ExitStack 405 | + from contextlib2 import ExitStack 406 | 407 | with ExitStack() as stack: 408 | @stack.callback 409 | @@ -849,7 +842,7 @@ 410 | inheriting from :class:`ContextDecorator` provides both capabilities in a 411 | single definition:: 412 | 413 | - from contextlib import ContextDecorator 414 | + from contextlib2 import ContextDecorator 415 | import logging 416 | 417 | logging.basicConfig(level=logging.INFO) 418 | @@ -911,7 +904,7 @@ 419 | context managers, and will complain about the underlying generator failing 420 | to yield if an attempt is made to use them a second time:: 421 | 422 | - >>> from contextlib import contextmanager 423 | + >>> from contextlib2 import contextmanager 424 | >>> @contextmanager 425 | ... def singleuse(): 426 | ... print("Before") 427 | @@ -946,7 +939,7 @@ 428 | :func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very 429 | simple example of reentrant use:: 430 | 431 | - >>> from contextlib import redirect_stdout 432 | + >>> from contextlib2 import redirect_stdout 433 | >>> from io import StringIO 434 | >>> stream = StringIO() 435 | >>> write_to_stream = redirect_stdout(stream) 436 | @@ -992,7 +985,7 @@ 437 | when leaving any with statement, regardless of where those callbacks 438 | were added:: 439 | 440 | - >>> from contextlib import ExitStack 441 | + >>> from contextlib2 import ExitStack 442 | >>> stack = ExitStack() 443 | >>> with stack: 444 | ... stack.callback(print, "Callback: from first context") 445 | @@ -1026,7 +1019,7 @@ 446 | Using separate :class:`ExitStack` instances instead of reusing a single 447 | instance avoids that problem:: 448 | 449 | - >>> from contextlib import ExitStack 450 | + >>> from contextlib2 import ExitStack 451 | >>> with ExitStack() as outer_stack: 452 | ... outer_stack.callback(print, "Callback: from outer context") 453 | ... with ExitStack() as inner_stack: 454 | -------------------------------------------------------------------------------- /test/test_contextlib_async.py: -------------------------------------------------------------------------------- 1 | """Unit tests for asynchronous features of contextlib2.py""" 2 | 3 | import asyncio 4 | from contextlib2 import ( 5 | asynccontextmanager, AbstractAsyncContextManager, 6 | AsyncExitStack, nullcontext, aclosing, contextmanager) 7 | import functools 8 | from test import support 9 | import unittest 10 | import traceback 11 | 12 | from .test_contextlib import TestBaseExitStack 13 | 14 | support.requires_working_socket(module=True) 15 | 16 | def _async_test(func): 17 | """Decorator to turn an async function into a test case.""" 18 | @functools.wraps(func) 19 | def wrapper(*args, **kwargs): 20 | coro = func(*args, **kwargs) 21 | asyncio.run(coro) 22 | return wrapper 23 | 24 | def tearDownModule(): 25 | asyncio.set_event_loop_policy(None) 26 | 27 | 28 | class TestAbstractAsyncContextManager(unittest.TestCase): 29 | 30 | @_async_test 31 | async def test_enter(self): 32 | class DefaultEnter(AbstractAsyncContextManager): 33 | async def __aexit__(self, *args): 34 | await super().__aexit__(*args) 35 | 36 | manager = DefaultEnter() 37 | self.assertIs(await manager.__aenter__(), manager) 38 | 39 | async with manager as context: 40 | self.assertIs(manager, context) 41 | 42 | @_async_test 43 | async def test_async_gen_propagates_generator_exit(self): 44 | # A regression test for https://bugs.python.org/issue33786. 45 | 46 | @asynccontextmanager 47 | async def ctx(): 48 | yield 49 | 50 | async def gen(): 51 | async with ctx(): 52 | yield 11 53 | 54 | g = gen() 55 | async for val in g: 56 | self.assertEqual(val, 11) 57 | break 58 | await g.aclose() 59 | 60 | def test_exit_is_abstract(self): 61 | class MissingAexit(AbstractAsyncContextManager): 62 | pass 63 | 64 | with self.assertRaises(TypeError): 65 | MissingAexit() 66 | 67 | def test_structural_subclassing(self): 68 | class ManagerFromScratch: 69 | async def __aenter__(self): 70 | return self 71 | async def __aexit__(self, exc_type, exc_value, traceback): 72 | return None 73 | 74 | self.assertTrue(issubclass(ManagerFromScratch, AbstractAsyncContextManager)) 75 | 76 | class DefaultEnter(AbstractAsyncContextManager): 77 | async def __aexit__(self, *args): 78 | await super().__aexit__(*args) 79 | 80 | self.assertTrue(issubclass(DefaultEnter, AbstractAsyncContextManager)) 81 | 82 | class NoneAenter(ManagerFromScratch): 83 | __aenter__ = None 84 | 85 | self.assertFalse(issubclass(NoneAenter, AbstractAsyncContextManager)) 86 | 87 | class NoneAexit(ManagerFromScratch): 88 | __aexit__ = None 89 | 90 | self.assertFalse(issubclass(NoneAexit, AbstractAsyncContextManager)) 91 | 92 | 93 | class AsyncContextManagerTestCase(unittest.TestCase): 94 | 95 | @_async_test 96 | async def test_contextmanager_plain(self): 97 | state = [] 98 | @asynccontextmanager 99 | async def woohoo(): 100 | state.append(1) 101 | yield 42 102 | state.append(999) 103 | async with woohoo() as x: 104 | self.assertEqual(state, [1]) 105 | self.assertEqual(x, 42) 106 | state.append(x) 107 | self.assertEqual(state, [1, 42, 999]) 108 | 109 | @_async_test 110 | async def test_contextmanager_finally(self): 111 | state = [] 112 | @asynccontextmanager 113 | async def woohoo(): 114 | state.append(1) 115 | try: 116 | yield 42 117 | finally: 118 | state.append(999) 119 | with self.assertRaises(ZeroDivisionError): 120 | async with woohoo() as x: 121 | self.assertEqual(state, [1]) 122 | self.assertEqual(x, 42) 123 | state.append(x) 124 | raise ZeroDivisionError() 125 | self.assertEqual(state, [1, 42, 999]) 126 | 127 | @_async_test 128 | async def test_contextmanager_traceback(self): 129 | @asynccontextmanager 130 | async def f(): 131 | yield 132 | 133 | try: 134 | async with f(): 135 | 1/0 136 | except ZeroDivisionError as e: 137 | frames = traceback.extract_tb(e.__traceback__) 138 | 139 | self.assertEqual(len(frames), 1) 140 | self.assertEqual(frames[0].name, 'test_contextmanager_traceback') 141 | self.assertEqual(frames[0].line, '1/0') 142 | 143 | # Repeat with RuntimeError (which goes through a different code path) 144 | class RuntimeErrorSubclass(RuntimeError): 145 | pass 146 | 147 | try: 148 | async with f(): 149 | raise RuntimeErrorSubclass(42) 150 | except RuntimeErrorSubclass as e: 151 | frames = traceback.extract_tb(e.__traceback__) 152 | 153 | self.assertEqual(len(frames), 1) 154 | self.assertEqual(frames[0].name, 'test_contextmanager_traceback') 155 | self.assertEqual(frames[0].line, 'raise RuntimeErrorSubclass(42)') 156 | 157 | class StopIterationSubclass(StopIteration): 158 | pass 159 | 160 | class StopAsyncIterationSubclass(StopAsyncIteration): 161 | pass 162 | 163 | for stop_exc in ( 164 | StopIteration('spam'), 165 | StopAsyncIteration('ham'), 166 | StopIterationSubclass('spam'), 167 | StopAsyncIterationSubclass('spam') 168 | ): 169 | with self.subTest(type=type(stop_exc)): 170 | try: 171 | async with f(): 172 | raise stop_exc 173 | except type(stop_exc) as e: 174 | self.assertIs(e, stop_exc) 175 | frames = traceback.extract_tb(e.__traceback__) 176 | else: 177 | self.fail(f'{stop_exc} was suppressed') 178 | 179 | self.assertEqual(len(frames), 1) 180 | self.assertEqual(frames[0].name, 'test_contextmanager_traceback') 181 | self.assertEqual(frames[0].line, 'raise stop_exc') 182 | 183 | @_async_test 184 | async def test_contextmanager_no_reraise(self): 185 | @asynccontextmanager 186 | async def whee(): 187 | yield 188 | ctx = whee() 189 | await ctx.__aenter__() 190 | # Calling __aexit__ should not result in an exception 191 | self.assertFalse(await ctx.__aexit__(TypeError, TypeError("foo"), None)) 192 | 193 | @_async_test 194 | async def test_contextmanager_trap_yield_after_throw(self): 195 | @asynccontextmanager 196 | async def whoo(): 197 | try: 198 | yield 199 | except: 200 | yield 201 | ctx = whoo() 202 | await ctx.__aenter__() 203 | with self.assertRaises(RuntimeError): 204 | await ctx.__aexit__(TypeError, TypeError('foo'), None) 205 | if support.check_impl_detail(cpython=True): 206 | # The "gen" attribute is an implementation detail. 207 | if support.cl2_async_gens_have_ag_suspended: 208 | self.assertFalse(ctx.gen.ag_suspended) 209 | 210 | @_async_test 211 | async def test_contextmanager_trap_no_yield(self): 212 | @asynccontextmanager 213 | async def whoo(): 214 | if False: 215 | yield 216 | ctx = whoo() 217 | with self.assertRaises(RuntimeError): 218 | await ctx.__aenter__() 219 | 220 | @_async_test 221 | async def test_contextmanager_trap_second_yield(self): 222 | @asynccontextmanager 223 | async def whoo(): 224 | yield 225 | yield 226 | ctx = whoo() 227 | await ctx.__aenter__() 228 | with self.assertRaises(RuntimeError): 229 | await ctx.__aexit__(None, None, None) 230 | if support.check_impl_detail(cpython=True): 231 | # The "gen" attribute is an implementation detail. 232 | if support.cl2_async_gens_have_ag_suspended: 233 | self.assertFalse(ctx.gen.ag_suspended) 234 | 235 | @_async_test 236 | async def test_contextmanager_non_normalised(self): 237 | @asynccontextmanager 238 | async def whoo(): 239 | try: 240 | yield 241 | except RuntimeError: 242 | raise SyntaxError 243 | 244 | ctx = whoo() 245 | await ctx.__aenter__() 246 | with self.assertRaises(SyntaxError): 247 | await ctx.__aexit__(RuntimeError, None, None) 248 | 249 | @_async_test 250 | async def test_contextmanager_except(self): 251 | state = [] 252 | @asynccontextmanager 253 | async def woohoo(): 254 | state.append(1) 255 | try: 256 | yield 42 257 | except ZeroDivisionError as e: 258 | state.append(e.args[0]) 259 | self.assertEqual(state, [1, 42, 999]) 260 | async with woohoo() as x: 261 | self.assertEqual(state, [1]) 262 | self.assertEqual(x, 42) 263 | state.append(x) 264 | raise ZeroDivisionError(999) 265 | self.assertEqual(state, [1, 42, 999]) 266 | 267 | @_async_test 268 | async def test_contextmanager_except_stopiter(self): 269 | @asynccontextmanager 270 | async def woohoo(): 271 | yield 272 | 273 | class StopIterationSubclass(StopIteration): 274 | pass 275 | 276 | class StopAsyncIterationSubclass(StopAsyncIteration): 277 | pass 278 | 279 | for stop_exc in ( 280 | StopIteration('spam'), 281 | StopAsyncIteration('ham'), 282 | StopIterationSubclass('spam'), 283 | StopAsyncIterationSubclass('spam') 284 | ): 285 | with self.subTest(type=type(stop_exc)): 286 | try: 287 | async with woohoo(): 288 | raise stop_exc 289 | except Exception as ex: 290 | self.assertIs(ex, stop_exc) 291 | else: 292 | self.fail(f'{stop_exc} was suppressed') 293 | 294 | @_async_test 295 | async def test_contextmanager_wrap_runtimeerror(self): 296 | @asynccontextmanager 297 | async def woohoo(): 298 | try: 299 | yield 300 | except Exception as exc: 301 | raise RuntimeError(f'caught {exc}') from exc 302 | 303 | with self.assertRaises(RuntimeError): 304 | async with woohoo(): 305 | 1 / 0 306 | 307 | # If the context manager wrapped StopAsyncIteration in a RuntimeError, 308 | # we also unwrap it, because we can't tell whether the wrapping was 309 | # done by the generator machinery or by the generator itself. 310 | with self.assertRaises(StopAsyncIteration): 311 | async with woohoo(): 312 | raise StopAsyncIteration 313 | 314 | def _create_contextmanager_attribs(self): 315 | def attribs(**kw): 316 | def decorate(func): 317 | for k,v in kw.items(): 318 | setattr(func,k,v) 319 | return func 320 | return decorate 321 | @asynccontextmanager 322 | @attribs(foo='bar') 323 | async def baz(spam): 324 | """Whee!""" 325 | yield 326 | return baz 327 | 328 | def test_contextmanager_attribs(self): 329 | baz = self._create_contextmanager_attribs() 330 | self.assertEqual(baz.__name__,'baz') 331 | self.assertEqual(baz.foo, 'bar') 332 | 333 | @support.requires_docstrings 334 | def test_contextmanager_doc_attrib(self): 335 | baz = self._create_contextmanager_attribs() 336 | self.assertEqual(baz.__doc__, "Whee!") 337 | 338 | @support.requires_docstrings 339 | @_async_test 340 | async def test_instance_docstring_given_cm_docstring(self): 341 | baz = self._create_contextmanager_attribs()(None) 342 | self.assertEqual(baz.__doc__, "Whee!") 343 | async with baz: 344 | pass # suppress warning 345 | 346 | @_async_test 347 | async def test_keywords(self): 348 | # Ensure no keyword arguments are inhibited 349 | @asynccontextmanager 350 | async def woohoo(self, func, args, kwds): 351 | yield (self, func, args, kwds) 352 | async with woohoo(self=11, func=22, args=33, kwds=44) as target: 353 | self.assertEqual(target, (11, 22, 33, 44)) 354 | 355 | @_async_test 356 | async def test_recursive(self): 357 | depth = 0 358 | ncols = 0 359 | 360 | @asynccontextmanager 361 | async def woohoo(): 362 | nonlocal ncols 363 | ncols += 1 364 | 365 | nonlocal depth 366 | before = depth 367 | depth += 1 368 | yield 369 | depth -= 1 370 | self.assertEqual(depth, before) 371 | 372 | @woohoo() 373 | async def recursive(): 374 | if depth < 10: 375 | await recursive() 376 | 377 | await recursive() 378 | 379 | self.assertEqual(ncols, 10) 380 | self.assertEqual(depth, 0) 381 | 382 | @_async_test 383 | async def test_decorator(self): 384 | entered = False 385 | 386 | @asynccontextmanager 387 | async def context(): 388 | nonlocal entered 389 | entered = True 390 | yield 391 | entered = False 392 | 393 | @context() 394 | async def test(): 395 | self.assertTrue(entered) 396 | 397 | self.assertFalse(entered) 398 | await test() 399 | self.assertFalse(entered) 400 | 401 | @_async_test 402 | async def test_decorator_with_exception(self): 403 | entered = False 404 | 405 | @asynccontextmanager 406 | async def context(): 407 | nonlocal entered 408 | try: 409 | entered = True 410 | yield 411 | finally: 412 | entered = False 413 | 414 | @context() 415 | async def test(): 416 | self.assertTrue(entered) 417 | raise NameError('foo') 418 | 419 | self.assertFalse(entered) 420 | with self.assertRaisesRegex(NameError, 'foo'): 421 | await test() 422 | self.assertFalse(entered) 423 | 424 | @_async_test 425 | async def test_decorating_method(self): 426 | 427 | @asynccontextmanager 428 | async def context(): 429 | yield 430 | 431 | 432 | class Test(object): 433 | 434 | @context() 435 | async def method(self, a, b, c=None): 436 | self.a = a 437 | self.b = b 438 | self.c = c 439 | 440 | # these tests are for argument passing when used as a decorator 441 | test = Test() 442 | await test.method(1, 2) 443 | self.assertEqual(test.a, 1) 444 | self.assertEqual(test.b, 2) 445 | self.assertEqual(test.c, None) 446 | 447 | test = Test() 448 | await test.method('a', 'b', 'c') 449 | self.assertEqual(test.a, 'a') 450 | self.assertEqual(test.b, 'b') 451 | self.assertEqual(test.c, 'c') 452 | 453 | test = Test() 454 | await test.method(a=1, b=2) 455 | self.assertEqual(test.a, 1) 456 | self.assertEqual(test.b, 2) 457 | 458 | 459 | class AclosingTestCase(unittest.TestCase): 460 | 461 | @support.requires_docstrings 462 | def test_instance_docs(self): 463 | cm_docstring = aclosing.__doc__ 464 | obj = aclosing(None) 465 | self.assertEqual(obj.__doc__, cm_docstring) 466 | 467 | @_async_test 468 | async def test_aclosing(self): 469 | state = [] 470 | class C: 471 | async def aclose(self): 472 | state.append(1) 473 | x = C() 474 | self.assertEqual(state, []) 475 | async with aclosing(x) as y: 476 | self.assertEqual(x, y) 477 | self.assertEqual(state, [1]) 478 | 479 | @_async_test 480 | async def test_aclosing_error(self): 481 | state = [] 482 | class C: 483 | async def aclose(self): 484 | state.append(1) 485 | x = C() 486 | self.assertEqual(state, []) 487 | with self.assertRaises(ZeroDivisionError): 488 | async with aclosing(x) as y: 489 | self.assertEqual(x, y) 490 | 1 / 0 491 | self.assertEqual(state, [1]) 492 | 493 | @_async_test 494 | async def test_aclosing_bpo41229(self): 495 | state = [] 496 | 497 | @contextmanager 498 | def sync_resource(): 499 | try: 500 | yield 501 | finally: 502 | state.append(1) 503 | 504 | async def agenfunc(): 505 | with sync_resource(): 506 | yield -1 507 | yield -2 508 | 509 | x = agenfunc() 510 | self.assertEqual(state, []) 511 | with self.assertRaises(ZeroDivisionError): 512 | async with aclosing(x) as y: 513 | self.assertEqual(x, y) 514 | self.assertEqual(-1, await x.__anext__()) 515 | 1 / 0 516 | self.assertEqual(state, [1]) 517 | 518 | 519 | class TestAsyncExitStack(TestBaseExitStack, unittest.TestCase): 520 | class SyncAsyncExitStack(AsyncExitStack): 521 | @staticmethod 522 | def run_coroutine(coro): 523 | loop = asyncio.get_event_loop_policy().get_event_loop() 524 | t = loop.create_task(coro) 525 | t.add_done_callback(lambda f: loop.stop()) 526 | loop.run_forever() 527 | 528 | exc = t.exception() 529 | if not exc: 530 | return t.result() 531 | else: 532 | context = exc.__context__ 533 | 534 | try: 535 | raise exc 536 | except: 537 | exc.__context__ = context 538 | raise exc 539 | 540 | def close(self): 541 | return self.run_coroutine(self.aclose()) 542 | 543 | def __enter__(self): 544 | return self.run_coroutine(self.__aenter__()) 545 | 546 | def __exit__(self, *exc_details): 547 | return self.run_coroutine(self.__aexit__(*exc_details)) 548 | 549 | exit_stack = SyncAsyncExitStack 550 | callback_error_internal_frames = [ 551 | ('__exit__', 'return self.run_coroutine(self.__aexit__(*exc_details))'), 552 | ('run_coroutine', 'raise exc'), 553 | ('run_coroutine', 'raise exc'), 554 | ('__aexit__', 'raise exc_details[1]'), 555 | ('__aexit__', 'cb_suppress = cb(*exc_details)'), 556 | ] 557 | 558 | def setUp(self): 559 | self.loop = asyncio.new_event_loop() 560 | asyncio.set_event_loop(self.loop) 561 | self.addCleanup(self.loop.close) 562 | self.addCleanup(asyncio.set_event_loop_policy, None) 563 | 564 | @_async_test 565 | async def test_async_callback(self): 566 | expected = [ 567 | ((), {}), 568 | ((1,), {}), 569 | ((1,2), {}), 570 | ((), dict(example=1)), 571 | ((1,), dict(example=1)), 572 | ((1,2), dict(example=1)), 573 | ] 574 | result = [] 575 | async def _exit(*args, **kwds): 576 | """Test metadata propagation""" 577 | result.append((args, kwds)) 578 | 579 | async with AsyncExitStack() as stack: 580 | for args, kwds in reversed(expected): 581 | if args and kwds: 582 | f = stack.push_async_callback(_exit, *args, **kwds) 583 | elif args: 584 | f = stack.push_async_callback(_exit, *args) 585 | elif kwds: 586 | f = stack.push_async_callback(_exit, **kwds) 587 | else: 588 | f = stack.push_async_callback(_exit) 589 | self.assertIs(f, _exit) 590 | for wrapper in stack._exit_callbacks: 591 | self.assertIs(wrapper[1].__wrapped__, _exit) 592 | self.assertNotEqual(wrapper[1].__name__, _exit.__name__) 593 | self.assertIsNone(wrapper[1].__doc__, _exit.__doc__) 594 | 595 | self.assertEqual(result, expected) 596 | 597 | result = [] 598 | async with AsyncExitStack() as stack: 599 | with self.assertRaises(TypeError): 600 | stack.push_async_callback(arg=1) 601 | with self.assertRaises(TypeError): 602 | self.exit_stack.push_async_callback(arg=2) 603 | with self.assertRaises(TypeError): 604 | stack.push_async_callback(callback=_exit, arg=3) 605 | self.assertEqual(result, []) 606 | 607 | @_async_test 608 | async def test_async_push(self): 609 | exc_raised = ZeroDivisionError 610 | async def _expect_exc(exc_type, exc, exc_tb): 611 | self.assertIs(exc_type, exc_raised) 612 | async def _suppress_exc(*exc_details): 613 | return True 614 | async def _expect_ok(exc_type, exc, exc_tb): 615 | self.assertIsNone(exc_type) 616 | self.assertIsNone(exc) 617 | self.assertIsNone(exc_tb) 618 | class ExitCM(object): 619 | def __init__(self, check_exc): 620 | self.check_exc = check_exc 621 | async def __aenter__(self): 622 | self.fail("Should not be called!") 623 | async def __aexit__(self, *exc_details): 624 | await self.check_exc(*exc_details) 625 | 626 | async with self.exit_stack() as stack: 627 | stack.push_async_exit(_expect_ok) 628 | self.assertIs(stack._exit_callbacks[-1][1], _expect_ok) 629 | cm = ExitCM(_expect_ok) 630 | stack.push_async_exit(cm) 631 | self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) 632 | stack.push_async_exit(_suppress_exc) 633 | self.assertIs(stack._exit_callbacks[-1][1], _suppress_exc) 634 | cm = ExitCM(_expect_exc) 635 | stack.push_async_exit(cm) 636 | self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) 637 | stack.push_async_exit(_expect_exc) 638 | self.assertIs(stack._exit_callbacks[-1][1], _expect_exc) 639 | stack.push_async_exit(_expect_exc) 640 | self.assertIs(stack._exit_callbacks[-1][1], _expect_exc) 641 | 1/0 642 | 643 | @_async_test 644 | async def test_enter_async_context(self): 645 | class TestCM(object): 646 | async def __aenter__(self): 647 | result.append(1) 648 | async def __aexit__(self, *exc_details): 649 | result.append(3) 650 | 651 | result = [] 652 | cm = TestCM() 653 | 654 | async with AsyncExitStack() as stack: 655 | @stack.push_async_callback # Registered first => cleaned up last 656 | async def _exit(): 657 | result.append(4) 658 | self.assertIsNotNone(_exit) 659 | await stack.enter_async_context(cm) 660 | self.assertIs(stack._exit_callbacks[-1][1].__self__, cm) 661 | result.append(2) 662 | 663 | self.assertEqual(result, [1, 2, 3, 4]) 664 | 665 | @_async_test 666 | async def test_enter_async_context_errors(self): 667 | class LacksEnterAndExit: 668 | pass 669 | class LacksEnter: 670 | async def __aexit__(self, *exc_info): 671 | pass 672 | class LacksExit: 673 | async def __aenter__(self): 674 | pass 675 | 676 | expected_error, expected_text = support.cl2_cm_api_exc_info_async() 677 | async with self.exit_stack() as stack: 678 | with self.assertRaisesRegex(expected_error, expected_text): 679 | await stack.enter_async_context(LacksEnterAndExit()) 680 | with self.assertRaisesRegex(expected_error, expected_text): 681 | await stack.enter_async_context(LacksEnter()) 682 | with self.assertRaisesRegex(expected_error, expected_text): 683 | await stack.enter_async_context(LacksExit()) 684 | self.assertFalse(stack._exit_callbacks) 685 | 686 | @_async_test 687 | async def test_async_exit_exception_chaining(self): 688 | # Ensure exception chaining matches the reference behaviour 689 | async def raise_exc(exc): 690 | raise exc 691 | 692 | saved_details = None 693 | async def suppress_exc(*exc_details): 694 | nonlocal saved_details 695 | saved_details = exc_details 696 | return True 697 | 698 | try: 699 | async with self.exit_stack() as stack: 700 | stack.push_async_callback(raise_exc, IndexError) 701 | stack.push_async_callback(raise_exc, KeyError) 702 | stack.push_async_callback(raise_exc, AttributeError) 703 | stack.push_async_exit(suppress_exc) 704 | stack.push_async_callback(raise_exc, ValueError) 705 | 1 / 0 706 | except IndexError as exc: 707 | self.assertIsInstance(exc.__context__, KeyError) 708 | self.assertIsInstance(exc.__context__.__context__, AttributeError) 709 | # Inner exceptions were suppressed 710 | self.assertIsNone(exc.__context__.__context__.__context__) 711 | else: 712 | self.fail("Expected IndexError, but no exception was raised") 713 | # Check the inner exceptions 714 | inner_exc = saved_details[1] 715 | self.assertIsInstance(inner_exc, ValueError) 716 | self.assertIsInstance(inner_exc.__context__, ZeroDivisionError) 717 | 718 | @_async_test 719 | async def test_async_exit_exception_explicit_none_context(self): 720 | # Ensure AsyncExitStack chaining matches actual nested `with` statements 721 | # regarding explicit __context__ = None. 722 | 723 | class MyException(Exception): 724 | pass 725 | 726 | @asynccontextmanager 727 | async def my_cm(): 728 | try: 729 | yield 730 | except BaseException: 731 | exc = MyException() 732 | try: 733 | raise exc 734 | finally: 735 | exc.__context__ = None 736 | 737 | @asynccontextmanager 738 | async def my_cm_with_exit_stack(): 739 | async with self.exit_stack() as stack: 740 | await stack.enter_async_context(my_cm()) 741 | yield stack 742 | 743 | for cm in (my_cm, my_cm_with_exit_stack): 744 | with self.subTest(): 745 | try: 746 | async with cm(): 747 | raise IndexError() 748 | except MyException as exc: 749 | self.assertIsNone(exc.__context__) 750 | else: 751 | self.fail("Expected IndexError, but no exception was raised") 752 | 753 | @_async_test 754 | async def test_instance_bypass_async(self): 755 | class Example(object): pass 756 | cm = Example() 757 | cm.__aenter__ = object() 758 | cm.__aexit__ = object() 759 | stack = self.exit_stack() 760 | expected_error, expected_text = support.cl2_cm_api_exc_info_async() 761 | with self.assertRaisesRegex(expected_error, expected_text): 762 | await stack.enter_async_context(cm) 763 | stack.push_async_exit(cm) 764 | self.assertIs(stack._exit_callbacks[-1][1], cm) 765 | 766 | 767 | class TestAsyncNullcontext(unittest.TestCase): 768 | @_async_test 769 | async def test_async_nullcontext(self): 770 | class C: 771 | pass 772 | c = C() 773 | async with nullcontext(c) as c_in: 774 | self.assertIs(c_in, c) 775 | 776 | 777 | if __name__ == '__main__': 778 | unittest.main() 779 | -------------------------------------------------------------------------------- /contextlib2/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities for with-statement contexts. See PEP 343.""" 2 | import abc 3 | import os 4 | import sys 5 | import _collections_abc 6 | from collections import deque 7 | from functools import wraps 8 | from types import MethodType 9 | 10 | # Python 3.8 compatibility: GenericAlias may not be defined 11 | try: 12 | from types import GenericAlias 13 | except ImportError: 14 | # If the real GenericAlias type doesn't exist, __class_getitem__ won't be used, 15 | # so the fallback placeholder doesn't need to provide any meaningful behaviour 16 | # (typecheckers may still be unhappy, but for that problem the answer is 17 | # "use a newer Python version with better typechecking support") 18 | class GenericAlias: 19 | pass 20 | 21 | # Python 3.10 and earlier compatibility: BaseExceptionGroup may not be defined 22 | try: 23 | BaseExceptionGroup 24 | except NameError: 25 | # If the real BaseExceptionGroup type doesn't exist, it will never actually 26 | # be raised. This means the fallback placeholder doesn't need to provide 27 | # any meaningful behaviour, it just needs to be compatible with 'issubclass' 28 | class BaseExceptionGroup(BaseException): 29 | pass 30 | 31 | # Python 3.9 and earlier compatibility: anext may not be defined 32 | try: 33 | anext 34 | except NameError: 35 | def anext(obj, /): 36 | return obj.__anext__() 37 | 38 | # Python 3.11+ behaviour consistency: replace AttributeError with TypeError 39 | if sys.version_info >= (3, 11): 40 | # enter_context() and enter_async_context() follow the change in the 41 | # exception type raised by with statements and async with statements 42 | _CL2_ERROR_TO_CONVERT = AttributeError 43 | else: 44 | # On older versions, raise AttributeError without any changes 45 | class _CL2_ERROR_TO_CONVERT(Exception): 46 | pass 47 | 48 | 49 | __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", 50 | "AbstractContextManager", "AbstractAsyncContextManager", 51 | "AsyncExitStack", "ContextDecorator", "ExitStack", 52 | "redirect_stdout", "redirect_stderr", "suppress", "aclosing", 53 | "chdir"] 54 | 55 | 56 | class AbstractContextManager(abc.ABC): 57 | 58 | """An abstract base class for context managers.""" 59 | 60 | __class_getitem__ = classmethod(GenericAlias) 61 | 62 | def __enter__(self): 63 | """Return `self` upon entering the runtime context.""" 64 | return self 65 | 66 | @abc.abstractmethod 67 | def __exit__(self, exc_type, exc_value, traceback): 68 | """Raise any exception triggered within the runtime context.""" 69 | return None 70 | 71 | @classmethod 72 | def __subclasshook__(cls, C): 73 | if cls is AbstractContextManager: 74 | return _collections_abc._check_methods(C, "__enter__", "__exit__") 75 | return NotImplemented 76 | 77 | 78 | class AbstractAsyncContextManager(abc.ABC): 79 | 80 | """An abstract base class for asynchronous context managers.""" 81 | 82 | __class_getitem__ = classmethod(GenericAlias) 83 | 84 | async def __aenter__(self): 85 | """Return `self` upon entering the runtime context.""" 86 | return self 87 | 88 | @abc.abstractmethod 89 | async def __aexit__(self, exc_type, exc_value, traceback): 90 | """Raise any exception triggered within the runtime context.""" 91 | return None 92 | 93 | @classmethod 94 | def __subclasshook__(cls, C): 95 | if cls is AbstractAsyncContextManager: 96 | return _collections_abc._check_methods(C, "__aenter__", 97 | "__aexit__") 98 | return NotImplemented 99 | 100 | 101 | class ContextDecorator(object): 102 | "A base class or mixin that enables context managers to work as decorators." 103 | 104 | def refresh_cm(self): 105 | """Returns the context manager used to actually wrap the call to the 106 | decorated function. 107 | 108 | The default implementation just returns *self*. 109 | 110 | Overriding this method allows otherwise one-shot context managers 111 | like _GeneratorContextManager to support use as decorators via 112 | implicit recreation. 113 | 114 | DEPRECATED: refresh_cm was never added to the standard library's 115 | ContextDecorator API 116 | """ 117 | import warnings # Only import if needed for the deprecation warning 118 | warnings.warn("refresh_cm was never added to the standard library", 119 | DeprecationWarning) 120 | return self._recreate_cm() 121 | 122 | def _recreate_cm(self): 123 | """Return a recreated instance of self. 124 | 125 | Allows an otherwise one-shot context manager like 126 | _GeneratorContextManager to support use as 127 | a decorator via implicit recreation. 128 | 129 | This is a private interface just for _GeneratorContextManager. 130 | See issue #11647 for details. 131 | """ 132 | return self 133 | 134 | def __call__(self, func): 135 | @wraps(func) 136 | def inner(*args, **kwds): 137 | with self._recreate_cm(): 138 | return func(*args, **kwds) 139 | return inner 140 | 141 | 142 | class AsyncContextDecorator(object): 143 | "A base class or mixin that enables async context managers to work as decorators." 144 | 145 | def _recreate_cm(self): 146 | """Return a recreated instance of self. 147 | """ 148 | return self 149 | 150 | def __call__(self, func): 151 | @wraps(func) 152 | async def inner(*args, **kwds): 153 | async with self._recreate_cm(): 154 | return await func(*args, **kwds) 155 | return inner 156 | 157 | 158 | class _GeneratorContextManagerBase: 159 | """Shared functionality for @contextmanager and @asynccontextmanager.""" 160 | 161 | def __init__(self, func, args, kwds): 162 | self.gen = func(*args, **kwds) 163 | self.func, self.args, self.kwds = func, args, kwds 164 | # Issue 19330: ensure context manager instances have good docstrings 165 | doc = getattr(func, "__doc__", None) 166 | if doc is None: 167 | doc = type(self).__doc__ 168 | self.__doc__ = doc 169 | # Unfortunately, this still doesn't provide good help output when 170 | # inspecting the created context manager instances, since pydoc 171 | # currently bypasses the instance docstring and shows the docstring 172 | # for the class instead. 173 | # See http://bugs.python.org/issue19404 for more details. 174 | 175 | def _recreate_cm(self): 176 | # _GCMB instances are one-shot context managers, so the 177 | # CM must be recreated each time a decorated function is 178 | # called 179 | return self.__class__(self.func, self.args, self.kwds) 180 | 181 | 182 | class _GeneratorContextManager( 183 | _GeneratorContextManagerBase, 184 | AbstractContextManager, 185 | ContextDecorator, 186 | ): 187 | """Helper for @contextmanager decorator.""" 188 | 189 | def __enter__(self): 190 | # do not keep args and kwds alive unnecessarily 191 | # they are only needed for recreation, which is not possible anymore 192 | del self.args, self.kwds, self.func 193 | try: 194 | return next(self.gen) 195 | except StopIteration: 196 | raise RuntimeError("generator didn't yield") from None 197 | 198 | def __exit__(self, typ, value, traceback): 199 | if typ is None: 200 | try: 201 | next(self.gen) 202 | except StopIteration: 203 | return False 204 | else: 205 | try: 206 | raise RuntimeError("generator didn't stop") 207 | finally: 208 | self.gen.close() 209 | else: 210 | if value is None: 211 | # Need to force instantiation so we can reliably 212 | # tell if we get the same exception back 213 | value = typ() 214 | try: 215 | self.gen.throw(value) 216 | except StopIteration as exc: 217 | # Suppress StopIteration *unless* it's the same exception that 218 | # was passed to throw(). This prevents a StopIteration 219 | # raised inside the "with" statement from being suppressed. 220 | return exc is not value 221 | except RuntimeError as exc: 222 | # Don't re-raise the passed in exception. (issue27122) 223 | if exc is value: 224 | exc.__traceback__ = traceback 225 | return False 226 | # Avoid suppressing if a StopIteration exception 227 | # was passed to throw() and later wrapped into a RuntimeError 228 | # (see PEP 479 for sync generators; async generators also 229 | # have this behavior). But do this only if the exception wrapped 230 | # by the RuntimeError is actually Stop(Async)Iteration (see 231 | # issue29692). 232 | if ( 233 | isinstance(value, StopIteration) 234 | and exc.__cause__ is value 235 | ): 236 | value.__traceback__ = traceback 237 | return False 238 | raise 239 | except BaseException as exc: 240 | # only re-raise if it's *not* the exception that was 241 | # passed to throw(), because __exit__() must not raise 242 | # an exception unless __exit__() itself failed. But throw() 243 | # has to raise the exception to signal propagation, so this 244 | # fixes the impedance mismatch between the throw() protocol 245 | # and the __exit__() protocol. 246 | if exc is not value: 247 | raise 248 | exc.__traceback__ = traceback 249 | return False 250 | try: 251 | raise RuntimeError("generator didn't stop after throw()") 252 | finally: 253 | self.gen.close() 254 | 255 | class _AsyncGeneratorContextManager( 256 | _GeneratorContextManagerBase, 257 | AbstractAsyncContextManager, 258 | AsyncContextDecorator, 259 | ): 260 | """Helper for @asynccontextmanager decorator.""" 261 | 262 | async def __aenter__(self): 263 | # do not keep args and kwds alive unnecessarily 264 | # they are only needed for recreation, which is not possible anymore 265 | del self.args, self.kwds, self.func 266 | try: 267 | return await anext(self.gen) 268 | except StopAsyncIteration: 269 | raise RuntimeError("generator didn't yield") from None 270 | 271 | async def __aexit__(self, typ, value, traceback): 272 | if typ is None: 273 | try: 274 | await anext(self.gen) 275 | except StopAsyncIteration: 276 | return False 277 | else: 278 | try: 279 | raise RuntimeError("generator didn't stop") 280 | finally: 281 | await self.gen.aclose() 282 | else: 283 | if value is None: 284 | # Need to force instantiation so we can reliably 285 | # tell if we get the same exception back 286 | value = typ() 287 | try: 288 | await self.gen.athrow(value) 289 | except StopAsyncIteration as exc: 290 | # Suppress StopIteration *unless* it's the same exception that 291 | # was passed to throw(). This prevents a StopIteration 292 | # raised inside the "with" statement from being suppressed. 293 | return exc is not value 294 | except RuntimeError as exc: 295 | # Don't re-raise the passed in exception. (issue27122) 296 | if exc is value: 297 | exc.__traceback__ = traceback 298 | return False 299 | # Avoid suppressing if a Stop(Async)Iteration exception 300 | # was passed to athrow() and later wrapped into a RuntimeError 301 | # (see PEP 479 for sync generators; async generators also 302 | # have this behavior). But do this only if the exception wrapped 303 | # by the RuntimeError is actually Stop(Async)Iteration (see 304 | # issue29692). 305 | if ( 306 | isinstance(value, (StopIteration, StopAsyncIteration)) 307 | and exc.__cause__ is value 308 | ): 309 | value.__traceback__ = traceback 310 | return False 311 | raise 312 | except BaseException as exc: 313 | # only re-raise if it's *not* the exception that was 314 | # passed to throw(), because __exit__() must not raise 315 | # an exception unless __exit__() itself failed. But throw() 316 | # has to raise the exception to signal propagation, so this 317 | # fixes the impedance mismatch between the throw() protocol 318 | # and the __exit__() protocol. 319 | if exc is not value: 320 | raise 321 | exc.__traceback__ = traceback 322 | return False 323 | try: 324 | raise RuntimeError("generator didn't stop after athrow()") 325 | finally: 326 | await self.gen.aclose() 327 | 328 | 329 | def contextmanager(func): 330 | """@contextmanager decorator. 331 | 332 | Typical usage: 333 | 334 | @contextmanager 335 | def some_generator(): 336 | 337 | try: 338 | yield 339 | finally: 340 | 341 | 342 | This makes this: 343 | 344 | with some_generator() as : 345 | 346 | 347 | equivalent to this: 348 | 349 | 350 | try: 351 | = 352 | 353 | finally: 354 | 355 | """ 356 | @wraps(func) 357 | def helper(*args, **kwds): 358 | return _GeneratorContextManager(func, args, kwds) 359 | return helper 360 | 361 | 362 | def asynccontextmanager(func): 363 | """@asynccontextmanager decorator. 364 | 365 | Typical usage: 366 | 367 | @asynccontextmanager 368 | async def some_async_generator(): 369 | 370 | try: 371 | yield 372 | finally: 373 | 374 | 375 | This makes this: 376 | 377 | async with some_async_generator() as : 378 | 379 | 380 | equivalent to this: 381 | 382 | 383 | try: 384 | = 385 | 386 | finally: 387 | 388 | """ 389 | @wraps(func) 390 | def helper(*args, **kwds): 391 | return _AsyncGeneratorContextManager(func, args, kwds) 392 | return helper 393 | 394 | 395 | class closing(AbstractContextManager): 396 | """Context to automatically close something at the end of a block. 397 | 398 | Code like this: 399 | 400 | with closing(.open()) as f: 401 | 402 | 403 | is equivalent to this: 404 | 405 | f = .open() 406 | try: 407 | 408 | finally: 409 | f.close() 410 | 411 | """ 412 | def __init__(self, thing): 413 | self.thing = thing 414 | def __enter__(self): 415 | return self.thing 416 | def __exit__(self, *exc_info): 417 | self.thing.close() 418 | 419 | 420 | class aclosing(AbstractAsyncContextManager): 421 | """Async context manager for safely finalizing an asynchronously cleaned-up 422 | resource such as an async generator, calling its ``aclose()`` method. 423 | 424 | Code like this: 425 | 426 | async with aclosing(.fetch()) as agen: 427 | 428 | 429 | is equivalent to this: 430 | 431 | agen = .fetch() 432 | try: 433 | 434 | finally: 435 | await agen.aclose() 436 | 437 | """ 438 | def __init__(self, thing): 439 | self.thing = thing 440 | async def __aenter__(self): 441 | return self.thing 442 | async def __aexit__(self, *exc_info): 443 | await self.thing.aclose() 444 | 445 | 446 | class _RedirectStream(AbstractContextManager): 447 | 448 | _stream = None 449 | 450 | def __init__(self, new_target): 451 | self._new_target = new_target 452 | # We use a list of old targets to make this CM re-entrant 453 | self._old_targets = [] 454 | 455 | def __enter__(self): 456 | self._old_targets.append(getattr(sys, self._stream)) 457 | setattr(sys, self._stream, self._new_target) 458 | return self._new_target 459 | 460 | def __exit__(self, exctype, excinst, exctb): 461 | setattr(sys, self._stream, self._old_targets.pop()) 462 | 463 | 464 | class redirect_stdout(_RedirectStream): 465 | """Context manager for temporarily redirecting stdout to another file. 466 | 467 | # How to send help() to stderr 468 | with redirect_stdout(sys.stderr): 469 | help(dir) 470 | 471 | # How to write help() to a file 472 | with open('help.txt', 'w') as f: 473 | with redirect_stdout(f): 474 | help(pow) 475 | """ 476 | 477 | _stream = "stdout" 478 | 479 | 480 | class redirect_stderr(_RedirectStream): 481 | """Context manager for temporarily redirecting stderr to another file.""" 482 | 483 | _stream = "stderr" 484 | 485 | 486 | class suppress(AbstractContextManager): 487 | """Context manager to suppress specified exceptions 488 | 489 | After the exception is suppressed, execution proceeds with the next 490 | statement following the with statement. 491 | 492 | with suppress(FileNotFoundError): 493 | os.remove(somefile) 494 | # Execution still resumes here if the file was already removed 495 | """ 496 | 497 | def __init__(self, *exceptions): 498 | self._exceptions = exceptions 499 | 500 | def __enter__(self): 501 | pass 502 | 503 | def __exit__(self, exctype, excinst, exctb): 504 | # Unlike isinstance and issubclass, CPython exception handling 505 | # currently only looks at the concrete type hierarchy (ignoring 506 | # the instance and subclass checking hooks). While Guido considers 507 | # that a bug rather than a feature, it's a fairly hard one to fix 508 | # due to various internal implementation details. suppress provides 509 | # the simpler issubclass based semantics, rather than trying to 510 | # exactly reproduce the limitations of the CPython interpreter. 511 | # 512 | # See http://bugs.python.org/issue12029 for more details 513 | if exctype is None: 514 | return 515 | if issubclass(exctype, self._exceptions): 516 | return True 517 | if issubclass(exctype, BaseExceptionGroup): 518 | match, rest = excinst.split(self._exceptions) 519 | if rest is None: 520 | return True 521 | raise rest 522 | return False 523 | 524 | 525 | class _BaseExitStack: 526 | """A base class for ExitStack and AsyncExitStack.""" 527 | 528 | @staticmethod 529 | def _create_exit_wrapper(cm, cm_exit): 530 | return MethodType(cm_exit, cm) 531 | 532 | @staticmethod 533 | def _create_cb_wrapper(callback, /, *args, **kwds): 534 | def _exit_wrapper(exc_type, exc, tb): 535 | callback(*args, **kwds) 536 | return _exit_wrapper 537 | 538 | def __init__(self): 539 | self._exit_callbacks = deque() 540 | 541 | def pop_all(self): 542 | """Preserve the context stack by transferring it to a new instance.""" 543 | new_stack = type(self)() 544 | new_stack._exit_callbacks = self._exit_callbacks 545 | self._exit_callbacks = deque() 546 | return new_stack 547 | 548 | def push(self, exit): 549 | """Registers a callback with the standard __exit__ method signature. 550 | 551 | Can suppress exceptions the same way __exit__ method can. 552 | Also accepts any object with an __exit__ method (registering a call 553 | to the method instead of the object itself). 554 | """ 555 | # We use an unbound method rather than a bound method to follow 556 | # the standard lookup behaviour for special methods. 557 | _cb_type = type(exit) 558 | 559 | try: 560 | exit_method = _cb_type.__exit__ 561 | except AttributeError: 562 | # Not a context manager, so assume it's a callable. 563 | self._push_exit_callback(exit) 564 | else: 565 | self._push_cm_exit(exit, exit_method) 566 | return exit # Allow use as a decorator. 567 | 568 | def enter_context(self, cm): 569 | """Enters the supplied context manager. 570 | 571 | If successful, also pushes its __exit__ method as a callback and 572 | returns the result of the __enter__ method. 573 | """ 574 | # We look up the special methods on the type to match the with 575 | # statement. 576 | cls = type(cm) 577 | try: 578 | _enter = cls.__enter__ 579 | _exit = cls.__exit__ 580 | except _CL2_ERROR_TO_CONVERT: 581 | raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " 582 | f"not support the context manager protocol") from None 583 | result = _enter(cm) 584 | self._push_cm_exit(cm, _exit) 585 | return result 586 | 587 | def callback(self, callback, /, *args, **kwds): 588 | """Registers an arbitrary callback and arguments. 589 | 590 | Cannot suppress exceptions. 591 | """ 592 | _exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds) 593 | 594 | # We changed the signature, so using @wraps is not appropriate, but 595 | # setting __wrapped__ may still help with introspection. 596 | _exit_wrapper.__wrapped__ = callback 597 | self._push_exit_callback(_exit_wrapper) 598 | return callback # Allow use as a decorator 599 | 600 | def _push_cm_exit(self, cm, cm_exit): 601 | """Helper to correctly register callbacks to __exit__ methods.""" 602 | _exit_wrapper = self._create_exit_wrapper(cm, cm_exit) 603 | self._push_exit_callback(_exit_wrapper, True) 604 | 605 | def _push_exit_callback(self, callback, is_sync=True): 606 | self._exit_callbacks.append((is_sync, callback)) 607 | 608 | 609 | # Inspired by discussions on http://bugs.python.org/issue13585 610 | class ExitStack(_BaseExitStack, AbstractContextManager): 611 | """Context manager for dynamic management of a stack of exit callbacks. 612 | 613 | For example: 614 | with ExitStack() as stack: 615 | files = [stack.enter_context(open(fname)) for fname in filenames] 616 | # All opened files will automatically be closed at the end of 617 | # the with statement, even if attempts to open files later 618 | # in the list raise an exception. 619 | """ 620 | 621 | def __enter__(self): 622 | return self 623 | 624 | def __exit__(self, *exc_details): 625 | received_exc = exc_details[0] is not None 626 | 627 | # We manipulate the exception state so it behaves as though 628 | # we were actually nesting multiple with statements 629 | frame_exc = sys.exc_info()[1] 630 | def _fix_exception_context(new_exc, old_exc): 631 | # Context may not be correct, so find the end of the chain 632 | while 1: 633 | exc_context = new_exc.__context__ 634 | if exc_context is None or exc_context is old_exc: 635 | # Context is already set correctly (see issue 20317) 636 | return 637 | if exc_context is frame_exc: 638 | break 639 | new_exc = exc_context 640 | # Change the end of the chain to point to the exception 641 | # we expect it to reference 642 | new_exc.__context__ = old_exc 643 | 644 | # Callbacks are invoked in LIFO order to match the behaviour of 645 | # nested context managers 646 | suppressed_exc = False 647 | pending_raise = False 648 | while self._exit_callbacks: 649 | is_sync, cb = self._exit_callbacks.pop() 650 | assert is_sync 651 | try: 652 | if cb(*exc_details): 653 | suppressed_exc = True 654 | pending_raise = False 655 | exc_details = (None, None, None) 656 | except: 657 | new_exc_details = sys.exc_info() 658 | # simulate the stack of exceptions by setting the context 659 | _fix_exception_context(new_exc_details[1], exc_details[1]) 660 | pending_raise = True 661 | exc_details = new_exc_details 662 | if pending_raise: 663 | try: 664 | # bare "raise exc_details[1]" replaces our carefully 665 | # set-up context 666 | fixed_ctx = exc_details[1].__context__ 667 | raise exc_details[1] 668 | except BaseException: 669 | exc_details[1].__context__ = fixed_ctx 670 | raise 671 | return received_exc and suppressed_exc 672 | 673 | def close(self): 674 | """Immediately unwind the context stack.""" 675 | self.__exit__(None, None, None) 676 | 677 | 678 | # Inspired by discussions on https://bugs.python.org/issue29302 679 | class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): 680 | """Async context manager for dynamic management of a stack of exit 681 | callbacks. 682 | 683 | For example: 684 | async with AsyncExitStack() as stack: 685 | connections = [await stack.enter_async_context(get_connection()) 686 | for i in range(5)] 687 | # All opened connections will automatically be released at the 688 | # end of the async with statement, even if attempts to open a 689 | # connection later in the list raise an exception. 690 | """ 691 | 692 | @staticmethod 693 | def _create_async_exit_wrapper(cm, cm_exit): 694 | return MethodType(cm_exit, cm) 695 | 696 | @staticmethod 697 | def _create_async_cb_wrapper(callback, /, *args, **kwds): 698 | async def _exit_wrapper(exc_type, exc, tb): 699 | await callback(*args, **kwds) 700 | return _exit_wrapper 701 | 702 | async def enter_async_context(self, cm): 703 | """Enters the supplied async context manager. 704 | 705 | If successful, also pushes its __aexit__ method as a callback and 706 | returns the result of the __aenter__ method. 707 | """ 708 | cls = type(cm) 709 | try: 710 | _enter = cls.__aenter__ 711 | _exit = cls.__aexit__ 712 | except _CL2_ERROR_TO_CONVERT: 713 | raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " 714 | f"not support the asynchronous context manager protocol" 715 | ) from None 716 | result = await _enter(cm) 717 | self._push_async_cm_exit(cm, _exit) 718 | return result 719 | 720 | def push_async_exit(self, exit): 721 | """Registers a coroutine function with the standard __aexit__ method 722 | signature. 723 | 724 | Can suppress exceptions the same way __aexit__ method can. 725 | Also accepts any object with an __aexit__ method (registering a call 726 | to the method instead of the object itself). 727 | """ 728 | _cb_type = type(exit) 729 | try: 730 | exit_method = _cb_type.__aexit__ 731 | except AttributeError: 732 | # Not an async context manager, so assume it's a coroutine function 733 | self._push_exit_callback(exit, False) 734 | else: 735 | self._push_async_cm_exit(exit, exit_method) 736 | return exit # Allow use as a decorator 737 | 738 | def push_async_callback(self, callback, /, *args, **kwds): 739 | """Registers an arbitrary coroutine function and arguments. 740 | 741 | Cannot suppress exceptions. 742 | """ 743 | _exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds) 744 | 745 | # We changed the signature, so using @wraps is not appropriate, but 746 | # setting __wrapped__ may still help with introspection. 747 | _exit_wrapper.__wrapped__ = callback 748 | self._push_exit_callback(_exit_wrapper, False) 749 | return callback # Allow use as a decorator 750 | 751 | async def aclose(self): 752 | """Immediately unwind the context stack.""" 753 | await self.__aexit__(None, None, None) 754 | 755 | def _push_async_cm_exit(self, cm, cm_exit): 756 | """Helper to correctly register coroutine function to __aexit__ 757 | method.""" 758 | _exit_wrapper = self._create_async_exit_wrapper(cm, cm_exit) 759 | self._push_exit_callback(_exit_wrapper, False) 760 | 761 | async def __aenter__(self): 762 | return self 763 | 764 | async def __aexit__(self, *exc_details): 765 | received_exc = exc_details[0] is not None 766 | 767 | # We manipulate the exception state so it behaves as though 768 | # we were actually nesting multiple with statements 769 | frame_exc = sys.exc_info()[1] 770 | def _fix_exception_context(new_exc, old_exc): 771 | # Context may not be correct, so find the end of the chain 772 | while 1: 773 | exc_context = new_exc.__context__ 774 | if exc_context is None or exc_context is old_exc: 775 | # Context is already set correctly (see issue 20317) 776 | return 777 | if exc_context is frame_exc: 778 | break 779 | new_exc = exc_context 780 | # Change the end of the chain to point to the exception 781 | # we expect it to reference 782 | new_exc.__context__ = old_exc 783 | 784 | # Callbacks are invoked in LIFO order to match the behaviour of 785 | # nested context managers 786 | suppressed_exc = False 787 | pending_raise = False 788 | while self._exit_callbacks: 789 | is_sync, cb = self._exit_callbacks.pop() 790 | try: 791 | if is_sync: 792 | cb_suppress = cb(*exc_details) 793 | else: 794 | cb_suppress = await cb(*exc_details) 795 | 796 | if cb_suppress: 797 | suppressed_exc = True 798 | pending_raise = False 799 | exc_details = (None, None, None) 800 | except: 801 | new_exc_details = sys.exc_info() 802 | # simulate the stack of exceptions by setting the context 803 | _fix_exception_context(new_exc_details[1], exc_details[1]) 804 | pending_raise = True 805 | exc_details = new_exc_details 806 | if pending_raise: 807 | try: 808 | # bare "raise exc_details[1]" replaces our carefully 809 | # set-up context 810 | fixed_ctx = exc_details[1].__context__ 811 | raise exc_details[1] 812 | except BaseException: 813 | exc_details[1].__context__ = fixed_ctx 814 | raise 815 | return received_exc and suppressed_exc 816 | 817 | 818 | class nullcontext(AbstractContextManager, AbstractAsyncContextManager): 819 | """Context manager that does no additional processing. 820 | 821 | Used as a stand-in for a normal context manager, when a particular 822 | block of code is only sometimes used with a normal context manager: 823 | 824 | cm = optional_cm if condition else nullcontext() 825 | with cm: 826 | # Perform operation, using optional_cm if condition is True 827 | """ 828 | 829 | def __init__(self, enter_result=None): 830 | self.enter_result = enter_result 831 | 832 | def __enter__(self): 833 | return self.enter_result 834 | 835 | def __exit__(self, *excinfo): 836 | pass 837 | 838 | async def __aenter__(self): 839 | return self.enter_result 840 | 841 | async def __aexit__(self, *excinfo): 842 | pass 843 | 844 | 845 | class chdir(AbstractContextManager): 846 | """Non thread-safe context manager to change the current working directory.""" 847 | 848 | def __init__(self, path): 849 | self.path = path 850 | self._old_cwd = [] 851 | 852 | def __enter__(self): 853 | self._old_cwd.append(os.getcwd()) 854 | os.chdir(self.path) 855 | 856 | def __exit__(self, *excinfo): 857 | os.chdir(self._old_cwd.pop()) 858 | 859 | # Preserve backwards compatibility 860 | class ContextStack(ExitStack): 861 | """(DEPRECATED) Backwards compatibility alias for ExitStack""" 862 | 863 | def __init__(self): 864 | import warnings # Only import if needed for the deprecation warning 865 | warnings.warn("ContextStack has been renamed to ExitStack", 866 | DeprecationWarning) 867 | super(ContextStack, self).__init__() 868 | 869 | def register_exit(self, callback): 870 | return self.push(callback) 871 | 872 | def register(self, callback, *args, **kwds): 873 | return self.callback(callback, *args, **kwds) 874 | 875 | def preserve(self): 876 | return self.pop_all() 877 | -------------------------------------------------------------------------------- /docs/contextlib2.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ------------- 3 | 4 | Functions and classes provided: 5 | 6 | .. class:: AbstractContextManager 7 | 8 | An :term:`abstract base class` for classes that implement 9 | :meth:`object.__enter__` and :meth:`object.__exit__`. A default 10 | implementation for :meth:`object.__enter__` is provided which returns 11 | ``self`` while :meth:`object.__exit__` is an abstract method which by default 12 | returns ``None``. See also the definition of :ref:`typecontextmanager`. 13 | 14 | .. versionadded:: 0.6.0 15 | Part of the standard library in Python 3.6 and later 16 | 17 | .. class:: AbstractAsyncContextManager 18 | 19 | An :term:`abstract base class` for classes that implement 20 | :meth:`object.__aenter__` and :meth:`object.__aexit__`. A default 21 | implementation for :meth:`object.__aenter__` is provided which returns 22 | ``self`` while :meth:`object.__aexit__` is an abstract method which by default 23 | returns ``None``. See also the definition of 24 | :ref:`async-context-managers`. 25 | 26 | .. versionadded:: 21.6.0 27 | Part of the standard library in Python 3.7 and later 28 | 29 | .. decorator:: contextmanager 30 | 31 | This function is a :term:`decorator` that can be used to define a factory 32 | function for :keyword:`with` statement context managers, without needing to 33 | create a class or separate :meth:`~object.__enter__` and :meth:`~object.__exit__` methods. 34 | 35 | While many objects natively support use in with statements, sometimes a 36 | resource needs to be managed that isn't a context manager in its own right, 37 | and doesn't implement a ``close()`` method for use with ``contextlib2.closing`` 38 | 39 | An abstract example would be the following to ensure correct resource 40 | management:: 41 | 42 | from contextlib2 import contextmanager 43 | 44 | @contextmanager 45 | def managed_resource(*args, **kwds): 46 | # Code to acquire resource, e.g.: 47 | resource = acquire_resource(*args, **kwds) 48 | try: 49 | yield resource 50 | finally: 51 | # Code to release resource, e.g.: 52 | release_resource(resource) 53 | 54 | The function can then be used like this:: 55 | 56 | >>> with managed_resource(timeout=3600) as resource: 57 | ... # Resource is released at the end of this block, 58 | ... # even if code in the block raises an exception 59 | 60 | The function being decorated must return a :term:`generator`-iterator when 61 | called. This iterator must yield exactly one value, which will be bound to 62 | the targets in the :keyword:`with` statement's :keyword:`!as` clause, if any. 63 | 64 | At the point where the generator yields, the block nested in the :keyword:`with` 65 | statement is executed. The generator is then resumed after the block is exited. 66 | If an unhandled exception occurs in the block, it is reraised inside the 67 | generator at the point where the yield occurred. Thus, you can use a 68 | :keyword:`try`...\ :keyword:`except`...\ :keyword:`finally` statement to trap 69 | the error (if any), or ensure that some cleanup takes place. If an exception is 70 | trapped merely in order to log it or to perform some action (rather than to 71 | suppress it entirely), the generator must reraise that exception. Otherwise the 72 | generator context manager will indicate to the :keyword:`!with` statement that 73 | the exception has been handled, and execution will resume with the statement 74 | immediately following the :keyword:`!with` statement. 75 | 76 | :func:`contextmanager` uses :class:`ContextDecorator` so the context managers 77 | it creates can be used as decorators as well as in :keyword:`with` statements. 78 | When used as a decorator, a new generator instance is implicitly created on 79 | each function call (this allows the otherwise "one-shot" context managers 80 | created by :func:`contextmanager` to meet the requirement that context 81 | managers support multiple invocations in order to be used as decorators). 82 | 83 | 84 | .. decorator:: asynccontextmanager 85 | 86 | Similar to :func:`~contextlib2.contextmanager`, but creates an 87 | :ref:`asynchronous context manager `. 88 | 89 | This function is a :term:`decorator` that can be used to define a factory 90 | function for :keyword:`async with` statement asynchronous context managers, 91 | without needing to create a class or separate :meth:`~object.__aenter__` and 92 | :meth:`~object.__aexit__` methods. It must be applied to an :term:`asynchronous 93 | generator` function. 94 | 95 | A simple example:: 96 | 97 | from contextlib2 import asynccontextmanager 98 | 99 | @asynccontextmanager 100 | async def get_connection(): 101 | conn = await acquire_db_connection() 102 | try: 103 | yield conn 104 | finally: 105 | await release_db_connection(conn) 106 | 107 | async def get_all_users(): 108 | async with get_connection() as conn: 109 | return conn.query('SELECT ...') 110 | 111 | .. versionadded:: 21.6.0 112 | Part of the standard library in Python 3.7 and later, enhanced in 113 | Python 3.10 and later to allow created async context managers to be used 114 | as async function decorators. 115 | 116 | Context managers defined with :func:`asynccontextmanager` can be used 117 | either as decorators or with :keyword:`async with` statements:: 118 | 119 | import time 120 | from contextlib2 import asynccontextmanager 121 | 122 | @asynccontextmanager 123 | async def timeit(): 124 | now = time.monotonic() 125 | try: 126 | yield 127 | finally: 128 | print(f'it took {time.monotonic() - now}s to run') 129 | 130 | @timeit() 131 | async def main(): 132 | # ... async code ... 133 | 134 | When used as a decorator, a new generator instance is implicitly created on 135 | each function call. This allows the otherwise "one-shot" context managers 136 | created by :func:`asynccontextmanager` to meet the requirement that context 137 | managers support multiple invocations in order to be used as decorators. 138 | 139 | 140 | .. function:: closing(thing) 141 | 142 | Return a context manager that closes *thing* upon completion of the block. This 143 | is basically equivalent to:: 144 | 145 | from contextlib2 import contextmanager 146 | 147 | @contextmanager 148 | def closing(thing): 149 | try: 150 | yield thing 151 | finally: 152 | thing.close() 153 | 154 | And lets you write code like this:: 155 | 156 | from contextlib2 import closing 157 | from urllib.request import urlopen 158 | 159 | with closing(urlopen('https://www.python.org')) as page: 160 | for line in page: 161 | print(line) 162 | 163 | without needing to explicitly close ``page``. Even if an error occurs, 164 | ``page.close()`` will be called when the :keyword:`with` block is exited. 165 | 166 | .. note:: 167 | 168 | Most types managing resources support the :term:`context manager` protocol, 169 | which closes *thing* on leaving the :keyword:`with` statement. 170 | As such, :func:`!closing` is most useful for third party types that don't 171 | support context managers. 172 | This example is purely for illustration purposes, 173 | as :func:`~urllib.request.urlopen` would normally be used in a context manager. 174 | 175 | .. function:: aclosing(thing) 176 | 177 | Return an async context manager that calls the ``aclose()`` method of *thing* 178 | upon completion of the block. This is basically equivalent to:: 179 | 180 | from contextlib2 import asynccontextmanager 181 | 182 | @asynccontextmanager 183 | async def aclosing(thing): 184 | try: 185 | yield thing 186 | finally: 187 | await thing.aclose() 188 | 189 | Significantly, ``aclosing()`` supports deterministic cleanup of async 190 | generators when they happen to exit early by :keyword:`break` or an 191 | exception. For example:: 192 | 193 | from contextlib2 import aclosing 194 | 195 | async with aclosing(my_generator()) as values: 196 | async for value in values: 197 | if value == 42: 198 | break 199 | 200 | This pattern ensures that the generator's async exit code is executed in 201 | the same context as its iterations (so that exceptions and context 202 | variables work as expected, and the exit code isn't run after the 203 | lifetime of some task it depends on). 204 | 205 | .. versionadded:: 21.6.0 206 | Part of the standard library in Python 3.10 and later 207 | 208 | 209 | .. _simplifying-support-for-single-optional-context-managers: 210 | 211 | .. function:: nullcontext(enter_result=None) 212 | 213 | Return a context manager that returns *enter_result* from ``__enter__``, but 214 | otherwise does nothing. It is intended to be used as a stand-in for an 215 | optional context manager, for example:: 216 | 217 | def myfunction(arg, ignore_exceptions=False): 218 | if ignore_exceptions: 219 | # Use suppress to ignore all exceptions. 220 | cm = contextlib2.suppress(Exception) 221 | else: 222 | # Do not ignore any exceptions, cm has no effect. 223 | cm = contextlib2.nullcontext() 224 | with cm: 225 | # Do something 226 | 227 | An example using *enter_result*:: 228 | 229 | def process_file(file_or_path): 230 | if isinstance(file_or_path, str): 231 | # If string, open file 232 | cm = open(file_or_path) 233 | else: 234 | # Caller is responsible for closing file 235 | cm = nullcontext(file_or_path) 236 | 237 | with cm as file: 238 | # Perform processing on the file 239 | 240 | It can also be used as a stand-in for 241 | :ref:`asynchronous context managers `:: 242 | 243 | async def send_http(session=None): 244 | if not session: 245 | # If no http session, create it with aiohttp 246 | cm = aiohttp.ClientSession() 247 | else: 248 | # Caller is responsible for closing the session 249 | cm = nullcontext(session) 250 | 251 | async with cm as session: 252 | # Send http requests with session 253 | 254 | .. versionadded:: 0.6.0 255 | Part of the standard library in Python 3.7 and later 256 | 257 | .. versionchanged:: 21.6.0 258 | Updated to Python 3.10 version with :term:`asynchronous context manager` support 259 | 260 | 261 | .. function:: suppress(*exceptions) 262 | 263 | Return a context manager that suppresses any of the specified exceptions 264 | if they occur in the body of a :keyword:`!with` statement and then 265 | resumes execution with the first statement following the end of the 266 | :keyword:`!with` statement. 267 | 268 | As with any other mechanism that completely suppresses exceptions, this 269 | context manager should be used only to cover very specific errors where 270 | silently continuing with program execution is known to be the right 271 | thing to do. 272 | 273 | For example:: 274 | 275 | from contextlib2 import suppress 276 | 277 | with suppress(FileNotFoundError): 278 | os.remove('somefile.tmp') 279 | 280 | with suppress(FileNotFoundError): 281 | os.remove('someotherfile.tmp') 282 | 283 | This code is equivalent to:: 284 | 285 | try: 286 | os.remove('somefile.tmp') 287 | except FileNotFoundError: 288 | pass 289 | 290 | try: 291 | os.remove('someotherfile.tmp') 292 | except FileNotFoundError: 293 | pass 294 | 295 | This context manager is :ref:`reentrant `. 296 | 297 | If the code within the :keyword:`!with` block raises a 298 | :exc:`BaseExceptionGroup`, suppressed exceptions are removed from the 299 | group. If any exceptions in the group are not suppressed, a group containing 300 | them is re-raised. 301 | 302 | .. versionadded:: 0.5 303 | Part of the standard library in Python 3.4 and later 304 | 305 | .. versionchanged:: 24.6.0 306 | Updated to Python 3.12 version that supports suppressing exceptions raised 307 | as part of a :exc:`BaseExceptionGroup`. 308 | 309 | .. function:: redirect_stdout(new_target) 310 | 311 | Context manager for temporarily redirecting :data:`sys.stdout` to 312 | another file or file-like object. 313 | 314 | This tool adds flexibility to existing functions or classes whose output 315 | is hardwired to stdout. 316 | 317 | For example, the output of :func:`help` normally is sent to *sys.stdout*. 318 | You can capture that output in a string by redirecting the output to an 319 | :class:`io.StringIO` object. The replacement stream is returned from the 320 | ``__enter__`` method and so is available as the target of the 321 | :keyword:`with` statement:: 322 | 323 | with redirect_stdout(io.StringIO()) as f: 324 | help(pow) 325 | s = f.getvalue() 326 | 327 | To send the output of :func:`help` to a file on disk, redirect the output 328 | to a regular file:: 329 | 330 | with open('help.txt', 'w') as f: 331 | with redirect_stdout(f): 332 | help(pow) 333 | 334 | To send the output of :func:`help` to *sys.stderr*:: 335 | 336 | with redirect_stdout(sys.stderr): 337 | help(pow) 338 | 339 | Note that the global side effect on :data:`sys.stdout` means that this 340 | context manager is not suitable for use in library code and most threaded 341 | applications. It also has no effect on the output of subprocesses. 342 | However, it is still a useful approach for many utility scripts. 343 | 344 | This context manager is :ref:`reentrant `. 345 | 346 | .. versionadded:: 0.5 347 | Part of the standard library in Python 3.4 and later 348 | 349 | 350 | .. function:: redirect_stderr(new_target) 351 | 352 | Similar to :func:`~contextlib2.redirect_stdout` but redirecting 353 | :data:`sys.stderr` to another file or file-like object. 354 | 355 | This context manager is :ref:`reentrant `. 356 | 357 | .. versionadded:: 0.5 358 | Part of the standard library in Python 3.5 and later 359 | 360 | 361 | .. function:: chdir(path) 362 | 363 | Non parallel-safe context manager to change the current working directory. 364 | As this changes a global state, the working directory, it is not suitable 365 | for use in most threaded or async contexts. It is also not suitable for most 366 | non-linear code execution, like generators, where the program execution is 367 | temporarily relinquished -- unless explicitly desired, you should not yield 368 | when this context manager is active. 369 | 370 | This is a simple wrapper around :func:`~os.chdir`, it changes the current 371 | working directory upon entering and restores the old one on exit. 372 | 373 | This context manager is :ref:`reentrant `. 374 | 375 | .. versionadded:: 24.6.0 376 | Part of the standard library in Python 3.11 and later 377 | 378 | 379 | .. class:: ContextDecorator() 380 | 381 | A base class that enables a context manager to also be used as a decorator. 382 | 383 | Context managers inheriting from ``ContextDecorator`` have to implement 384 | ``__enter__`` and ``__exit__`` as normal. ``__exit__`` retains its optional 385 | exception handling even when used as a decorator. 386 | 387 | ``ContextDecorator`` is used by :func:`contextmanager`, so you get this 388 | functionality automatically. 389 | 390 | Example of ``ContextDecorator``:: 391 | 392 | from contextlib2 import ContextDecorator 393 | 394 | class mycontext(ContextDecorator): 395 | def __enter__(self): 396 | print('Starting') 397 | return self 398 | 399 | def __exit__(self, *exc): 400 | print('Finishing') 401 | return False 402 | 403 | The class can then be used like this:: 404 | 405 | >>> @mycontext() 406 | ... def function(): 407 | ... print('The bit in the middle') 408 | ... 409 | >>> function() 410 | Starting 411 | The bit in the middle 412 | Finishing 413 | 414 | >>> with mycontext(): 415 | ... print('The bit in the middle') 416 | ... 417 | Starting 418 | The bit in the middle 419 | Finishing 420 | 421 | This change is just syntactic sugar for any construct of the following form:: 422 | 423 | def f(): 424 | with cm(): 425 | # Do stuff 426 | 427 | ``ContextDecorator`` lets you instead write:: 428 | 429 | @cm() 430 | def f(): 431 | # Do stuff 432 | 433 | It makes it clear that the ``cm`` applies to the whole function, rather than 434 | just a piece of it (and saving an indentation level is nice, too). 435 | 436 | Existing context managers that already have a base class can be extended by 437 | using ``ContextDecorator`` as a mixin class:: 438 | 439 | from contextlib2 import ContextDecorator 440 | 441 | class mycontext(ContextBaseClass, ContextDecorator): 442 | def __enter__(self): 443 | return self 444 | 445 | def __exit__(self, *exc): 446 | return False 447 | 448 | .. note:: 449 | As the decorated function must be able to be called multiple times, the 450 | underlying context manager must support use in multiple :keyword:`with` 451 | statements. If this is not the case, then the original construct with the 452 | explicit :keyword:`!with` statement inside the function should be used. 453 | 454 | 455 | .. class:: AsyncContextDecorator 456 | 457 | Similar to :class:`ContextDecorator` but only for asynchronous functions. 458 | 459 | Example of ``AsyncContextDecorator``:: 460 | 461 | from asyncio import run 462 | from contextlib2 import AsyncContextDecorator 463 | 464 | class mycontext(AsyncContextDecorator): 465 | async def __aenter__(self): 466 | print('Starting') 467 | return self 468 | 469 | async def __aexit__(self, *exc): 470 | print('Finishing') 471 | return False 472 | 473 | The class can then be used like this:: 474 | 475 | >>> @mycontext() 476 | ... async def function(): 477 | ... print('The bit in the middle') 478 | ... 479 | >>> run(function()) 480 | Starting 481 | The bit in the middle 482 | Finishing 483 | 484 | >>> async def function(): 485 | ... async with mycontext(): 486 | ... print('The bit in the middle') 487 | ... 488 | >>> run(function()) 489 | Starting 490 | The bit in the middle 491 | Finishing 492 | 493 | .. versionadded:: 21.6.0 494 | Part of the standard library in Python 3.10 and later 495 | 496 | 497 | .. class:: ExitStack() 498 | 499 | A context manager that is designed to make it easy to programmatically 500 | combine other context managers and cleanup functions, especially those 501 | that are optional or otherwise driven by input data. 502 | 503 | For example, a set of files may easily be handled in a single with 504 | statement as follows:: 505 | 506 | with ExitStack() as stack: 507 | files = [stack.enter_context(open(fname)) for fname in filenames] 508 | # All opened files will automatically be closed at the end of 509 | # the with statement, even if attempts to open files later 510 | # in the list raise an exception 511 | 512 | The :meth:`~object.__enter__` method returns the :class:`ExitStack` instance, and 513 | performs no additional operations. 514 | 515 | Each instance maintains a stack of registered callbacks that are called in 516 | reverse order when the instance is closed (either explicitly or implicitly 517 | at the end of a :keyword:`with` statement). Note that callbacks are *not* 518 | invoked implicitly when the context stack instance is garbage collected. 519 | 520 | This stack model is used so that context managers that acquire their 521 | resources in their ``__init__`` method (such as file objects) can be 522 | handled correctly. 523 | 524 | Since registered callbacks are invoked in the reverse order of 525 | registration, this ends up behaving as if multiple nested :keyword:`with` 526 | statements had been used with the registered set of callbacks. This even 527 | extends to exception handling - if an inner callback suppresses or replaces 528 | an exception, then outer callbacks will be passed arguments based on that 529 | updated state. 530 | 531 | This is a relatively low level API that takes care of the details of 532 | correctly unwinding the stack of exit callbacks. It provides a suitable 533 | foundation for higher level context managers that manipulate the exit 534 | stack in application specific ways. 535 | 536 | .. versionadded:: 0.4 537 | Part of the standard library in Python 3.3 and later 538 | 539 | .. method:: enter_context(cm) 540 | 541 | Enters a new context manager and adds its :meth:`~object.__exit__` method to 542 | the callback stack. The return value is the result of the context 543 | manager's own :meth:`~object.__enter__` method. 544 | 545 | These context managers may suppress exceptions just as they normally 546 | would if used directly as part of a :keyword:`with` statement. 547 | 548 | .. versionchanged:: 24.6.0 549 | When running on Python 3.11 or later, raises :exc:`TypeError` instead 550 | of :exc:`AttributeError` if *cm* is not a context manager. This aligns 551 | with the behaviour of :keyword:`with` statements in Python 3.11+. 552 | 553 | .. method:: push(exit) 554 | 555 | Adds a context manager's :meth:`~object.__exit__` method to the callback stack. 556 | 557 | As ``__enter__`` is *not* invoked, this method can be used to cover 558 | part of an :meth:`~object.__enter__` implementation with a context manager's own 559 | :meth:`~object.__exit__` method. 560 | 561 | If passed an object that is not a context manager, this method assumes 562 | it is a callback with the same signature as a context manager's 563 | :meth:`~object.__exit__` method and adds it directly to the callback stack. 564 | 565 | By returning true values, these callbacks can suppress exceptions the 566 | same way context manager :meth:`~object.__exit__` methods can. 567 | 568 | The passed in object is returned from the function, allowing this 569 | method to be used as a function decorator. 570 | 571 | .. method:: callback(callback, /, *args, **kwds) 572 | 573 | Accepts an arbitrary callback function and arguments and adds it to 574 | the callback stack. 575 | 576 | Unlike the other methods, callbacks added this way cannot suppress 577 | exceptions (as they are never passed the exception details). 578 | 579 | The passed in callback is returned from the function, allowing this 580 | method to be used as a function decorator. 581 | 582 | .. method:: pop_all() 583 | 584 | Transfers the callback stack to a fresh :class:`ExitStack` instance 585 | and returns it. No callbacks are invoked by this operation - instead, 586 | they will now be invoked when the new stack is closed (either 587 | explicitly or implicitly at the end of a :keyword:`with` statement). 588 | 589 | For example, a group of files can be opened as an "all or nothing" 590 | operation as follows:: 591 | 592 | with ExitStack() as stack: 593 | files = [stack.enter_context(open(fname)) for fname in filenames] 594 | # Hold onto the close method, but don't call it yet. 595 | close_files = stack.pop_all().close 596 | # If opening any file fails, all previously opened files will be 597 | # closed automatically. If all files are opened successfully, 598 | # they will remain open even after the with statement ends. 599 | # close_files() can then be invoked explicitly to close them all. 600 | 601 | .. method:: close() 602 | 603 | Immediately unwinds the callback stack, invoking callbacks in the 604 | reverse order of registration. For any context managers and exit 605 | callbacks registered, the arguments passed in will indicate that no 606 | exception occurred. 607 | 608 | .. class:: AsyncExitStack() 609 | 610 | An :ref:`asynchronous context manager `, similar 611 | to :class:`ExitStack`, that supports combining both synchronous and 612 | asynchronous context managers, as well as having coroutines for 613 | cleanup logic. 614 | 615 | The :meth:`~ExitStack.close` method is not implemented; :meth:`aclose` must be used 616 | instead. 617 | 618 | .. method:: enter_async_context(cm) 619 | :async: 620 | 621 | Similar to :meth:`ExitStack.enter_context` but expects an asynchronous context 622 | manager. 623 | 624 | .. versionchanged:: 24.6.0 625 | When running on Python 3.11 or later, raises :exc:`TypeError` instead 626 | of :exc:`AttributeError` if *cm* is not an asynchronous context manager. 627 | This aligns with the behaviour of ``async with`` statements in Python 3.11+. 628 | 629 | .. method:: push_async_exit(exit) 630 | 631 | Similar to :meth:`ExitStack.push` but expects either an asynchronous context manager 632 | or a coroutine function. 633 | 634 | .. method:: push_async_callback(callback, /, *args, **kwds) 635 | 636 | Similar to :meth:`ExitStack.callback` but expects a coroutine function. 637 | 638 | .. method:: aclose() 639 | :async: 640 | 641 | Similar to :meth:`ExitStack.close` but properly handles awaitables. 642 | 643 | Continuing the example for :func:`asynccontextmanager`:: 644 | 645 | async with AsyncExitStack() as stack: 646 | connections = [await stack.enter_async_context(get_connection()) 647 | for i in range(5)] 648 | # All opened connections will automatically be released at the end of 649 | # the async with statement, even if attempts to open a connection 650 | # later in the list raise an exception. 651 | 652 | .. versionadded:: 21.6.0 653 | Part of the standard library in Python 3.7 and later 654 | 655 | 656 | Examples and Recipes 657 | -------------------- 658 | 659 | This section describes some examples and recipes for making effective use of 660 | the tools provided by :mod:`contextlib2`. 661 | 662 | 663 | Supporting a variable number of context managers 664 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 665 | 666 | The primary use case for :class:`ExitStack` is the one given in the class 667 | documentation: supporting a variable number of context managers and other 668 | cleanup operations in a single :keyword:`with` statement. The variability 669 | may come from the number of context managers needed being driven by user 670 | input (such as opening a user specified collection of files), or from 671 | some of the context managers being optional:: 672 | 673 | with ExitStack() as stack: 674 | for resource in resources: 675 | stack.enter_context(resource) 676 | if need_special_resource(): 677 | special = acquire_special_resource() 678 | stack.callback(release_special_resource, special) 679 | # Perform operations that use the acquired resources 680 | 681 | As shown, :class:`ExitStack` also makes it quite easy to use :keyword:`with` 682 | statements to manage arbitrary resources that don't natively support the 683 | context management protocol. 684 | 685 | 686 | Catching exceptions from ``__enter__`` methods 687 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 688 | 689 | It is occasionally desirable to catch exceptions from an ``__enter__`` 690 | method implementation, *without* inadvertently catching exceptions from 691 | the :keyword:`with` statement body or the context manager's ``__exit__`` 692 | method. By using :class:`ExitStack` the steps in the context management 693 | protocol can be separated slightly in order to allow this:: 694 | 695 | stack = ExitStack() 696 | try: 697 | x = stack.enter_context(cm) 698 | except Exception: 699 | # handle __enter__ exception 700 | else: 701 | with stack: 702 | # Handle normal case 703 | 704 | Actually needing to do this is likely to indicate that the underlying API 705 | should be providing a direct resource management interface for use with 706 | :keyword:`try`/:keyword:`except`/:keyword:`finally` statements, but not 707 | all APIs are well designed in that regard. When a context manager is the 708 | only resource management API provided, then :class:`ExitStack` can make it 709 | easier to handle various situations that can't be handled directly in a 710 | :keyword:`with` statement. 711 | 712 | 713 | Cleaning up in an ``__enter__`` implementation 714 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 715 | 716 | As noted in the documentation of :meth:`ExitStack.push`, this 717 | method can be useful in cleaning up an already allocated resource if later 718 | steps in the :meth:`~object.__enter__` implementation fail. 719 | 720 | Here's an example of doing this for a context manager that accepts resource 721 | acquisition and release functions, along with an optional validation function, 722 | and maps them to the context management protocol:: 723 | 724 | from contextlib2 import contextmanager, AbstractContextManager, ExitStack 725 | 726 | class ResourceManager(AbstractContextManager): 727 | 728 | def __init__(self, acquire_resource, release_resource, check_resource_ok=None): 729 | self.acquire_resource = acquire_resource 730 | self.release_resource = release_resource 731 | if check_resource_ok is None: 732 | def check_resource_ok(resource): 733 | return True 734 | self.check_resource_ok = check_resource_ok 735 | 736 | @contextmanager 737 | def _cleanup_on_error(self): 738 | with ExitStack() as stack: 739 | stack.push(self) 740 | yield 741 | # The validation check passed and didn't raise an exception 742 | # Accordingly, we want to keep the resource, and pass it 743 | # back to our caller 744 | stack.pop_all() 745 | 746 | def __enter__(self): 747 | resource = self.acquire_resource() 748 | with self._cleanup_on_error(): 749 | if not self.check_resource_ok(resource): 750 | msg = "Failed validation for {!r}" 751 | raise RuntimeError(msg.format(resource)) 752 | return resource 753 | 754 | def __exit__(self, *exc_details): 755 | # We don't need to duplicate any of our resource release logic 756 | self.release_resource() 757 | 758 | 759 | Replacing any use of ``try-finally`` and flag variables 760 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 761 | 762 | A pattern you will sometimes see is a ``try-finally`` statement with a flag 763 | variable to indicate whether or not the body of the ``finally`` clause should 764 | be executed. In its simplest form (that can't already be handled just by 765 | using an ``except`` clause instead), it looks something like this:: 766 | 767 | cleanup_needed = True 768 | try: 769 | result = perform_operation() 770 | if result: 771 | cleanup_needed = False 772 | finally: 773 | if cleanup_needed: 774 | cleanup_resources() 775 | 776 | As with any ``try`` statement based code, this can cause problems for 777 | development and review, because the setup code and the cleanup code can end 778 | up being separated by arbitrarily long sections of code. 779 | 780 | :class:`ExitStack` makes it possible to instead register a callback for 781 | execution at the end of a ``with`` statement, and then later decide to skip 782 | executing that callback:: 783 | 784 | from contextlib2 import ExitStack 785 | 786 | with ExitStack() as stack: 787 | stack.callback(cleanup_resources) 788 | result = perform_operation() 789 | if result: 790 | stack.pop_all() 791 | 792 | This allows the intended cleanup up behaviour to be made explicit up front, 793 | rather than requiring a separate flag variable. 794 | 795 | If a particular application uses this pattern a lot, it can be simplified 796 | even further by means of a small helper class:: 797 | 798 | from contextlib2 import ExitStack 799 | 800 | class Callback(ExitStack): 801 | def __init__(self, callback, /, *args, **kwds): 802 | super().__init__() 803 | self.callback(callback, *args, **kwds) 804 | 805 | def cancel(self): 806 | self.pop_all() 807 | 808 | with Callback(cleanup_resources) as cb: 809 | result = perform_operation() 810 | if result: 811 | cb.cancel() 812 | 813 | If the resource cleanup isn't already neatly bundled into a standalone 814 | function, then it is still possible to use the decorator form of 815 | :meth:`ExitStack.callback` to declare the resource cleanup in 816 | advance:: 817 | 818 | from contextlib2 import ExitStack 819 | 820 | with ExitStack() as stack: 821 | @stack.callback 822 | def cleanup_resources(): 823 | ... 824 | result = perform_operation() 825 | if result: 826 | stack.pop_all() 827 | 828 | Due to the way the decorator protocol works, a callback function 829 | declared this way cannot take any parameters. Instead, any resources to 830 | be released must be accessed as closure variables. 831 | 832 | 833 | Using a context manager as a function decorator 834 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 835 | 836 | :class:`ContextDecorator` makes it possible to use a context manager in 837 | both an ordinary ``with`` statement and also as a function decorator. 838 | 839 | For example, it is sometimes useful to wrap functions or groups of statements 840 | with a logger that can track the time of entry and time of exit. Rather than 841 | writing both a function decorator and a context manager for the task, 842 | inheriting from :class:`ContextDecorator` provides both capabilities in a 843 | single definition:: 844 | 845 | from contextlib2 import ContextDecorator 846 | import logging 847 | 848 | logging.basicConfig(level=logging.INFO) 849 | 850 | class track_entry_and_exit(ContextDecorator): 851 | def __init__(self, name): 852 | self.name = name 853 | 854 | def __enter__(self): 855 | logging.info('Entering: %s', self.name) 856 | 857 | def __exit__(self, exc_type, exc, exc_tb): 858 | logging.info('Exiting: %s', self.name) 859 | 860 | Instances of this class can be used as both a context manager:: 861 | 862 | with track_entry_and_exit('widget loader'): 863 | print('Some time consuming activity goes here') 864 | load_widget() 865 | 866 | And also as a function decorator:: 867 | 868 | @track_entry_and_exit('widget loader') 869 | def activity(): 870 | print('Some time consuming activity goes here') 871 | load_widget() 872 | 873 | Note that there is one additional limitation when using context managers 874 | as function decorators: there's no way to access the return value of 875 | :meth:`~object.__enter__`. If that value is needed, then it is still necessary to use 876 | an explicit ``with`` statement. 877 | 878 | .. seealso:: 879 | 880 | :pep:`343` - The "with" statement 881 | The specification, background, and examples for the Python :keyword:`with` 882 | statement. 883 | 884 | .. _single-use-reusable-and-reentrant-cms: 885 | 886 | Single use, reusable and reentrant context managers 887 | --------------------------------------------------- 888 | 889 | Most context managers are written in a way that means they can only be 890 | used effectively in a :keyword:`with` statement once. These single use 891 | context managers must be created afresh each time they're used - 892 | attempting to use them a second time will trigger an exception or 893 | otherwise not work correctly. 894 | 895 | This common limitation means that it is generally advisable to create 896 | context managers directly in the header of the :keyword:`with` statement 897 | where they are used (as shown in all of the usage examples above). 898 | 899 | Files are an example of effectively single use context managers, since 900 | the first :keyword:`with` statement will close the file, preventing any 901 | further IO operations using that file object. 902 | 903 | Context managers created using :func:`contextmanager` are also single use 904 | context managers, and will complain about the underlying generator failing 905 | to yield if an attempt is made to use them a second time:: 906 | 907 | >>> from contextlib2 import contextmanager 908 | >>> @contextmanager 909 | ... def singleuse(): 910 | ... print("Before") 911 | ... yield 912 | ... print("After") 913 | ... 914 | >>> cm = singleuse() 915 | >>> with cm: 916 | ... pass 917 | ... 918 | Before 919 | After 920 | >>> with cm: 921 | ... pass 922 | ... 923 | Traceback (most recent call last): 924 | ... 925 | RuntimeError: generator didn't yield 926 | 927 | 928 | .. _reentrant-cms: 929 | 930 | Reentrant context managers 931 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 932 | 933 | More sophisticated context managers may be "reentrant". These context 934 | managers can not only be used in multiple :keyword:`with` statements, 935 | but may also be used *inside* a :keyword:`!with` statement that is already 936 | using the same context manager. 937 | 938 | :class:`threading.RLock` is an example of a reentrant context manager, as are 939 | :func:`suppress`, :func:`redirect_stdout`, and :func:`chdir`. Here's a very 940 | simple example of reentrant use:: 941 | 942 | >>> from contextlib2 import redirect_stdout 943 | >>> from io import StringIO 944 | >>> stream = StringIO() 945 | >>> write_to_stream = redirect_stdout(stream) 946 | >>> with write_to_stream: 947 | ... print("This is written to the stream rather than stdout") 948 | ... with write_to_stream: 949 | ... print("This is also written to the stream") 950 | ... 951 | >>> print("This is written directly to stdout") 952 | This is written directly to stdout 953 | >>> print(stream.getvalue()) 954 | This is written to the stream rather than stdout 955 | This is also written to the stream 956 | 957 | Real world examples of reentrancy are more likely to involve multiple 958 | functions calling each other and hence be far more complicated than this 959 | example. 960 | 961 | Note also that being reentrant is *not* the same thing as being thread safe. 962 | :func:`redirect_stdout`, for example, is definitely not thread safe, as it 963 | makes a global modification to the system state by binding :data:`sys.stdout` 964 | to a different stream. 965 | 966 | 967 | .. _reusable-cms: 968 | 969 | Reusable context managers 970 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 971 | 972 | Distinct from both single use and reentrant context managers are "reusable" 973 | context managers (or, to be completely explicit, "reusable, but not 974 | reentrant" context managers, since reentrant context managers are also 975 | reusable). These context managers support being used multiple times, but 976 | will fail (or otherwise not work correctly) if the specific context manager 977 | instance has already been used in a containing with statement. 978 | 979 | :class:`threading.Lock` is an example of a reusable, but not reentrant, 980 | context manager (for a reentrant lock, it is necessary to use 981 | :class:`threading.RLock` instead). 982 | 983 | Another example of a reusable, but not reentrant, context manager is 984 | :class:`ExitStack`, as it invokes *all* currently registered callbacks 985 | when leaving any with statement, regardless of where those callbacks 986 | were added:: 987 | 988 | >>> from contextlib2 import ExitStack 989 | >>> stack = ExitStack() 990 | >>> with stack: 991 | ... stack.callback(print, "Callback: from first context") 992 | ... print("Leaving first context") 993 | ... 994 | Leaving first context 995 | Callback: from first context 996 | >>> with stack: 997 | ... stack.callback(print, "Callback: from second context") 998 | ... print("Leaving second context") 999 | ... 1000 | Leaving second context 1001 | Callback: from second context 1002 | >>> with stack: 1003 | ... stack.callback(print, "Callback: from outer context") 1004 | ... with stack: 1005 | ... stack.callback(print, "Callback: from inner context") 1006 | ... print("Leaving inner context") 1007 | ... print("Leaving outer context") 1008 | ... 1009 | Leaving inner context 1010 | Callback: from inner context 1011 | Callback: from outer context 1012 | Leaving outer context 1013 | 1014 | As the output from the example shows, reusing a single stack object across 1015 | multiple with statements works correctly, but attempting to nest them 1016 | will cause the stack to be cleared at the end of the innermost with 1017 | statement, which is unlikely to be desirable behaviour. 1018 | 1019 | Using separate :class:`ExitStack` instances instead of reusing a single 1020 | instance avoids that problem:: 1021 | 1022 | >>> from contextlib2 import ExitStack 1023 | >>> with ExitStack() as outer_stack: 1024 | ... outer_stack.callback(print, "Callback: from outer context") 1025 | ... with ExitStack() as inner_stack: 1026 | ... inner_stack.callback(print, "Callback: from inner context") 1027 | ... print("Leaving inner context") 1028 | ... print("Leaving outer context") 1029 | ... 1030 | Leaving inner context 1031 | Callback: from inner context 1032 | Leaving outer context 1033 | Callback: from outer context 1034 | --------------------------------------------------------------------------------