Import Upstream version 1.2.0

This commit is contained in:
luoyaoming 2024-05-07 15:01:27 +08:00
parent 41e98eced5
commit 184cb2dc60
32 changed files with 630 additions and 281 deletions

54
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Bug Report
description: File a bug report
labels: ["bug"]
body:
- type: markdown
attributes:
value: >
If you observed a crash in the library, or saw unexpected behavior in it, report
your findings here.
- type: checkboxes
attributes:
label: Things to check first
options:
- label: >
I have searched the existing issues and didn't find my bug already reported
there
required: true
- label: >
I have checked that my bug is still present in the latest release
required: true
- type: input
id: exceptiongroup-version
attributes:
label: Exceptiongroup version
description: What version of exceptiongroup were you running?
validations:
required: true
- type: input
id: python-version
attributes:
label: Python version
description: What version of Python were you running?
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: >
Unless you are reporting a crash, tell us what you expected to happen instead.
validations:
required: true
- type: textarea
id: mwe
attributes:
label: How can we reproduce the bug?
description: >
In order to investigate the bug, we need to be able to reproduce it on our own.
Please create a
[minimum workable example](https://stackoverflow.com/help/minimal-reproducible-example)
that demonstrates the problem. List any third party libraries required for this,
but avoid using them unless absolutely necessary.
validations:
required: true

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,35 @@
name: Feature request
description: Suggest a new feature
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: >
If you have thought of a new feature that would increase the usefulness of this
project, please use this form to send us your idea.
- type: checkboxes
attributes:
label: Things to check first
options:
- label: >
I have searched the existing issues and didn't find my feature already
requested there
required: true
- type: textarea
id: feature
attributes:
label: Feature description
description: >
Describe the feature in detail. The more specific the description you can give,
the easier it should be to implement this feature.
validations:
required: true
- type: textarea
id: usecase
attributes:
label: Use case
description: >
Explain why you need this feature, and why you think it would be useful to
others too.
validations:
required: true

View File

@ -9,10 +9,12 @@ on:
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
jobs: jobs:
publish: build:
name: Build the source tarball and the wheel
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: release
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -20,8 +22,38 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pip install build run: pip install build
- name: Create packages - name: Create packages
run: python -m build -s -w . run: python -m build
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: dist
path: dist
publish:
name: Publish build artifacts to the PyPI
needs: build
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- name: Retrieve packages
uses: actions/download-artifact@v3
- name: Upload packages - name: Upload packages
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
release:
name: Create a GitHub release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- id: changelog
uses: agronholm/release-notes@v1
with: with:
password: ${{ secrets.pypi_token }} path: CHANGES.rst
- uses: ncipollo/release-action@v1
with:
body: ${{ steps.changelog.outputs.changelog }}

View File

@ -9,7 +9,7 @@ jobs:
pyright: pyright:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
@ -27,38 +27,32 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy-3.8] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", pypy-3.10]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: actions/cache@v3 allow-prereleases: true
with: cache: pip
path: ~/.cache/pip cache-dependency-path: pyproject.toml
key: pip-test-${{ matrix.python-version }}-${{ matrix.os }}
- name: Install dependencies - name: Install dependencies
run: pip install .[test] coveralls coverage[toml] run: pip install -e .[test] coverage
- name: Test with pytest - name: Test with pytest
run: coverage run -m pytest run: coverage run -m pytest
- name: Upload Coverage - name: Upload Coverage
run: coveralls --service=github uses: coverallsapp/github-action@v2
env: with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} parallel: true
COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
COVERALLS_PARALLEL: true
coveralls: coveralls:
name: Finish Coveralls name: Finish Coveralls
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: python:3-slim
steps: steps:
- name: Finished - name: Finished
run: | uses: coverallsapp/github-action@v2
pip install coveralls with:
coveralls --service=github --finish parallel-finished: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ __pycache__
.coverage .coverage
.pytest_cache/ .pytest_cache/
.mypy_cache/ .mypy_cache/
.ruff_cache/
.eggs/ .eggs/
.tox .tox
.idea .idea

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-added-large-files - id: check-added-large-files
- id: check-case-conflict - id: check-case-conflict
@ -15,37 +15,10 @@ repos:
args: ["--fix=lf"] args: ["--fix=lf"]
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pycqa/isort - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v5.11.3 rev: v0.1.6
hooks: hooks:
- id: isort - id: ruff
args: [--fix, --show-fixes]
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
hooks:
- id: pyupgrade
args: ["--py37-plus", "--keep-runtime-typing"]
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
exclude: "tests/test_catch_py311.py" exclude: "tests/test_catch_py311.py"
- id: ruff-format
- repo: https://github.com/csachs/pyproject-flake8
rev: v6.0.0.post1
hooks:
- id: pyproject-flake8
additional_dependencies: [flake8-bugbear]
exclude: "tests/test_catch_py311.py"
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
hooks:
- id: python-check-blanket-noqa
- id: python-check-blanket-type-ignore
- id: python-no-eval
- id: python-use-type-annotations
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal

View File

@ -3,6 +3,42 @@ Version history
This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_. This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
**1.2.0**
- Added special monkeypatching if `Apport <https://github.com/canonical/apport>`_ has
overridden ``sys.excepthook`` so it will format exception groups correctly
(PR by John Litborn)
- Added a backport of ``contextlib.suppress()`` from Python 3.12.1 which also handles
suppressing exceptions inside exception groups
- Fixed bare ``raise`` in a handler reraising the original naked exception rather than
an exception group which is what is raised when you do a ``raise`` in an ``except*``
handler
**1.1.3**
- ``catch()`` now raises a ``TypeError`` if passed an async exception handler instead of
just giving a ``RuntimeWarning`` about the coroutine never being awaited. (#66, PR by
John Litborn)
- Fixed plain ``raise`` statement in an exception handler callback to work like a
``raise`` in an ``except*`` block
- Fixed new exception group not being chained to the original exception when raising an
exception group from exceptions raised in handler callbacks
- Fixed type annotations of the ``derive()``, ``subgroup()`` and ``split()`` methods to
match the ones in typeshed
**1.1.2**
- Changed handling of exceptions in exception group handler callbacks to not wrap a
single exception in an exception group, as per
`CPython issue 103590 <https://github.com/python/cpython/issues/103590>`_
**1.1.1**
- Worked around
`CPython issue #98778 <https://github.com/python/cpython/issues/98778>`_,
``urllib.error.HTTPError(..., fp=None)`` raises ``KeyError`` on unknown attribute
access, on affected Python versions. (PR by Zac Hatfield-Dodds)
**1.1.0** **1.1.0**
- Backported upstream fix for gh-99553 (custom subclasses of ``BaseExceptionGroup`` that - Backported upstream fix for gh-99553 (custom subclasses of ``BaseExceptionGroup`` that

View File

@ -26,6 +26,8 @@ It contains the following:
* ``traceback.format_exception_only()`` * ``traceback.format_exception_only()``
* ``traceback.print_exception()`` * ``traceback.print_exception()``
* ``traceback.print_exc()`` * ``traceback.print_exc()``
* A backported version of ``contextlib.suppress()`` from Python 3.12.1 which also
handles suppressing exceptions inside exception groups
If this package is imported on Python 3.11 or later, the built-in implementations of the If this package is imported on Python 3.11 or later, the built-in implementations of the
exception group classes are used instead, ``TracebackException`` is not monkey patched exception group classes are used instead, ``TracebackException`` is not monkey patched
@ -84,6 +86,18 @@ would be written with this backport like this:
**NOTE**: Just like with ``except*``, you cannot handle ``BaseExceptionGroup`` or **NOTE**: Just like with ``except*``, you cannot handle ``BaseExceptionGroup`` or
``ExceptionGroup`` with ``catch()``. ``ExceptionGroup`` with ``catch()``.
Suppressing exceptions
======================
This library contains a backport of the ``contextlib.suppress()`` context manager from
Python 3.12.1. It allows you to selectively ignore certain exceptions, even when they're
inside exception groups::
from exceptiongroup import suppress
with suppress(RuntimeError):
raise ExceptionGroup("", [RuntimeError("boo")])
Notes on monkey patching Notes on monkey patching
======================== ========================

5
debian/changelog vendored
View File

@ -1,5 +0,0 @@
python-exceptiongroup (1.1.0-ok1) yangtze; urgency=medium
* Build for openkylin.
-- sufang <sufang@kylinos.cn> Tue, 07 Feb 2023 11:24:32 +0800

1
debian/clean vendored
View File

@ -1 +0,0 @@
src/exceptiongroup/_version.py

29
debian/control vendored
View File

@ -1,29 +0,0 @@
Source: python-exceptiongroup
Section: python
Priority: optional
Maintainer: OpenKylin Developers <packaging@lists.openkylin.top>
Build-Depends:
debhelper-compat (= 13),
dh-python,
dh-sequence-python3,
pybuild-plugin-pyproject,
python3-all,
python3-flit-scm,
python3-pytest <!nocheck>,
Standards-Version: 4.6.1
Testsuite: autopkgtest-pkg-pybuild
Vcs-Git: https://gitee.com/openkylin/python-exceptiongroup.git
Vcs-Browser: https://gitee.com/openkylin/python-exceptiongroup
Homepage: https://github.com/agronholm/exceptiongroup/
Rules-Requires-Root: no
Package: python3-exceptiongroup
Architecture: all
Depends:
${misc:Depends},
${python3:Depends},
Breaks:
python3-trio (<< 0.22),
Description: Backport of PEP 654 (exception groups)
This is a backport of the BaseExceptionGroup and ExceptionGroup classes from
Python 3.11.

106
debian/copyright vendored
View File

@ -1,106 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: python-exceptiongroup
Source: <url://example.com>
#
# Please double check copyright with the licensecheck(1) command.
Files: .github/workflows/publish.yml
.github/workflows/test.yml
.gitignore
.pre-commit-config.yaml
CHANGES.rst
README.rst
pyproject.toml
src/exceptiongroup/__init__.py
src/exceptiongroup/_catch.py
src/exceptiongroup/_exceptions.py
src/exceptiongroup/_formatting.py
src/exceptiongroup/py.typed
tests/__init__.py
tests/conftest.py
tests/test_catch.py
tests/test_catch_py311.py
tests/test_exceptions.py
tests/test_formatting.py
Copyright: __NO_COPYRIGHT_NOR_LICENSE__
License: __NO_COPYRIGHT_NOR_LICENSE__
#----------------------------------------------------------------------------
# Files marked as NO_LICENSE_TEXT_FOUND may be covered by the following
# license/copyright files.
#----------------------------------------------------------------------------
# License file: LICENSE
The MIT License (MIT)
.
Copyright (c) 2022 Alex Grönholm
.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
.
.
This project contains code copied from the Python standard library.
The following is the required license notice for those parts.
.
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
.
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.

View File

@ -1 +0,0 @@
# You must remove unused comment lines for the released package.

View File

@ -1 +0,0 @@
README.rst

10
debian/rules vendored
View File

@ -1,10 +0,0 @@
#!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
export PYBUILD_NAME=exceptiongroup
export SETUPTOOLS_SCM_PRETEND_VERSION = $(DEB_VERSION_UPSTREAM)
%:
dh $@ --buildsystem=pybuild

View File

@ -1 +0,0 @@
3.0 (native)

View File

@ -1,4 +0,0 @@
Bug-Database: https://github.com/agronholm/exceptiongroup//issues
Bug-Submit: https://github.com/agronholm/exceptiongroup//issues/new
Repository: https://github.com/agronholm/exceptiongroup/.git
Repository-Browse: https://github.com/agronholm/exceptiongroup/

3
debian/watch vendored
View File

@ -1,3 +0,0 @@
version=4
opts="pgpmode=none" \
https://github.com/agronholm/exceptiongroup/tags (?:.*?/)?v?(\d[\d.]*)\.tar\.gz

View File

@ -30,6 +30,9 @@ test = [
] ]
[tool.flit.sdist] [tool.flit.sdist]
include = [
"tests",
]
exclude = [ exclude = [
".github/*", ".github/*",
".gitignore", ".gitignore",
@ -41,16 +44,22 @@ version_scheme = "post-release"
local_scheme = "dirty-tag" local_scheme = "dirty-tag"
write_to = "src/exceptiongroup/_version.py" write_to = "src/exceptiongroup/_version.py"
[tool.black] [tool.ruff]
target-version = ['py37'] select = [
"E", "F", "W", # default flake-8
"I", # isort
"ISC", # flake8-implicit-str-concat
"PGH", # pygrep-hooks
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
]
[tool.isort] [tool.ruff.pyupgrade]
src_paths = ["src"] # Preserve types, even if a file imports `from __future__ import annotations`.
skip_gitignore = true keep-runtime-typing = true
profile = "black"
[tool.flake8] [tool.ruff.isort]
max-line-length = 88 known-first-party = ["exceptiongroup"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-rsx --tb=short --strict-config --strict-markers" addopts = "-rsx --tb=short --strict-config --strict-markers"
@ -69,13 +78,14 @@ exclude_lines = [
[tool.tox] [tool.tox]
legacy_tox_ini = """ legacy_tox_ini = """
[tox] [tox]
envlist = py37, py38, py39, py310, py311, pypy3 envlist = py37, py38, py39, py310, py311, py312, pypy3
skip_missing_interpreters = true skip_missing_interpreters = true
minversion = 4.0 minversion = 4.0
[testenv] [testenv]
extras = test extras = test
commands = python -m pytest {posargs} commands = python -m pytest {posargs}
usedevelop = true
[testenv:pyright] [testenv:pyright]
deps = pyright deps = pyright

View File

@ -6,6 +6,7 @@ __all__ = [
"format_exception_only", "format_exception_only",
"print_exception", "print_exception",
"print_exc", "print_exc",
"suppress",
] ]
import os import os
@ -38,3 +39,8 @@ else:
BaseExceptionGroup = BaseExceptionGroup BaseExceptionGroup = BaseExceptionGroup
ExceptionGroup = ExceptionGroup ExceptionGroup = ExceptionGroup
if sys.version_info < (3, 12, 1):
from ._suppress import suppress
else:
from contextlib import suppress

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import inspect
import sys import sys
from collections.abc import Callable, Iterable, Mapping from collections.abc import Callable, Iterable, Mapping
from contextlib import AbstractContextManager from contextlib import AbstractContextManager
@ -33,11 +34,20 @@ class _Catcher:
elif unhandled is None: elif unhandled is None:
return True return True
else: else:
raise unhandled from None if isinstance(exc, BaseExceptionGroup):
try:
raise unhandled from exc.__cause__
except BaseExceptionGroup:
# Change __context__ to __cause__ because Python 3.11 does this
# too
unhandled.__context__ = exc.__cause__
raise
raise unhandled from exc
return False return False
def handle_exception(self, exc: BaseException) -> BaseExceptionGroup | None: def handle_exception(self, exc: BaseException) -> BaseException | None:
excgroup: BaseExceptionGroup | None excgroup: BaseExceptionGroup | None
if isinstance(exc, BaseExceptionGroup): if isinstance(exc, BaseExceptionGroup):
excgroup = exc excgroup = exc
@ -49,16 +59,30 @@ class _Catcher:
matched, excgroup = excgroup.split(exc_types) matched, excgroup = excgroup.split(exc_types)
if matched: if matched:
try: try:
handler(matched) try:
raise matched
except BaseExceptionGroup:
result = handler(matched)
except BaseExceptionGroup as new_exc:
if new_exc is matched:
new_exceptions.append(new_exc)
else:
new_exceptions.extend(new_exc.exceptions)
except BaseException as new_exc: except BaseException as new_exc:
new_exceptions.append(new_exc) new_exceptions.append(new_exc)
else:
if inspect.iscoroutine(result):
raise TypeError(
f"Error trying to handle {matched!r} with {handler!r}. "
"Exception handler must be a sync function."
) from exc
if not excgroup: if not excgroup:
break break
if new_exceptions: if new_exceptions:
if excgroup: if len(new_exceptions) == 1:
new_exceptions.append(excgroup) return new_exceptions[0]
return BaseExceptionGroup("", new_exceptions) return BaseExceptionGroup("", new_exceptions)
elif ( elif (

View File

@ -27,7 +27,7 @@ def check_direct_subclass(
def get_condition_filter( def get_condition_filter(
condition: type[_BaseExceptionT] condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...] | tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool] | Callable[[_BaseExceptionT_co], bool],
) -> Callable[[_BaseExceptionT_co], bool]: ) -> Callable[[_BaseExceptionT_co], bool]:
if isclass(condition) and issubclass( if isclass(condition) and issubclass(
cast(Type[BaseException], condition), BaseException cast(Type[BaseException], condition), BaseException
@ -60,7 +60,7 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
for i, exc in enumerate(__exceptions): for i, exc in enumerate(__exceptions):
if not isinstance(exc, BaseException): if not isinstance(exc, BaseException):
raise ValueError( raise ValueError(
f"Item {i} of second argument (exceptions) is not an " f"exception" f"Item {i} of second argument (exceptions) is not an exception"
) )
if cls is BaseExceptionGroup: if cls is BaseExceptionGroup:
@ -105,6 +105,12 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]: ) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]:
return tuple(self._exceptions) return tuple(self._exceptions)
@overload
def subgroup(
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> ExceptionGroup[_ExceptionT] | None:
...
@overload @overload
def subgroup( def subgroup(
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...] self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
@ -113,16 +119,16 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
@overload @overload
def subgroup( def subgroup(
self: Self, __condition: Callable[[_BaseExceptionT_co], bool] self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> Self | None: ) -> BaseExceptionGroup[_BaseExceptionT_co] | None:
... ...
def subgroup( def subgroup(
self: Self, self,
__condition: type[_BaseExceptionT] __condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...] | tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool], | Callable[[_BaseExceptionT_co | Self], bool],
) -> BaseExceptionGroup[_BaseExceptionT] | Self | None: ) -> BaseExceptionGroup[_BaseExceptionT] | None:
condition = get_condition_filter(__condition) condition = get_condition_filter(__condition)
modified = False modified = False
if condition(self): if condition(self):
@ -155,25 +161,50 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
@overload @overload
def split( def split(
self: Self, self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
__condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...], ) -> tuple[
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]: ExceptionGroup[_ExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
... ...
@overload @overload
def split( def split(
self: Self, __condition: Callable[[_BaseExceptionT_co], bool] self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
) -> tuple[Self | None, Self | None]: ) -> tuple[
BaseExceptionGroup[_BaseExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
...
@overload
def split(
self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> tuple[
BaseExceptionGroup[_BaseExceptionT_co] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
... ...
def split( def split(
self: Self, self,
__condition: type[_BaseExceptionT] __condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...] | tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool], | Callable[[_BaseExceptionT_co], bool],
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None] | tuple[ ) -> (
Self | None, Self | None tuple[
]: ExceptionGroup[_ExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]
| tuple[
BaseExceptionGroup[_BaseExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]
| tuple[
BaseExceptionGroup[_BaseExceptionT_co] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]
):
condition = get_condition_filter(__condition) condition = get_condition_filter(__condition)
if condition(self): if condition(self):
return self, None return self, None
@ -209,7 +240,19 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
return matching_group, nonmatching_group return matching_group, nonmatching_group
def derive(self: Self, __excs: Sequence[_BaseExceptionT_co]) -> Self: @overload
def derive(self, __excs: Sequence[_ExceptionT]) -> ExceptionGroup[_ExceptionT]:
...
@overload
def derive(
self, __excs: Sequence[_BaseExceptionT]
) -> BaseExceptionGroup[_BaseExceptionT]:
...
def derive(
self, __excs: Sequence[_BaseExceptionT]
) -> BaseExceptionGroup[_BaseExceptionT]:
eg = BaseExceptionGroup(self.message, __excs) eg = BaseExceptionGroup(self.message, __excs)
if hasattr(self, "__notes__"): if hasattr(self, "__notes__"):
# Create a new list so that add_note() only affects one exceptiongroup # Create a new list so that add_note() only affects one exceptiongroup
@ -245,28 +288,32 @@ class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
@overload @overload
def subgroup( def subgroup(
self: Self, __condition: Callable[[_ExceptionT_co], bool] self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> Self | None: ) -> ExceptionGroup[_ExceptionT_co] | None:
... ...
def subgroup( def subgroup(
self: Self, self,
__condition: type[_ExceptionT] __condition: type[_ExceptionT]
| tuple[type[_ExceptionT], ...] | tuple[type[_ExceptionT], ...]
| Callable[[_ExceptionT_co], bool], | Callable[[_ExceptionT_co], bool],
) -> ExceptionGroup[_ExceptionT] | Self | None: ) -> ExceptionGroup[_ExceptionT] | None:
return super().subgroup(__condition) return super().subgroup(__condition)
@overload # type: ignore[override] @overload
def split( def split(
self: Self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...] self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None]: ) -> tuple[
ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None
]:
... ...
@overload @overload
def split( def split(
self: Self, __condition: Callable[[_ExceptionT_co], bool] self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> tuple[Self | None, Self | None]: ) -> tuple[
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
]:
... ...
def split( def split(
@ -274,7 +321,7 @@ class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
__condition: type[_ExceptionT] __condition: type[_ExceptionT]
| tuple[type[_ExceptionT], ...] | tuple[type[_ExceptionT], ...]
| Callable[[_ExceptionT_co], bool], | Callable[[_ExceptionT_co], bool],
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None] | tuple[ ) -> tuple[
Self | None, Self | None ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
]: ]:
return super().split(__condition) return super().split(__condition)

View File

@ -103,7 +103,16 @@ class PatchedTracebackException(traceback.TracebackException):
# Capture now to permit freeing resources: only complication is in the # Capture now to permit freeing resources: only complication is in the
# unofficial API _format_final_exc_line # unofficial API _format_final_exc_line
self._str = _safe_string(exc_value, "exception") self._str = _safe_string(exc_value, "exception")
try:
self.__notes__ = getattr(exc_value, "__notes__", None) self.__notes__ = getattr(exc_value, "__notes__", None)
except KeyError:
# Workaround for https://github.com/python/cpython/issues/98778 on Python
# <= 3.9, and some 3.10 and 3.11 patch versions.
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
if sys.version_info[:2] <= (3, 11) and isinstance(exc_value, HTTPError):
self.__notes__ = None
else:
raise
if exc_type and issubclass(exc_type, SyntaxError): if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially # Handle SyntaxError's specially
@ -350,6 +359,46 @@ if sys.excepthook is sys.__excepthook__:
) )
sys.excepthook = exceptiongroup_excepthook sys.excepthook = exceptiongroup_excepthook
# Ubuntu's system Python has a sitecustomize.py file that imports
# apport_python_hook and replaces sys.excepthook.
#
# The custom hook captures the error for crash reporting, and then calls
# sys.__excepthook__ to actually print the error.
#
# We don't mind it capturing the error for crash reporting, but we want to
# take over printing the error. So we monkeypatch the apport_python_hook
# module so that instead of calling sys.__excepthook__, it calls our custom
# hook.
#
# More details: https://github.com/python-trio/trio/issues/1065
if getattr(sys.excepthook, "__name__", None) in (
"apport_excepthook",
# on ubuntu 22.10 the hook was renamed to partial_apport_excepthook
"partial_apport_excepthook",
):
# patch traceback like above
traceback.TracebackException.__init__ = ( # type: ignore[assignment]
PatchedTracebackException.__init__
)
traceback.TracebackException.format = ( # type: ignore[assignment]
PatchedTracebackException.format
)
traceback.TracebackException.format_exception_only = ( # type: ignore[assignment]
PatchedTracebackException.format_exception_only
)
from types import ModuleType
import apport_python_hook
assert sys.excepthook is apport_python_hook.apport_excepthook
# monkeypatch the sys module that apport has imported
fake_sys = ModuleType("exceptiongroup_fake_sys")
fake_sys.__dict__.update(sys.__dict__)
fake_sys.__excepthook__ = exceptiongroup_excepthook
apport_python_hook.sys = fake_sys
@singledispatch @singledispatch
def format_exception_only(__exc: BaseException) -> List[str]: def format_exception_only(__exc: BaseException) -> List[str]:

View File

@ -0,0 +1,40 @@
import sys
from contextlib import AbstractContextManager
if sys.version_info < (3, 11):
from ._exceptions import BaseExceptionGroup
class suppress(AbstractContextManager):
"""Backport of :class:`contextlib.suppress` from Python 3.12.1."""
def __init__(self, *exceptions):
self._exceptions = exceptions
def __enter__(self):
pass
def __exit__(self, exctype, excinst, exctb):
# Unlike isinstance and issubclass, CPython exception handling
# currently only looks at the concrete type hierarchy (ignoring
# the instance and subclass checking hooks). While Guido considers
# that a bug rather than a feature, it's a fairly hard one to fix
# due to various internal implementation details. suppress provides
# the simpler issubclass based semantics, rather than trying to
# exactly reproduce the limitations of the CPython interpreter.
#
# See http://bugs.python.org/issue12029 for more details
if exctype is None:
return
if issubclass(exctype, self._exceptions):
return True
if issubclass(exctype, BaseExceptionGroup):
match, rest = excinst.split(self._exceptions)
if rest is None:
return True
raise rest
return False

View File

@ -0,0 +1,13 @@
# The apport_python_hook package is only installed as part of Ubuntu's system
# python, and not available in venvs. So before we can import it we have to
# make sure it's on sys.path.
import sys
sys.path.append("/usr/lib/python3/dist-packages")
import apport_python_hook # noqa: E402 # unsorted import
apport_python_hook.install()
from exceptiongroup import ExceptionGroup # noqa: E402 # unsorted import
raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])

View File

@ -0,0 +1,63 @@
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
import pytest
import exceptiongroup
def run_script(name: str) -> subprocess.CompletedProcess[bytes]:
exceptiongroup_path = Path(exceptiongroup.__file__).parent.parent
script_path = Path(__file__).parent / name
env = dict(os.environ)
print("parent PYTHONPATH:", env.get("PYTHONPATH"))
if "PYTHONPATH" in env: # pragma: no cover
pp = env["PYTHONPATH"].split(os.pathsep)
else:
pp = []
pp.insert(0, str(exceptiongroup_path))
pp.insert(0, str(script_path.parent))
env["PYTHONPATH"] = os.pathsep.join(pp)
print("subprocess PYTHONPATH:", env.get("PYTHONPATH"))
cmd = [sys.executable, "-u", str(script_path)]
print("running:", cmd)
completed = subprocess.run(
cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
print("process output:")
print(completed.stdout.decode("utf-8"))
return completed
@pytest.mark.skipif(
sys.version_info > (3, 11),
reason="No patching is done on Python >= 3.11",
)
@pytest.mark.skipif(
not Path("/usr/lib/python3/dist-packages/apport_python_hook.py").exists(),
reason="need Ubuntu with python3-apport installed",
)
def test_apport_excepthook_monkeypatch_interaction():
completed = run_script("apport_excepthook.py")
stdout = completed.stdout.decode("utf-8")
file = Path(__file__).parent / "apport_excepthook.py"
assert stdout == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{file}", line 13, in <module>
| raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])
| exceptiongroup.ExceptionGroup: msg1 (2 sub-exceptions)
+-+---------------- 1 ----------------
| KeyError: 'msg2'
+---------------- 2 ----------------
| ValueError: msg3
+------------------------------------
"""
)

View File

@ -148,15 +148,41 @@ def test_catch_handler_raises():
def handler(exc): def handler(exc):
raise RuntimeError("new") raise RuntimeError("new")
try: with pytest.raises(RuntimeError, match="new") as exc:
with catch({(ValueError, ValueError): handler}): with catch({(ValueError, ValueError): handler}):
raise ExceptionGroup("booboo", [ValueError("bar")]) excgrp = ExceptionGroup("booboo", [ValueError("bar")])
except ExceptionGroup as exc: raise excgrp
assert exc.message == ""
assert len(exc.exceptions) == 1 context = exc.value.__context__
assert isinstance(exc.exceptions[0], RuntimeError) assert isinstance(context, ExceptionGroup)
else: assert str(context) == "booboo (1 sub-exception)"
pytest.fail("Did not raise an ExceptionGroup") assert len(context.exceptions) == 1
assert isinstance(context.exceptions[0], ValueError)
assert exc.value.__cause__ is None
def test_bare_raise_in_handler():
"""Test that a bare "raise" "middle" ecxeption group gets discarded."""
def handler(exc):
raise
with pytest.raises(ExceptionGroup) as excgrp:
with catch({(ValueError,): handler, (RuntimeError,): lambda eg: None}):
try:
first_exc = RuntimeError("first")
raise first_exc
except RuntimeError as exc:
middle_exc = ExceptionGroup(
"bad", [ValueError(), ValueError(), TypeError()]
)
raise middle_exc from exc
assert len(excgrp.value.exceptions) == 2
assert all(isinstance(exc, ValueError) for exc in excgrp.value.exceptions)
assert excgrp.value is not middle_exc
assert excgrp.value.__cause__ is first_exc
assert excgrp.value.__context__ is first_exc
def test_catch_subclass(): def test_catch_subclass():
@ -168,3 +194,29 @@ def test_catch_subclass():
assert isinstance(lookup_errors[0], ExceptionGroup) assert isinstance(lookup_errors[0], ExceptionGroup)
exceptions = lookup_errors[0].exceptions exceptions = lookup_errors[0].exceptions
assert isinstance(exceptions[0], KeyError) assert isinstance(exceptions[0], KeyError)
def test_async_handler(request):
async def handler(eg):
pass
def delegate(eg):
coro = handler(eg)
request.addfinalizer(coro.close)
return coro
with pytest.raises(TypeError, match="Exception handler must be a sync function."):
with catch({TypeError: delegate}):
raise ExceptionGroup("message", [TypeError("uh-oh")])
def test_bare_reraise_from_naked_exception():
def handler(eg):
raise
with pytest.raises(ExceptionGroup) as excgrp, catch({Exception: handler}):
raise KeyError("foo")
assert len(excgrp.value.exceptions) == 1
assert isinstance(excgrp.value.exceptions[0], KeyError)
assert str(excgrp.value.exceptions[0]) == "'foo'"

View File

@ -1,3 +1,5 @@
import sys
import pytest import pytest
from exceptiongroup import ExceptionGroup from exceptiongroup import ExceptionGroup
@ -121,18 +123,24 @@ def test_catch_full_match():
pass pass
@pytest.mark.skipif(
sys.version_info < (3, 11, 4),
reason="Behavior was changed in 3.11.4",
)
def test_catch_handler_raises(): def test_catch_handler_raises():
with pytest.raises(RuntimeError, match="new") as exc:
try: try:
try: excgrp = ExceptionGroup("booboo", [ValueError("bar")])
raise ExceptionGroup("booboo", [ValueError("bar")]) raise excgrp
except* ValueError: except* ValueError:
raise RuntimeError("new") raise RuntimeError("new")
except ExceptionGroup as exc:
assert exc.message == "" context = exc.value.__context__
assert len(exc.exceptions) == 1 assert isinstance(context, ExceptionGroup)
assert isinstance(exc.exceptions[0], RuntimeError) assert str(context) == "booboo (1 sub-exception)"
else: assert len(context.exceptions) == 1
pytest.fail("Did not raise an ExceptionGroup") assert isinstance(context.exceptions[0], ValueError)
assert exc.value.__cause__ is None
def test_catch_subclass(): def test_catch_subclass():
@ -146,3 +154,37 @@ def test_catch_subclass():
assert isinstance(lookup_errors[0], ExceptionGroup) assert isinstance(lookup_errors[0], ExceptionGroup)
exceptions = lookup_errors[0].exceptions exceptions = lookup_errors[0].exceptions
assert isinstance(exceptions[0], KeyError) assert isinstance(exceptions[0], KeyError)
def test_bare_raise_in_handler():
"""Test that the "middle" ecxeption group gets discarded."""
with pytest.raises(ExceptionGroup) as excgrp:
try:
try:
first_exc = RuntimeError("first")
raise first_exc
except RuntimeError as exc:
middle_exc = ExceptionGroup(
"bad", [ValueError(), ValueError(), TypeError()]
)
raise middle_exc from exc
except* ValueError:
raise
except* TypeError:
pass
assert excgrp.value is not middle_exc
assert excgrp.value.__cause__ is first_exc
assert excgrp.value.__context__ is first_exc
def test_bare_reraise_from_naked_exception():
with pytest.raises(ExceptionGroup) as excgrp:
try:
raise KeyError("foo")
except* KeyError:
raise
assert len(excgrp.value.exceptions) == 1
assert isinstance(excgrp.value.exceptions[0], KeyError)
assert str(excgrp.value.exceptions[0]) == "'foo'"

View File

@ -290,7 +290,7 @@ class ExceptionGroupSubgroupTests(ExceptionGroupTestBase):
] ]
for match_type, template in testcases: for match_type, template in testcases:
subeg = eg.subgroup(lambda e: isinstance(e, match_type)) # noqa: B023 subeg = eg.subgroup(lambda e: isinstance(e, match_type))
self.assertEqual(subeg.message, eg.message) self.assertEqual(subeg.message, eg.message)
self.assertMatchesTemplate(subeg, ExceptionGroup, template) self.assertMatchesTemplate(subeg, ExceptionGroup, template)
@ -355,7 +355,7 @@ class ExceptionGroupSplitTests(ExceptionGroupTestBase):
] ]
for match_type, match_template, rest_template in testcases: for match_type, match_template, rest_template in testcases:
match, rest = eg.split(lambda e: isinstance(e, match_type)) # noqa: B023 match, rest = eg.split(lambda e: isinstance(e, match_type))
self.assertEqual(match.message, eg.message) self.assertEqual(match.message, eg.message)
self.assertMatchesTemplate(match, ExceptionGroup, match_template) self.assertMatchesTemplate(match, ExceptionGroup, match_template)
if rest_template is not None: if rest_template is not None:
@ -451,7 +451,7 @@ class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase):
eg = create_nested_eg() eg = create_nested_eg()
line0 = create_nested_eg.__code__.co_firstlineno line0 = create_nested_eg.__code__.co_firstlineno
for (tb, expected) in [ for tb, expected in [
(eg.__traceback__, line0 + 19), (eg.__traceback__, line0 + 19),
(eg.exceptions[0].__traceback__, line0 + 6), (eg.exceptions[0].__traceback__, line0 + 6),
(eg.exceptions[1].__traceback__, line0 + 14), (eg.exceptions[1].__traceback__, line0 + 14),
@ -469,7 +469,7 @@ class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase):
line0 = create_nested_eg.__code__.co_firstlineno line0 = create_nested_eg.__code__.co_firstlineno
expected_tbs = [[line0 + 19, line0 + 6, line0 + 4], [line0 + 19, line0 + 14]] expected_tbs = [[line0 + 19, line0 + 6, line0 + 4], [line0 + 19, line0 + 14]]
for (i, (_, tbs)) in enumerate(leaf_generator(eg)): for i, (_, tbs) in enumerate(leaf_generator(eg)):
self.assertSequenceEqual([tb.tb_lineno for tb in tbs], expected_tbs[i]) self.assertSequenceEqual([tb.tb_lineno for tb in tbs], expected_tbs[i])

View File

@ -1,5 +1,7 @@
import sys import sys
import traceback
from typing import NoReturn from typing import NoReturn
from urllib.error import HTTPError
import pytest import pytest
from _pytest.capture import CaptureFixture from _pytest.capture import CaptureFixture
@ -528,3 +530,9 @@ def test_bug_suggestions_attributeerror_no_obj(
print_exception(e) # does not crash print_exception(e) # does not crash
output = capsys.readouterr().err output = capsys.readouterr().err
assert "NamedAttributeError" in output assert "NamedAttributeError" in output
def test_works_around_httperror_bug():
# See https://github.com/python/cpython/issues/98778 in Python <= 3.9
err = HTTPError("url", 405, "METHOD NOT ALLOWED", None, None)
traceback.TracebackException(type(err), err, None)

16
tests/test_suppress.py Normal file
View File

@ -0,0 +1,16 @@
import sys
import pytest
from exceptiongroup import suppress
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
def test_suppress_exception():
with pytest.raises(ExceptionGroup) as exc, suppress(SystemExit):
raise BaseExceptionGroup("", [SystemExit(1), RuntimeError("boo")])
assert len(exc.value.exceptions) == 1
assert isinstance(exc.value.exceptions[0], RuntimeError)