Import Upstream version 1.2.0
This commit is contained in:
parent
41e98eced5
commit
184cb2dc60
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
|
@ -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
|
|
@ -9,10 +9,12 @@ on:
|
|||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
build:
|
||||
name: Build the source tarball and the wheel
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
@ -20,8 +22,38 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pip install build
|
||||
- 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
|
||||
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:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
path: CHANGES.rst
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
|
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
|
@ -27,38 +27,32 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-test-${{ matrix.python-version }}-${{ matrix.os }}
|
||||
allow-prereleases: true
|
||||
cache: pip
|
||||
cache-dependency-path: pyproject.toml
|
||||
- name: Install dependencies
|
||||
run: pip install .[test] coveralls coverage[toml]
|
||||
run: pip install -e .[test] coverage
|
||||
- name: Test with pytest
|
||||
run: coverage run -m pytest
|
||||
- name: Upload Coverage
|
||||
run: coveralls --service=github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
|
||||
COVERALLS_PARALLEL: true
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
parallel: true
|
||||
|
||||
coveralls:
|
||||
name: Finish Coveralls
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
container: python:3-slim
|
||||
steps:
|
||||
- name: Finished
|
||||
run: |
|
||||
pip install coveralls
|
||||
coveralls --service=github --finish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
parallel-finished: true
|
||||
|
|
|
@ -7,6 +7,7 @@ __pycache__
|
|||
.coverage
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.eggs/
|
||||
.tox
|
||||
.idea
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
|
@ -15,37 +15,10 @@ repos:
|
|||
args: ["--fix=lf"]
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: v5.11.3
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.6
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- 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
|
||||
- id: ruff
|
||||
args: [--fix, --show-fixes]
|
||||
exclude: "tests/test_catch_py311.py"
|
||||
|
||||
- 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
|
||||
- id: ruff-format
|
||||
|
|
36
CHANGES.rst
36
CHANGES.rst
|
@ -3,6 +3,42 @@ Version history
|
|||
|
||||
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**
|
||||
|
||||
- Backported upstream fix for gh-99553 (custom subclasses of ``BaseExceptionGroup`` that
|
||||
|
|
14
README.rst
14
README.rst
|
@ -26,6 +26,8 @@ It contains the following:
|
|||
* ``traceback.format_exception_only()``
|
||||
* ``traceback.print_exception()``
|
||||
* ``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
|
||||
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
|
||||
``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
|
||||
========================
|
||||
|
||||
|
|
|
@ -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 +0,0 @@
|
|||
src/exceptiongroup/_version.py
|
|
@ -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.
|
|
@ -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.
|
|
@ -1 +0,0 @@
|
|||
# You must remove unused comment lines for the released package.
|
|
@ -1 +0,0 @@
|
|||
README.rst
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
3.0 (native)
|
|
@ -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/
|
|
@ -1,3 +0,0 @@
|
|||
version=4
|
||||
opts="pgpmode=none" \
|
||||
https://github.com/agronholm/exceptiongroup/tags (?:.*?/)?v?(\d[\d.]*)\.tar\.gz
|
|
@ -30,6 +30,9 @@ test = [
|
|||
]
|
||||
|
||||
[tool.flit.sdist]
|
||||
include = [
|
||||
"tests",
|
||||
]
|
||||
exclude = [
|
||||
".github/*",
|
||||
".gitignore",
|
||||
|
@ -41,16 +44,22 @@ version_scheme = "post-release"
|
|||
local_scheme = "dirty-tag"
|
||||
write_to = "src/exceptiongroup/_version.py"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py37']
|
||||
[tool.ruff]
|
||||
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]
|
||||
src_paths = ["src"]
|
||||
skip_gitignore = true
|
||||
profile = "black"
|
||||
[tool.ruff.pyupgrade]
|
||||
# Preserve types, even if a file imports `from __future__ import annotations`.
|
||||
keep-runtime-typing = true
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 88
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["exceptiongroup"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-rsx --tb=short --strict-config --strict-markers"
|
||||
|
@ -69,13 +78,14 @@ exclude_lines = [
|
|||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py37, py38, py39, py310, py311, pypy3
|
||||
envlist = py37, py38, py39, py310, py311, py312, pypy3
|
||||
skip_missing_interpreters = true
|
||||
minversion = 4.0
|
||||
|
||||
[testenv]
|
||||
extras = test
|
||||
commands = python -m pytest {posargs}
|
||||
usedevelop = true
|
||||
|
||||
[testenv:pyright]
|
||||
deps = pyright
|
||||
|
|
|
@ -6,6 +6,7 @@ __all__ = [
|
|||
"format_exception_only",
|
||||
"print_exception",
|
||||
"print_exc",
|
||||
"suppress",
|
||||
]
|
||||
|
||||
import os
|
||||
|
@ -38,3 +39,8 @@ else:
|
|||
|
||||
BaseExceptionGroup = BaseExceptionGroup
|
||||
ExceptionGroup = ExceptionGroup
|
||||
|
||||
if sys.version_info < (3, 12, 1):
|
||||
from ._suppress import suppress
|
||||
else:
|
||||
from contextlib import suppress
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from contextlib import AbstractContextManager
|
||||
|
@ -33,11 +34,20 @@ class _Catcher:
|
|||
elif unhandled is None:
|
||||
return True
|
||||
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
|
||||
|
||||
def handle_exception(self, exc: BaseException) -> BaseExceptionGroup | None:
|
||||
def handle_exception(self, exc: BaseException) -> BaseException | None:
|
||||
excgroup: BaseExceptionGroup | None
|
||||
if isinstance(exc, BaseExceptionGroup):
|
||||
excgroup = exc
|
||||
|
@ -49,16 +59,30 @@ class _Catcher:
|
|||
matched, excgroup = excgroup.split(exc_types)
|
||||
if matched:
|
||||
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:
|
||||
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:
|
||||
break
|
||||
|
||||
if new_exceptions:
|
||||
if excgroup:
|
||||
new_exceptions.append(excgroup)
|
||||
if len(new_exceptions) == 1:
|
||||
return new_exceptions[0]
|
||||
|
||||
return BaseExceptionGroup("", new_exceptions)
|
||||
elif (
|
||||
|
|
|
@ -27,7 +27,7 @@ def check_direct_subclass(
|
|||
def get_condition_filter(
|
||||
condition: type[_BaseExceptionT]
|
||||
| tuple[type[_BaseExceptionT], ...]
|
||||
| Callable[[_BaseExceptionT_co], bool]
|
||||
| Callable[[_BaseExceptionT_co], bool],
|
||||
) -> Callable[[_BaseExceptionT_co], bool]:
|
||||
if isclass(condition) and issubclass(
|
||||
cast(Type[BaseException], condition), BaseException
|
||||
|
@ -60,7 +60,7 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
|
|||
for i, exc in enumerate(__exceptions):
|
||||
if not isinstance(exc, BaseException):
|
||||
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:
|
||||
|
@ -105,6 +105,12 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
|
|||
) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], ...]:
|
||||
return tuple(self._exceptions)
|
||||
|
||||
@overload
|
||||
def subgroup(
|
||||
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
|
||||
) -> ExceptionGroup[_ExceptionT] | None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def subgroup(
|
||||
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
|
||||
|
@ -113,16 +119,16 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
|
|||
|
||||
@overload
|
||||
def subgroup(
|
||||
self: Self, __condition: Callable[[_BaseExceptionT_co], bool]
|
||||
) -> Self | None:
|
||||
self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
|
||||
) -> BaseExceptionGroup[_BaseExceptionT_co] | None:
|
||||
...
|
||||
|
||||
def subgroup(
|
||||
self: Self,
|
||||
self,
|
||||
__condition: type[_BaseExceptionT]
|
||||
| tuple[type[_BaseExceptionT], ...]
|
||||
| Callable[[_BaseExceptionT_co], bool],
|
||||
) -> BaseExceptionGroup[_BaseExceptionT] | Self | None:
|
||||
| Callable[[_BaseExceptionT_co | Self], bool],
|
||||
) -> BaseExceptionGroup[_BaseExceptionT] | None:
|
||||
condition = get_condition_filter(__condition)
|
||||
modified = False
|
||||
if condition(self):
|
||||
|
@ -155,25 +161,50 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
|
|||
|
||||
@overload
|
||||
def split(
|
||||
self: Self,
|
||||
__condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...],
|
||||
) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]:
|
||||
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
|
||||
) -> tuple[
|
||||
ExceptionGroup[_ExceptionT] | None,
|
||||
BaseExceptionGroup[_BaseExceptionT_co] | None,
|
||||
]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def split(
|
||||
self: Self, __condition: Callable[[_BaseExceptionT_co], bool]
|
||||
) -> tuple[Self | None, Self | None]:
|
||||
self, __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...]
|
||||
) -> 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(
|
||||
self: Self,
|
||||
self,
|
||||
__condition: type[_BaseExceptionT]
|
||||
| tuple[type[_BaseExceptionT], ...]
|
||||
| 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)
|
||||
if condition(self):
|
||||
return self, None
|
||||
|
@ -209,7 +240,19 @@ class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
|
|||
|
||||
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)
|
||||
if hasattr(self, "__notes__"):
|
||||
# Create a new list so that add_note() only affects one exceptiongroup
|
||||
|
@ -245,28 +288,32 @@ class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
|
|||
|
||||
@overload
|
||||
def subgroup(
|
||||
self: Self, __condition: Callable[[_ExceptionT_co], bool]
|
||||
) -> Self | None:
|
||||
self, __condition: Callable[[_ExceptionT_co | Self], bool]
|
||||
) -> ExceptionGroup[_ExceptionT_co] | None:
|
||||
...
|
||||
|
||||
def subgroup(
|
||||
self: Self,
|
||||
self,
|
||||
__condition: type[_ExceptionT]
|
||||
| tuple[type[_ExceptionT], ...]
|
||||
| Callable[[_ExceptionT_co], bool],
|
||||
) -> ExceptionGroup[_ExceptionT] | Self | None:
|
||||
) -> ExceptionGroup[_ExceptionT] | None:
|
||||
return super().subgroup(__condition)
|
||||
|
||||
@overload # type: ignore[override]
|
||||
@overload
|
||||
def split(
|
||||
self: Self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
|
||||
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None]:
|
||||
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
|
||||
) -> tuple[
|
||||
ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None
|
||||
]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def split(
|
||||
self: Self, __condition: Callable[[_ExceptionT_co], bool]
|
||||
) -> tuple[Self | None, Self | None]:
|
||||
self, __condition: Callable[[_ExceptionT_co | Self], bool]
|
||||
) -> tuple[
|
||||
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
|
||||
]:
|
||||
...
|
||||
|
||||
def split(
|
||||
|
@ -274,7 +321,7 @@ class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
|
|||
__condition: type[_ExceptionT]
|
||||
| tuple[type[_ExceptionT], ...]
|
||||
| Callable[[_ExceptionT_co], bool],
|
||||
) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None] | tuple[
|
||||
Self | None, Self | None
|
||||
) -> tuple[
|
||||
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
|
||||
]:
|
||||
return super().split(__condition)
|
||||
|
|
|
@ -103,7 +103,16 @@ class PatchedTracebackException(traceback.TracebackException):
|
|||
# Capture now to permit freeing resources: only complication is in the
|
||||
# unofficial API _format_final_exc_line
|
||||
self._str = _safe_string(exc_value, "exception")
|
||||
try:
|
||||
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):
|
||||
# Handle SyntaxError's specially
|
||||
|
@ -350,6 +359,46 @@ if sys.excepthook is sys.__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
|
||||
def format_exception_only(__exc: BaseException) -> List[str]:
|
||||
|
|
|
@ -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
|
|
@ -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")])
|
|
@ -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
|
||||
+------------------------------------
|
||||
"""
|
||||
)
|
|
@ -148,15 +148,41 @@ def test_catch_handler_raises():
|
|||
def handler(exc):
|
||||
raise RuntimeError("new")
|
||||
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="new") as exc:
|
||||
with catch({(ValueError, ValueError): handler}):
|
||||
raise ExceptionGroup("booboo", [ValueError("bar")])
|
||||
except ExceptionGroup as exc:
|
||||
assert exc.message == ""
|
||||
assert len(exc.exceptions) == 1
|
||||
assert isinstance(exc.exceptions[0], RuntimeError)
|
||||
else:
|
||||
pytest.fail("Did not raise an ExceptionGroup")
|
||||
excgrp = ExceptionGroup("booboo", [ValueError("bar")])
|
||||
raise excgrp
|
||||
|
||||
context = exc.value.__context__
|
||||
assert isinstance(context, ExceptionGroup)
|
||||
assert str(context) == "booboo (1 sub-exception)"
|
||||
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():
|
||||
|
@ -168,3 +194,29 @@ def test_catch_subclass():
|
|||
assert isinstance(lookup_errors[0], ExceptionGroup)
|
||||
exceptions = lookup_errors[0].exceptions
|
||||
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'"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
@ -121,18 +123,24 @@ def test_catch_full_match():
|
|||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 11, 4),
|
||||
reason="Behavior was changed in 3.11.4",
|
||||
)
|
||||
def test_catch_handler_raises():
|
||||
with pytest.raises(RuntimeError, match="new") as exc:
|
||||
try:
|
||||
try:
|
||||
raise ExceptionGroup("booboo", [ValueError("bar")])
|
||||
excgrp = ExceptionGroup("booboo", [ValueError("bar")])
|
||||
raise excgrp
|
||||
except* ValueError:
|
||||
raise RuntimeError("new")
|
||||
except ExceptionGroup as exc:
|
||||
assert exc.message == ""
|
||||
assert len(exc.exceptions) == 1
|
||||
assert isinstance(exc.exceptions[0], RuntimeError)
|
||||
else:
|
||||
pytest.fail("Did not raise an ExceptionGroup")
|
||||
|
||||
context = exc.value.__context__
|
||||
assert isinstance(context, ExceptionGroup)
|
||||
assert str(context) == "booboo (1 sub-exception)"
|
||||
assert len(context.exceptions) == 1
|
||||
assert isinstance(context.exceptions[0], ValueError)
|
||||
assert exc.value.__cause__ is None
|
||||
|
||||
|
||||
def test_catch_subclass():
|
||||
|
@ -146,3 +154,37 @@ def test_catch_subclass():
|
|||
assert isinstance(lookup_errors[0], ExceptionGroup)
|
||||
exceptions = lookup_errors[0].exceptions
|
||||
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'"
|
||||
|
|
|
@ -290,7 +290,7 @@ class ExceptionGroupSubgroupTests(ExceptionGroupTestBase):
|
|||
]
|
||||
|
||||
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.assertMatchesTemplate(subeg, ExceptionGroup, template)
|
||||
|
||||
|
@ -355,7 +355,7 @@ class ExceptionGroupSplitTests(ExceptionGroupTestBase):
|
|||
]
|
||||
|
||||
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.assertMatchesTemplate(match, ExceptionGroup, match_template)
|
||||
if rest_template is not None:
|
||||
|
@ -451,7 +451,7 @@ class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase):
|
|||
eg = create_nested_eg()
|
||||
|
||||
line0 = create_nested_eg.__code__.co_firstlineno
|
||||
for (tb, expected) in [
|
||||
for tb, expected in [
|
||||
(eg.__traceback__, line0 + 19),
|
||||
(eg.exceptions[0].__traceback__, line0 + 6),
|
||||
(eg.exceptions[1].__traceback__, line0 + 14),
|
||||
|
@ -469,7 +469,7 @@ class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase):
|
|||
line0 = create_nested_eg.__code__.co_firstlineno
|
||||
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])
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import sys
|
||||
import traceback
|
||||
from typing import NoReturn
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import pytest
|
||||
from _pytest.capture import CaptureFixture
|
||||
|
@ -528,3 +530,9 @@ def test_bug_suggestions_attributeerror_no_obj(
|
|||
print_exception(e) # does not crash
|
||||
output = capsys.readouterr().err
|
||||
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)
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue