pax_global_header 0000666 0000000 0000000 00000000064 14527066330 0014520 g ustar 00root root 0000000 0000000 52 comment=4e2df2190d3c61a2e7940d6908b32658dfadab3f
exceptiongroup-1.2.0/ 0000775 0000000 0000000 00000000000 14527066330 0014573 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/.github/ 0000775 0000000 0000000 00000000000 14527066330 0016133 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14527066330 0020316 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/bug_report.yaml 0000664 0000000 0000000 00000003265 14527066330 0023360 0 ustar 00root root 0000000 0000000 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
exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000034 14527066330 0022303 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/features_request.yaml 0000664 0000000 0000000 00000002034 14527066330 0024567 0 ustar 00root root 0000000 0000000 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
exceptiongroup-1.2.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14527066330 0020170 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/.github/workflows/publish.yml 0000664 0000000 0000000 00000002577 14527066330 0022374 0 ustar 00root root 0000000 0000000 name: Publish packages to PyPI
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+.post[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
jobs:
build:
name: Build the source tarball and the wheel
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x
- name: Install dependencies
run: pip install build
- name: Create packages
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:
path: CHANGES.rst
- uses: ncipollo/release-action@v1
with:
body: ${{ steps.changelog.outputs.changelog }}
exceptiongroup-1.2.0/.github/workflows/test.yml 0000664 0000000 0000000 00000002571 14527066330 0021677 0 ustar 00root root 0000000 0000000 name: test suite
on:
push:
branches: [main]
pull_request:
jobs:
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.x
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: pip-pyright
- name: Install dependencies
run: pip install -e . pyright
- name: Run pyright
run: pyright --verifytypes exceptiongroup
test:
strategy:
fail-fast: false
matrix:
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@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
run: pip install -e .[test] coverage
- name: Test with pytest
run: coverage run -m pytest
- name: Upload Coverage
uses: coverallsapp/github-action@v2
with:
parallel: true
coveralls:
name: Finish Coveralls
needs: test
runs-on: ubuntu-latest
steps:
- name: Finished
uses: coverallsapp/github-action@v2
with:
parallel-finished: true
exceptiongroup-1.2.0/.gitignore 0000664 0000000 0000000 00000000256 14527066330 0016566 0 ustar 00root root 0000000 0000000 *.egg-info
*.dist-info
*.pyc
build
dist
__pycache__
.coverage
.pytest_cache/
.mypy_cache/
.ruff_cache/
.eggs/
.tox
.idea
.cache
.local
venv*/
/src/exceptiongroup/_version.py
exceptiongroup-1.2.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000001123 14527066330 0021051 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-yaml
- id: debug-statements
exclude: "tests/test_catch_py311.py"
- id: end-of-file-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
args: [--fix, --show-fixes]
exclude: "tests/test_catch_py311.py"
- id: ruff-format
exceptiongroup-1.2.0/CHANGES.rst 0000664 0000000 0000000 00000010541 14527066330 0016376 0 ustar 00root root 0000000 0000000 Version history
===============
This library adheres to `Semantic Versioning 2.0 `_.
**1.2.0**
- Added special monkeypatching if `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 `_
**1.1.1**
- Worked around
`CPython issue #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
also inherit from ``Exception`` should not be able to wrap base exceptions)
- Moved all initialization code to ``__new__()`` (thus matching Python 3.11 behavior)
**1.0.4**
- Fixed regression introduced in v1.0.3 where the code computing the suggestions would
assume that both the ``obj`` attribute of ``AttributeError`` is always available, even
though this is only true from Python 3.10 onwards
(#43; PR by Carl Friedrich Bolz-Tereick)
**1.0.3**
- Fixed monkey patching breaking suggestions (on a ``NameError`` or ``AttributeError``)
on Python 3.10 (#41; PR by Carl Friedrich Bolz-Tereick)
**1.0.2**
- Updated type annotations to match the ones in ``typeshed``
**1.0.1**
- Fixed formatted traceback missing exceptions beyond 2 nesting levels of
``__context__`` or ``__cause__``
**1.0.0**
- Fixed
``AttributeError: 'PatchedTracebackException' object has no attribute '__cause__'``
on Python 3.10 (only) when a traceback is printed from an exception where an exception
group is set as the cause (#33)
- Fixed a loop in exception groups being rendered incorrectly (#35)
- Fixed the patched formatting functions (``format_exception()``etc.) not passing the
``compact=True`` flag on Python 3.10 like the original functions do
**1.0.0rc9**
- Added custom versions of several ``traceback`` functions that work with exception
groups even if monkey patching was disabled or blocked
**1.0.0rc8**
- Don't monkey patch anything if ``sys.excepthook`` has been altered
- Fixed formatting of ``SyntaxError`` in the monkey patched
``TracebackException.format_exception_only()`` method
**1.0.0rc7**
- **BACKWARDS INCOMPATIBLE** Changed ``catch()`` to not wrap an exception in an
exception group if only one exception arrived at ``catch()`` and it was not matched
with any handlers. This was to match the behavior of ``except*``.
**1.0.0rc6**
- **BACKWARDS INCOMPATIBLE** Changed ``catch()`` to match the behavior of ``except*``:
each handler will be called only once per key in the ``handlers`` dictionary, and with
an exception group as the argument. Handlers now also catch subclasses of the given
exception types, just like ``except*``.
**1.0.0rc5**
- Patch for ``traceback.TracebackException.format_exception_only()`` (PR by Zac Hatfield-Dodds)
**1.0.0rc4**
- Update `PEP 678`_ support to use ``.add_note()`` and ``__notes__`` (PR by Zac Hatfield-Dodds)
**1.0.0rc3**
- Added message about the number of sub-exceptions
**1.0.0rc2**
- Display and copy ``__note__`` (draft `PEP 678`_) if available (PR by Zac Hatfield-Dodds)
.. _PEP 678: https://www.python.org/dev/peps/pep-0678/
**1.0.0rc1**
- Initial release
exceptiongroup-1.2.0/LICENSE 0000664 0000000 0000000 00000007170 14527066330 0015605 0 ustar 00root root 0000000 0000000 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.
exceptiongroup-1.2.0/README.rst 0000664 0000000 0000000 00000013244 14527066330 0016266 0 ustar 00root root 0000000 0000000 .. image:: https://github.com/agronholm/exceptiongroup/actions/workflows/test.yml/badge.svg
:target: https://github.com/agronholm/exceptiongroup/actions/workflows/test.yml
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/exceptiongroup/badge.svg?branch=main
:target: https://coveralls.io/github/agronholm/exceptiongroup?branch=main
:alt: Code Coverage
This is a backport of the ``BaseExceptionGroup`` and ``ExceptionGroup`` classes from
Python 3.11.
It contains the following:
* The ``exceptiongroup.BaseExceptionGroup`` and ``exceptiongroup.ExceptionGroup``
classes
* A utility function (``exceptiongroup.catch()``) for catching exceptions possibly
nested in an exception group
* Patches to the ``TracebackException`` class that properly formats exception groups
(installed on import)
* An exception hook that handles formatting of exception groups through
``TracebackException`` (installed on import)
* Special versions of some of the functions from the ``traceback`` module, modified to
correctly handle exception groups even when monkey patching is disabled, or blocked by
another custom exception hook:
* ``traceback.format_exception()``
* ``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
and the exception hook won't be installed.
See the `standard library documentation`_ for more information on exception groups.
.. _standard library documentation: https://docs.python.org/3/library/exceptions.html
Catching exceptions
===================
Due to the lack of the ``except*`` syntax introduced by `PEP 654`_ in earlier Python
versions, you need to use ``exceptiongroup.catch()`` to catch exceptions that are
potentially nested inside an exception group. This function returns a context manager
that calls the given handler for any exceptions matching the sole argument.
The argument to ``catch()`` must be a dict (or any ``Mapping``) where each key is either
an exception class or an iterable of exception classes. Each value must be a callable
that takes a single positional argument. The handler will be called at most once, with
an exception group as an argument which will contain all the exceptions that are any
of the given types, or their subclasses. The exception group may contain nested groups
containing more matching exceptions.
Thus, the following Python 3.11+ code:
.. code-block:: python3
try:
...
except* (ValueError, KeyError) as excgroup:
for exc in excgroup.exceptions:
print('Caught exception:', type(exc))
except* RuntimeError:
print('Caught runtime error')
would be written with this backport like this:
.. code-block:: python3
from exceptiongroup import ExceptionGroup, catch
def value_key_err_handler(excgroup: ExceptionGroup) -> None:
for exc in excgroup.exceptions:
print('Caught exception:', type(exc))
def runtime_err_handler(exc: ExceptionGroup) -> None:
print('Caught runtime error')
with catch({
(ValueError, KeyError): value_key_err_handler,
RuntimeError: runtime_err_handler
}):
...
**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
========================
To make exception groups render properly when an unhandled exception group is being
printed out, this package does two things when it is imported on any Python version
earlier than 3.11:
#. The ``traceback.TracebackException`` class is monkey patched to store extra
information about exception groups (in ``__init__()``) and properly format them (in
``format()``)
#. An exception hook is installed at ``sys.excepthook``, provided that no other hook is
already present. This hook causes the exception to be formatted using
``traceback.TracebackException`` rather than the built-in rendered.
If ``sys.exceptionhook`` is found to be set to something else than the default when
``exceptiongroup`` is imported, no monkeypatching is done at all.
To prevent the exception hook and patches from being installed, set the environment
variable ``EXCEPTIONGROUP_NO_PATCH`` to ``1``.
Formatting exception groups
---------------------------
Normally, the monkey patching applied by this library on import will cause exception
groups to be printed properly in tracebacks. But in cases when the monkey patching is
blocked by a third party exception hook, or monkey patching is explicitly disabled,
you can still manually format exceptions using the special versions of the ``traceback``
functions, like ``format_exception()``, listed at the top of this page. They work just
like their counterparts in the ``traceback`` module, except that they use a separately
patched subclass of ``TracebackException`` to perform the rendering.
Particularly in cases where a library installs its own exception hook, it is recommended
to use these special versions to do the actual formatting of exceptions/tracebacks.
.. _PEP 654: https://www.python.org/dev/peps/pep-0654/
exceptiongroup-1.2.0/pyproject.toml 0000664 0000000 0000000 00000004210 14527066330 0017504 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["flit_scm"]
build-backend = "flit_scm:buildapi"
[project]
name = "exceptiongroup"
description = "Backport of PEP 654 (exception groups)"
readme = "README.rst"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Typing :: Typed"
]
authors = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}]
license = {file = "LICENSE"}
requires-python = ">=3.7"
dynamic = ["version"]
[project.urls]
Changelog = "https://github.com/agronholm/exceptiongroup/blob/main/CHANGES.rst"
"Source code" = "https://github.com/agronholm/exceptiongroup"
"Issue Tracker" = "https://github.com/agronholm/exceptiongroup/issues"
[project.optional-dependencies]
test = [
"pytest >= 6"
]
[tool.flit.sdist]
include = [
"tests",
]
exclude = [
".github/*",
".gitignore",
".pre-commit-config.yaml"
]
[tool.setuptools_scm]
version_scheme = "post-release"
local_scheme = "dirty-tag"
write_to = "src/exceptiongroup/_version.py"
[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.ruff.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true
[tool.ruff.isort]
known-first-party = ["exceptiongroup"]
[tool.pytest.ini_options]
addopts = "-rsx --tb=short --strict-config --strict-markers"
testpaths = ["tests"]
[tool.coverage.run]
source = ["exceptiongroup"]
relative_files = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:"
]
[tool.tox]
legacy_tox_ini = """
[tox]
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
commands = pyright --verifytypes exceptiongroup
usedevelop = true
"""
exceptiongroup-1.2.0/src/ 0000775 0000000 0000000 00000000000 14527066330 0015362 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/src/exceptiongroup/ 0000775 0000000 0000000 00000000000 14527066330 0020435 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/src/exceptiongroup/__init__.py 0000664 0000000 0000000 00000002031 14527066330 0022542 0 ustar 00root root 0000000 0000000 __all__ = [
"BaseExceptionGroup",
"ExceptionGroup",
"catch",
"format_exception",
"format_exception_only",
"print_exception",
"print_exc",
"suppress",
]
import os
import sys
from ._catch import catch
from ._version import version as __version__ # noqa: F401
if sys.version_info < (3, 11):
from ._exceptions import BaseExceptionGroup, ExceptionGroup
from ._formatting import (
format_exception,
format_exception_only,
print_exc,
print_exception,
)
if os.getenv("EXCEPTIONGROUP_NO_PATCH") != "1":
from . import _formatting # noqa: F401
BaseExceptionGroup.__module__ = __name__
ExceptionGroup.__module__ = __name__
else:
from traceback import (
format_exception,
format_exception_only,
print_exc,
print_exception,
)
BaseExceptionGroup = BaseExceptionGroup
ExceptionGroup = ExceptionGroup
if sys.version_info < (3, 12, 1):
from ._suppress import suppress
else:
from contextlib import suppress
exceptiongroup-1.2.0/src/exceptiongroup/_catch.py 0000664 0000000 0000000 00000011075 14527066330 0022234 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import inspect
import sys
from collections.abc import Callable, Iterable, Mapping
from contextlib import AbstractContextManager
from types import TracebackType
from typing import TYPE_CHECKING, Any
if sys.version_info < (3, 11):
from ._exceptions import BaseExceptionGroup
if TYPE_CHECKING:
_Handler = Callable[[BaseException], Any]
class _Catcher:
def __init__(self, handler_map: Mapping[tuple[type[BaseException], ...], _Handler]):
self._handler_map = handler_map
def __enter__(self) -> None:
pass
def __exit__(
self,
etype: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
if exc is not None:
unhandled = self.handle_exception(exc)
if unhandled is exc:
return False
elif unhandled is None:
return True
else:
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) -> BaseException | None:
excgroup: BaseExceptionGroup | None
if isinstance(exc, BaseExceptionGroup):
excgroup = exc
else:
excgroup = BaseExceptionGroup("", [exc])
new_exceptions: list[BaseException] = []
for exc_types, handler in self._handler_map.items():
matched, excgroup = excgroup.split(exc_types)
if matched:
try:
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 len(new_exceptions) == 1:
return new_exceptions[0]
return BaseExceptionGroup("", new_exceptions)
elif (
excgroup and len(excgroup.exceptions) == 1 and excgroup.exceptions[0] is exc
):
return exc
else:
return excgroup
def catch(
__handlers: Mapping[type[BaseException] | Iterable[type[BaseException]], _Handler]
) -> AbstractContextManager[None]:
if not isinstance(__handlers, Mapping):
raise TypeError("the argument must be a mapping")
handler_map: dict[
tuple[type[BaseException], ...], Callable[[BaseExceptionGroup]]
] = {}
for type_or_iterable, handler in __handlers.items():
iterable: tuple[type[BaseException]]
if isinstance(type_or_iterable, type) and issubclass(
type_or_iterable, BaseException
):
iterable = (type_or_iterable,)
elif isinstance(type_or_iterable, Iterable):
iterable = tuple(type_or_iterable)
else:
raise TypeError(
"each key must be either an exception classes or an iterable thereof"
)
if not callable(handler):
raise TypeError("handlers must be callable")
for exc_type in iterable:
if not isinstance(exc_type, type) or not issubclass(
exc_type, BaseException
):
raise TypeError(
"each key must be either an exception classes or an iterable "
"thereof"
)
if issubclass(exc_type, BaseExceptionGroup):
raise TypeError(
"catching ExceptionGroup with catch() is not allowed. "
"Use except instead."
)
handler_map[iterable] = handler
return _Catcher(handler_map)
exceptiongroup-1.2.0/src/exceptiongroup/_exceptions.py 0000664 0000000 0000000 00000025402 14527066330 0023332 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from collections.abc import Callable, Sequence
from functools import partial
from inspect import getmro, isclass
from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload
if TYPE_CHECKING:
from typing import Self
_BaseExceptionT_co = TypeVar("_BaseExceptionT_co", bound=BaseException, covariant=True)
_BaseExceptionT = TypeVar("_BaseExceptionT", bound=BaseException)
_ExceptionT_co = TypeVar("_ExceptionT_co", bound=Exception, covariant=True)
_ExceptionT = TypeVar("_ExceptionT", bound=Exception)
def check_direct_subclass(
exc: BaseException, parents: tuple[type[BaseException]]
) -> bool:
for cls in getmro(exc.__class__)[:-1]:
if cls in parents:
return True
return False
def get_condition_filter(
condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool],
) -> Callable[[_BaseExceptionT_co], bool]:
if isclass(condition) and issubclass(
cast(Type[BaseException], condition), BaseException
):
return partial(check_direct_subclass, parents=(condition,))
elif isinstance(condition, tuple):
if all(isclass(x) and issubclass(x, BaseException) for x in condition):
return partial(check_direct_subclass, parents=condition)
elif callable(condition):
return cast("Callable[[BaseException], bool]", condition)
raise TypeError("expected a function, exception type or tuple of exception types")
class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
"""A combination of multiple unrelated exceptions."""
def __new__(
cls, __message: str, __exceptions: Sequence[_BaseExceptionT_co]
) -> Self:
if not isinstance(__message, str):
raise TypeError(f"argument 1 must be str, not {type(__message)}")
if not isinstance(__exceptions, Sequence):
raise TypeError("second argument (exceptions) must be a sequence")
if not __exceptions:
raise ValueError(
"second argument (exceptions) must be a non-empty sequence"
)
for i, exc in enumerate(__exceptions):
if not isinstance(exc, BaseException):
raise ValueError(
f"Item {i} of second argument (exceptions) is not an exception"
)
if cls is BaseExceptionGroup:
if all(isinstance(exc, Exception) for exc in __exceptions):
cls = ExceptionGroup
if issubclass(cls, Exception):
for exc in __exceptions:
if not isinstance(exc, Exception):
if cls is ExceptionGroup:
raise TypeError(
"Cannot nest BaseExceptions in an ExceptionGroup"
)
else:
raise TypeError(
f"Cannot nest BaseExceptions in {cls.__name__!r}"
)
instance = super().__new__(cls, __message, __exceptions)
instance._message = __message
instance._exceptions = __exceptions
return instance
def add_note(self, note: str) -> None:
if not isinstance(note, str):
raise TypeError(
f"Expected a string, got note={note!r} (type {type(note).__name__})"
)
if not hasattr(self, "__notes__"):
self.__notes__: list[str] = []
self.__notes__.append(note)
@property
def message(self) -> str:
return self._message
@property
def exceptions(
self,
) -> 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], ...]
) -> BaseExceptionGroup[_BaseExceptionT] | None:
...
@overload
def subgroup(
self, __condition: Callable[[_BaseExceptionT_co | Self], bool]
) -> BaseExceptionGroup[_BaseExceptionT_co] | None:
...
def subgroup(
self,
__condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co | Self], bool],
) -> BaseExceptionGroup[_BaseExceptionT] | None:
condition = get_condition_filter(__condition)
modified = False
if condition(self):
return self
exceptions: list[BaseException] = []
for exc in self.exceptions:
if isinstance(exc, BaseExceptionGroup):
subgroup = exc.subgroup(__condition)
if subgroup is not None:
exceptions.append(subgroup)
if subgroup is not exc:
modified = True
elif condition(exc):
exceptions.append(exc)
else:
modified = True
if not modified:
return self
elif exceptions:
group = self.derive(exceptions)
group.__cause__ = self.__cause__
group.__context__ = self.__context__
group.__traceback__ = self.__traceback__
return group
else:
return None
@overload
def split(
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[
ExceptionGroup[_ExceptionT] | None,
BaseExceptionGroup[_BaseExceptionT_co] | None,
]:
...
@overload
def split(
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,
__condition: type[_BaseExceptionT]
| tuple[type[_BaseExceptionT], ...]
| Callable[[_BaseExceptionT_co], bool],
) -> (
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
matching_exceptions: list[BaseException] = []
nonmatching_exceptions: list[BaseException] = []
for exc in self.exceptions:
if isinstance(exc, BaseExceptionGroup):
matching, nonmatching = exc.split(condition)
if matching is not None:
matching_exceptions.append(matching)
if nonmatching is not None:
nonmatching_exceptions.append(nonmatching)
elif condition(exc):
matching_exceptions.append(exc)
else:
nonmatching_exceptions.append(exc)
matching_group: Self | None = None
if matching_exceptions:
matching_group = self.derive(matching_exceptions)
matching_group.__cause__ = self.__cause__
matching_group.__context__ = self.__context__
matching_group.__traceback__ = self.__traceback__
nonmatching_group: Self | None = None
if nonmatching_exceptions:
nonmatching_group = self.derive(nonmatching_exceptions)
nonmatching_group.__cause__ = self.__cause__
nonmatching_group.__context__ = self.__context__
nonmatching_group.__traceback__ = self.__traceback__
return matching_group, nonmatching_group
@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
eg.__notes__ = list(self.__notes__)
return eg
def __str__(self) -> str:
suffix = "" if len(self._exceptions) == 1 else "s"
return f"{self.message} ({len(self._exceptions)} sub-exception{suffix})"
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, {self._exceptions!r})"
class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
def __new__(cls, __message: str, __exceptions: Sequence[_ExceptionT_co]) -> Self:
return super().__new__(cls, __message, __exceptions)
if TYPE_CHECKING:
@property
def exceptions(
self,
) -> tuple[_ExceptionT_co | ExceptionGroup[_ExceptionT_co], ...]:
...
@overload # type: ignore[override]
def subgroup(
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> ExceptionGroup[_ExceptionT] | None:
...
@overload
def subgroup(
self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> ExceptionGroup[_ExceptionT_co] | None:
...
def subgroup(
self,
__condition: type[_ExceptionT]
| tuple[type[_ExceptionT], ...]
| Callable[[_ExceptionT_co], bool],
) -> ExceptionGroup[_ExceptionT] | None:
return super().subgroup(__condition)
@overload
def split(
self, __condition: type[_ExceptionT] | tuple[type[_ExceptionT], ...]
) -> tuple[
ExceptionGroup[_ExceptionT] | None, ExceptionGroup[_ExceptionT_co] | None
]:
...
@overload
def split(
self, __condition: Callable[[_ExceptionT_co | Self], bool]
) -> tuple[
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
]:
...
def split(
self: Self,
__condition: type[_ExceptionT]
| tuple[type[_ExceptionT], ...]
| Callable[[_ExceptionT_co], bool],
) -> tuple[
ExceptionGroup[_ExceptionT_co] | None, ExceptionGroup[_ExceptionT_co] | None
]:
return super().split(__condition)
exceptiongroup-1.2.0/src/exceptiongroup/_formatting.py 0000664 0000000 0000000 00000051015 14527066330 0023322 0 ustar 00root root 0000000 0000000 # traceback_exception_init() adapted from trio
#
# _ExceptionPrintContext and traceback_exception_format() copied from the standard
# library
from __future__ import annotations
import collections.abc
import sys
import textwrap
import traceback
from functools import singledispatch
from types import TracebackType
from typing import Any, List, Optional
from ._exceptions import BaseExceptionGroup
max_group_width = 15
max_group_depth = 10
_cause_message = (
"\nThe above exception was the direct cause of the following exception:\n\n"
)
_context_message = (
"\nDuring handling of the above exception, another exception occurred:\n\n"
)
def _format_final_exc_line(etype, value):
valuestr = _safe_string(value, "exception")
if value is None or not valuestr:
line = f"{etype}\n"
else:
line = f"{etype}: {valuestr}\n"
return line
def _safe_string(value, what, func=str):
try:
return func(value)
except BaseException:
return f"<{what} {func.__name__}() failed>"
class _ExceptionPrintContext:
def __init__(self):
self.seen = set()
self.exception_group_depth = 0
self.need_close = False
def indent(self):
return " " * (2 * self.exception_group_depth)
def emit(self, text_gen, margin_char=None):
if margin_char is None:
margin_char = "|"
indent_str = self.indent()
if self.exception_group_depth:
indent_str += margin_char + " "
if isinstance(text_gen, str):
yield textwrap.indent(text_gen, indent_str, lambda line: True)
else:
for text in text_gen:
yield textwrap.indent(text, indent_str, lambda line: True)
def exceptiongroup_excepthook(
etype: type[BaseException], value: BaseException, tb: TracebackType | None
) -> None:
sys.stderr.write("".join(traceback.format_exception(etype, value, tb)))
class PatchedTracebackException(traceback.TracebackException):
def __init__(
self,
exc_type: type[BaseException],
exc_value: BaseException,
exc_traceback: TracebackType | None,
*,
limit: int | None = None,
lookup_lines: bool = True,
capture_locals: bool = False,
compact: bool = False,
_seen: set[int] | None = None,
) -> None:
kwargs: dict[str, Any] = {}
if sys.version_info >= (3, 10):
kwargs["compact"] = compact
is_recursive_call = _seen is not None
if _seen is None:
_seen = set()
_seen.add(id(exc_value))
self.stack = traceback.StackSummary.extract(
traceback.walk_tb(exc_traceback),
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
)
self.exc_type = exc_type
# 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
self.filename = exc_value.filename
lno = exc_value.lineno
self.lineno = str(lno) if lno is not None else None
self.text = exc_value.text
self.offset = exc_value.offset
self.msg = exc_value.msg
if sys.version_info >= (3, 10):
end_lno = exc_value.end_lineno
self.end_lineno = str(end_lno) if end_lno is not None else None
self.end_offset = exc_value.end_offset
elif (
exc_type
and issubclass(exc_type, (NameError, AttributeError))
and getattr(exc_value, "name", None) is not None
):
suggestion = _compute_suggestion_error(exc_value, exc_traceback)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
if lookup_lines:
# Force all lines in the stack to be loaded
for frame in self.stack:
frame.line
self.__suppress_context__ = (
exc_value.__suppress_context__ if exc_value is not None else False
)
# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
# queue to avoid recursion (only the top-level call gets _seen == None)
if not is_recursive_call:
queue = [(self, exc_value)]
while queue:
te, e = queue.pop()
if e and e.__cause__ is not None and id(e.__cause__) not in _seen:
cause = PatchedTracebackException(
type(e.__cause__),
e.__cause__,
e.__cause__.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen,
)
else:
cause = None
if compact:
need_context = (
cause is None and e is not None and not e.__suppress_context__
)
else:
need_context = True
if (
e
and e.__context__ is not None
and need_context
and id(e.__context__) not in _seen
):
context = PatchedTracebackException(
type(e.__context__),
e.__context__,
e.__context__.__traceback__,
limit=limit,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen,
)
else:
context = None
# Capture each of the exceptions in the ExceptionGroup along with each
# of their causes and contexts
if e and isinstance(e, BaseExceptionGroup):
exceptions = []
for exc in e.exceptions:
texc = PatchedTracebackException(
type(exc),
exc,
exc.__traceback__,
lookup_lines=lookup_lines,
capture_locals=capture_locals,
_seen=_seen,
)
exceptions.append(texc)
else:
exceptions = None
te.__cause__ = cause
te.__context__ = context
te.exceptions = exceptions
if cause:
queue.append((te.__cause__, e.__cause__))
if context:
queue.append((te.__context__, e.__context__))
if exceptions:
queue.extend(zip(te.exceptions, e.exceptions))
def format(self, *, chain=True, _ctx=None):
if _ctx is None:
_ctx = _ExceptionPrintContext()
output = []
exc = self
if chain:
while exc:
if exc.__cause__ is not None:
chained_msg = _cause_message
chained_exc = exc.__cause__
elif exc.__context__ is not None and not exc.__suppress_context__:
chained_msg = _context_message
chained_exc = exc.__context__
else:
chained_msg = None
chained_exc = None
output.append((chained_msg, exc))
exc = chained_exc
else:
output.append((None, exc))
for msg, exc in reversed(output):
if msg is not None:
yield from _ctx.emit(msg)
if exc.exceptions is None:
if exc.stack:
yield from _ctx.emit("Traceback (most recent call last):\n")
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.format_exception_only())
elif _ctx.exception_group_depth > max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n")
else:
# format exception group
is_toplevel = _ctx.exception_group_depth == 0
if is_toplevel:
_ctx.exception_group_depth += 1
if exc.stack:
yield from _ctx.emit(
"Exception Group Traceback (most recent call last):\n",
margin_char="+" if is_toplevel else None,
)
yield from _ctx.emit(exc.stack.format())
yield from _ctx.emit(exc.format_exception_only())
num_excs = len(exc.exceptions)
if num_excs <= max_group_width:
n = num_excs
else:
n = max_group_width + 1
_ctx.need_close = False
for i in range(n):
last_exc = i == n - 1
if last_exc:
# The closing frame may be added by a recursive call
_ctx.need_close = True
if max_group_width is not None:
truncated = i >= max_group_width
else:
truncated = False
title = f"{i + 1}" if not truncated else "..."
yield (
_ctx.indent()
+ ("+-" if i == 0 else " ")
+ f"+---------------- {title} ----------------\n"
)
_ctx.exception_group_depth += 1
if not truncated:
yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx)
else:
remaining = num_excs - max_group_width
plural = "s" if remaining > 1 else ""
yield from _ctx.emit(
f"and {remaining} more exception{plural}\n"
)
if last_exc and _ctx.need_close:
yield _ctx.indent() + "+------------------------------------\n"
_ctx.need_close = False
_ctx.exception_group_depth -= 1
if is_toplevel:
assert _ctx.exception_group_depth == 1
_ctx.exception_group_depth = 0
def format_exception_only(self):
"""Format the exception part of the traceback.
The return value is a generator of strings, each ending in a newline.
Normally, the generator emits a single string; however, for
SyntaxError exceptions, it emits several lines that (when
printed) display detailed information about where the syntax
error occurred.
The message indicating which exception occurred is always the last
string in the output.
"""
if self.exc_type is None:
yield traceback._format_final_exc_line(None, self._str)
return
stype = self.exc_type.__qualname__
smod = self.exc_type.__module__
if smod not in ("__main__", "builtins"):
if not isinstance(smod, str):
smod = ""
stype = smod + "." + stype
if not issubclass(self.exc_type, SyntaxError):
yield _format_final_exc_line(stype, self._str)
elif traceback_exception_format_syntax_error is not None:
yield from traceback_exception_format_syntax_error(self, stype)
else:
yield from traceback_exception_original_format_exception_only(self)
if isinstance(self.__notes__, collections.abc.Sequence):
for note in self.__notes__:
note = _safe_string(note, "note")
yield from [line + "\n" for line in note.split("\n")]
elif self.__notes__ is not None:
yield _safe_string(self.__notes__, "__notes__", func=repr)
traceback_exception_original_format = traceback.TracebackException.format
traceback_exception_original_format_exception_only = (
traceback.TracebackException.format_exception_only
)
traceback_exception_format_syntax_error = getattr(
traceback.TracebackException, "_format_syntax_error", None
)
if sys.excepthook is sys.__excepthook__:
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
)
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]:
return list(
PatchedTracebackException(
type(__exc), __exc, None, compact=True
).format_exception_only()
)
@format_exception_only.register
def _(__exc: type, value: BaseException) -> List[str]:
return format_exception_only(value)
@singledispatch
def format_exception(
__exc: BaseException,
limit: Optional[int] = None,
chain: bool = True,
) -> List[str]:
return list(
PatchedTracebackException(
type(__exc), __exc, __exc.__traceback__, limit=limit, compact=True
).format(chain=chain)
)
@format_exception.register
def _(
__exc: type,
value: BaseException,
tb: TracebackType,
limit: Optional[int] = None,
chain: bool = True,
) -> List[str]:
return format_exception(value, limit, chain)
@singledispatch
def print_exception(
__exc: BaseException,
limit: Optional[int] = None,
file: Any = None,
chain: bool = True,
) -> None:
if file is None:
file = sys.stderr
for line in PatchedTracebackException(
type(__exc), __exc, __exc.__traceback__, limit=limit
).format(chain=chain):
print(line, file=file, end="")
@print_exception.register
def _(
__exc: type,
value: BaseException,
tb: TracebackType,
limit: Optional[int] = None,
file: Any = None,
chain: bool = True,
) -> None:
print_exception(value, limit, file, chain)
def print_exc(
limit: Optional[int] = None,
file: Any | None = None,
chain: bool = True,
) -> None:
value = sys.exc_info()[1]
print_exception(value, limit, file, chain)
# Python levenshtein edit distance code for NameError/AttributeError
# suggestions, backported from 3.12
_MAX_CANDIDATE_ITEMS = 750
_MAX_STRING_SIZE = 40
_MOVE_COST = 2
_CASE_COST = 1
_SENTINEL = object()
def _substitution_cost(ch_a, ch_b):
if ch_a == ch_b:
return 0
if ch_a.lower() == ch_b.lower():
return _CASE_COST
return _MOVE_COST
def _compute_suggestion_error(exc_value, tb):
wrong_name = getattr(exc_value, "name", None)
if wrong_name is None or not isinstance(wrong_name, str):
return None
if isinstance(exc_value, AttributeError):
obj = getattr(exc_value, "obj", _SENTINEL)
if obj is _SENTINEL:
return None
obj = exc_value.obj
try:
d = dir(obj)
except Exception:
return None
else:
assert isinstance(exc_value, NameError)
# find most recent frame
if tb is None:
return None
while tb.tb_next is not None:
tb = tb.tb_next
frame = tb.tb_frame
d = list(frame.f_locals) + list(frame.f_globals) + list(frame.f_builtins)
if len(d) > _MAX_CANDIDATE_ITEMS:
return None
wrong_name_len = len(wrong_name)
if wrong_name_len > _MAX_STRING_SIZE:
return None
best_distance = wrong_name_len
suggestion = None
for possible_name in d:
if possible_name == wrong_name:
# A missing attribute is "found". Don't suggest it (see GH-88821).
continue
# No more than 1/3 of the involved characters should need changed.
max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
# Don't take matches we've already beaten.
max_distance = min(max_distance, best_distance - 1)
current_distance = _levenshtein_distance(
wrong_name, possible_name, max_distance
)
if current_distance > max_distance:
continue
if not suggestion or current_distance < best_distance:
suggestion = possible_name
best_distance = current_distance
return suggestion
def _levenshtein_distance(a, b, max_cost):
# A Python implementation of Python/suggestions.c:levenshtein_distance.
# Both strings are the same
if a == b:
return 0
# Trim away common affixes
pre = 0
while a[pre:] and b[pre:] and a[pre] == b[pre]:
pre += 1
a = a[pre:]
b = b[pre:]
post = 0
while a[: post or None] and b[: post or None] and a[post - 1] == b[post - 1]:
post -= 1
a = a[: post or None]
b = b[: post or None]
if not a or not b:
return _MOVE_COST * (len(a) + len(b))
if len(a) > _MAX_STRING_SIZE or len(b) > _MAX_STRING_SIZE:
return max_cost + 1
# Prefer shorter buffer
if len(b) < len(a):
a, b = b, a
# Quick fail when a match is impossible
if (len(b) - len(a)) * _MOVE_COST > max_cost:
return max_cost + 1
# Instead of producing the whole traditional len(a)-by-len(b)
# matrix, we can update just one row in place.
# Initialize the buffer row
row = list(range(_MOVE_COST, _MOVE_COST * (len(a) + 1), _MOVE_COST))
result = 0
for bindex in range(len(b)):
bchar = b[bindex]
distance = result = bindex * _MOVE_COST
minimum = sys.maxsize
for index in range(len(a)):
# 1) Previous distance in this row is cost(b[:b_index], a[:index])
substitute = distance + _substitution_cost(bchar, a[index])
# 2) cost(b[:b_index], a[:index+1]) from previous row
distance = row[index]
# 3) existing result is cost(b[:b_index+1], a[index])
insert_delete = min(result, distance) + _MOVE_COST
result = min(insert_delete, substitute)
# cost(b[:b_index+1], a[:index+1])
row[index] = result
if result < minimum:
minimum = result
if minimum > max_cost:
# Everything in this row is too big, so bail early.
return max_cost + 1
return result
exceptiongroup-1.2.0/src/exceptiongroup/_suppress.py 0000664 0000000 0000000 00000002472 14527066330 0023037 0 ustar 00root root 0000000 0000000 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
exceptiongroup-1.2.0/src/exceptiongroup/py.typed 0000664 0000000 0000000 00000000000 14527066330 0022122 0 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/tests/ 0000775 0000000 0000000 00000000000 14527066330 0015735 5 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/tests/__init__.py 0000664 0000000 0000000 00000000000 14527066330 0020034 0 ustar 00root root 0000000 0000000 exceptiongroup-1.2.0/tests/apport_excepthook.py 0000664 0000000 0000000 00000000737 14527066330 0022054 0 ustar 00root root 0000000 0000000 # 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")])
exceptiongroup-1.2.0/tests/conftest.py 0000664 0000000 0000000 00000000124 14527066330 0020131 0 ustar 00root root 0000000 0000000 import sys
if sys.version_info < (3, 11):
collect_ignore_glob = ["*_py311.py"]
exceptiongroup-1.2.0/tests/test_apport_monkeypatching.py 0000664 0000000 0000000 00000003605 14527066330 0023757 0 ustar 00root root 0000000 0000000 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
| raise ExceptionGroup("msg1", [KeyError("msg2"), ValueError("msg3")])
| exceptiongroup.ExceptionGroup: msg1 (2 sub-exceptions)
+-+---------------- 1 ----------------
| KeyError: 'msg2'
+---------------- 2 ----------------
| ValueError: msg3
+------------------------------------
"""
)
exceptiongroup-1.2.0/tests/test_catch.py 0000664 0000000 0000000 00000015773 14527066330 0020445 0 ustar 00root root 0000000 0000000 import pytest
from exceptiongroup import BaseExceptionGroup, ExceptionGroup, catch
def test_bad_arg():
with pytest.raises(TypeError, match="the argument must be a mapping"):
with catch(1):
pass
def test_bad_handler():
with pytest.raises(TypeError, match="handlers must be callable"):
with catch({RuntimeError: None}):
pass
@pytest.mark.parametrize(
"exc_type",
[
pytest.param(BaseExceptionGroup, id="naked_basegroup"),
pytest.param(ExceptionGroup, id="naked_group"),
pytest.param((ValueError, BaseExceptionGroup), id="iterable_basegroup"),
pytest.param((ValueError, ExceptionGroup), id="iterable_group"),
],
)
def test_catch_exceptiongroup(exc_type):
with pytest.raises(TypeError, match="catching ExceptionGroup with catch"):
with catch({exc_type: (lambda e: True)}):
pass
def test_catch_ungrouped():
value_type_errors = []
zero_division_errors = []
for exc in [ValueError("foo"), TypeError("bar"), ZeroDivisionError()]:
with catch(
{
(ValueError, TypeError): value_type_errors.append,
ZeroDivisionError: zero_division_errors.append,
}
):
raise exc
assert len(value_type_errors) == 2
assert isinstance(value_type_errors[0], ExceptionGroup)
assert len(value_type_errors[0].exceptions) == 1
assert isinstance(value_type_errors[0].exceptions[0], ValueError)
assert isinstance(value_type_errors[1], ExceptionGroup)
assert len(value_type_errors[1].exceptions) == 1
assert isinstance(value_type_errors[1].exceptions[0], TypeError)
assert len(zero_division_errors) == 1
assert isinstance(zero_division_errors[0], ExceptionGroup)
assert isinstance(zero_division_errors[0].exceptions[0], ZeroDivisionError)
assert len(zero_division_errors[0].exceptions) == 1
def test_catch_group():
value_runtime_errors = []
zero_division_errors = []
with catch(
{
(ValueError, RuntimeError): value_runtime_errors.append,
ZeroDivisionError: zero_division_errors.append,
}
):
raise ExceptionGroup(
"booboo",
[
ValueError("foo"),
ValueError("bar"),
RuntimeError("bar"),
ZeroDivisionError(),
],
)
assert len(value_runtime_errors) == 1
assert isinstance(value_runtime_errors[0], ExceptionGroup)
exceptions = value_runtime_errors[0].exceptions
assert isinstance(exceptions[0], ValueError)
assert isinstance(exceptions[1], ValueError)
assert isinstance(exceptions[2], RuntimeError)
assert len(zero_division_errors) == 1
assert isinstance(zero_division_errors[0], ExceptionGroup)
exceptions = zero_division_errors[0].exceptions
assert isinstance(exceptions[0], ZeroDivisionError)
def test_catch_nested_group():
value_runtime_errors = []
zero_division_errors = []
with catch(
{
(ValueError, RuntimeError): value_runtime_errors.append,
ZeroDivisionError: zero_division_errors.append,
}
):
nested_group = ExceptionGroup(
"nested", [RuntimeError("bar"), ZeroDivisionError()]
)
raise ExceptionGroup("booboo", [ValueError("foo"), nested_group])
assert len(value_runtime_errors) == 1
exceptions = value_runtime_errors[0].exceptions
assert isinstance(exceptions[0], ValueError)
assert isinstance(exceptions[1], ExceptionGroup)
assert isinstance(exceptions[1].exceptions[0], RuntimeError)
assert len(zero_division_errors) == 1
assert isinstance(zero_division_errors[0], ExceptionGroup)
assert isinstance(zero_division_errors[0].exceptions[0], ExceptionGroup)
assert isinstance(
zero_division_errors[0].exceptions[0].exceptions[0], ZeroDivisionError
)
def test_catch_no_match():
try:
with catch({(ValueError, RuntimeError): (lambda e: None)}):
group = ExceptionGroup("booboo", [ZeroDivisionError()])
raise group
except ExceptionGroup as exc:
assert exc is not group
else:
pytest.fail("Did not raise an ExceptionGroup")
def test_catch_single_no_match():
try:
with catch({(ValueError, RuntimeError): (lambda e: None)}):
raise ZeroDivisionError
except ZeroDivisionError:
pass
else:
pytest.fail("Did not raise an ZeroDivisionError")
def test_catch_full_match():
with catch({(ValueError, RuntimeError): (lambda e: None)}):
raise ExceptionGroup("booboo", [ValueError()])
def test_catch_handler_raises():
def handler(exc):
raise RuntimeError("new")
with pytest.raises(RuntimeError, match="new") as exc:
with catch({(ValueError, ValueError): handler}):
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():
lookup_errors = []
with catch({LookupError: lookup_errors.append}):
raise KeyError("foo")
assert len(lookup_errors) == 1
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'"
exceptiongroup-1.2.0/tests/test_catch_py311.py 0000664 0000000 0000000 00000013506 14527066330 0021372 0 ustar 00root root 0000000 0000000 import sys
import pytest
from exceptiongroup import ExceptionGroup
def test_catch_ungrouped():
value_type_errors = []
zero_division_errors = []
for exc in [ValueError("foo"), TypeError("bar"), ZeroDivisionError()]:
try:
raise exc
except* (ValueError, TypeError) as e:
value_type_errors.append(e)
except* ZeroDivisionError as e:
zero_division_errors.append(e)
assert len(value_type_errors) == 2
assert isinstance(value_type_errors[0], ExceptionGroup)
assert len(value_type_errors[0].exceptions) == 1
assert isinstance(value_type_errors[0].exceptions[0], ValueError)
assert isinstance(value_type_errors[1], ExceptionGroup)
assert len(value_type_errors[1].exceptions) == 1
assert isinstance(value_type_errors[1].exceptions[0], TypeError)
assert len(zero_division_errors) == 1
assert isinstance(zero_division_errors[0], ExceptionGroup)
assert isinstance(zero_division_errors[0].exceptions[0], ZeroDivisionError)
assert len(zero_division_errors[0].exceptions) == 1
def test_catch_group():
value_runtime_errors = []
zero_division_errors = []
try:
raise ExceptionGroup(
"booboo",
[
ValueError("foo"),
ValueError("bar"),
RuntimeError("bar"),
ZeroDivisionError(),
],
)
except* (ValueError, RuntimeError) as exc:
value_runtime_errors.append(exc)
except* ZeroDivisionError as exc:
zero_division_errors.append(exc)
assert len(value_runtime_errors) == 1
assert isinstance(value_runtime_errors[0], ExceptionGroup)
exceptions = value_runtime_errors[0].exceptions
assert isinstance(exceptions[0], ValueError)
assert isinstance(exceptions[1], ValueError)
assert isinstance(exceptions[2], RuntimeError)
assert len(zero_division_errors) == 1
assert isinstance(zero_division_errors[0], ExceptionGroup)
exceptions = zero_division_errors[0].exceptions
assert isinstance(exceptions[0], ZeroDivisionError)
def test_catch_nested_group():
value_runtime_errors = []
zero_division_errors = []
try:
nested_group = ExceptionGroup(
"nested", [RuntimeError("bar"), ZeroDivisionError()]
)
raise ExceptionGroup("booboo", [ValueError("foo"), nested_group])
except* (ValueError, RuntimeError) as exc:
value_runtime_errors.append(exc)
except* ZeroDivisionError as exc:
zero_division_errors.append(exc)
assert len(value_runtime_errors) == 1
exceptions = value_runtime_errors[0].exceptions
assert isinstance(exceptions[0], ValueError)
assert isinstance(exceptions[1], ExceptionGroup)
assert isinstance(exceptions[1].exceptions[0], RuntimeError)
assert len(zero_division_errors) == 1
assert isinstance(zero_division_errors[0], ExceptionGroup)
assert isinstance(zero_division_errors[0].exceptions[0], ExceptionGroup)
assert isinstance(
zero_division_errors[0].exceptions[0].exceptions[0], ZeroDivisionError
)
def test_catch_no_match():
try:
try:
group = ExceptionGroup("booboo", [ZeroDivisionError()])
raise group
except* (ValueError, RuntimeError):
pass
except ExceptionGroup as exc:
assert isinstance(exc.exceptions[0], ZeroDivisionError)
assert exc is not group
else:
pytest.fail("Did not raise an ExceptionGroup")
def test_catch_single_no_match():
try:
try:
raise ZeroDivisionError
except* (ValueError, RuntimeError):
pass
except ZeroDivisionError:
pass
else:
pytest.fail("Did not raise an ZeroDivisionError")
def test_catch_full_match():
try:
raise ExceptionGroup("booboo", [ValueError()])
except* (ValueError, RuntimeError):
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:
excgrp = ExceptionGroup("booboo", [ValueError("bar")])
raise excgrp
except* ValueError:
raise RuntimeError("new")
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():
lookup_errors = []
try:
raise KeyError("foo")
except* LookupError as e:
lookup_errors.append(e)
assert len(lookup_errors) == 1
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'"
exceptiongroup-1.2.0/tests/test_exceptions.py 0000664 0000000 0000000 00000073713 14527066330 0021542 0 ustar 00root root 0000000 0000000 # Copied from the standard library
import collections.abc
import sys
import unittest
import pytest
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
class TestExceptionGroupTypeHierarchy(unittest.TestCase):
def test_exception_group_types(self):
self.assertTrue(issubclass(ExceptionGroup, Exception))
self.assertTrue(issubclass(ExceptionGroup, BaseExceptionGroup))
self.assertTrue(issubclass(BaseExceptionGroup, BaseException))
def test_exception_group_is_generic_type(self):
E = OSError
self.assertEqual(ExceptionGroup[E].__origin__, ExceptionGroup)
self.assertEqual(BaseExceptionGroup[E].__origin__, BaseExceptionGroup)
class BadConstructorArgs(unittest.TestCase):
def test_bad_EG_construction__too_few_args(self):
if sys.version_info >= (3, 11):
MSG = (
r"BaseExceptionGroup.__new__\(\) takes exactly 2 arguments \(1 given\)"
)
else:
MSG = (
r"__new__\(\) missing 1 required positional argument: "
r"'_ExceptionGroup__exceptions'"
)
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup("no errors")
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup([ValueError("no msg")])
def test_bad_EG_construction__too_many_args(self):
if sys.version_info >= (3, 11):
MSG = (
r"BaseExceptionGroup.__new__\(\) takes exactly 2 arguments \(3 given\)"
)
else:
MSG = r"__new__\(\) takes 3 positional arguments but 4 were given"
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup("eg", [ValueError("too")], [TypeError("many")])
def test_bad_EG_construction__bad_message(self):
MSG = "argument 1 must be str, not "
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup(ValueError(12), SyntaxError("bad syntax"))
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup(None, [ValueError(12)])
def test_bad_EG_construction__bad_excs_sequence(self):
MSG = r"second argument \(exceptions\) must be a sequence"
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup("errors not sequence", {ValueError(42)})
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup("eg", None)
MSG = r"second argument \(exceptions\) must be a non-empty sequence"
with self.assertRaisesRegex(ValueError, MSG):
ExceptionGroup("eg", [])
def test_bad_EG_construction__nested_non_exceptions(self):
MSG = r"Item [0-9]+ of second argument \(exceptions\) is not an exception"
with self.assertRaisesRegex(ValueError, MSG):
ExceptionGroup("expect instance, not type", [OSError])
with self.assertRaisesRegex(ValueError, MSG):
ExceptionGroup("bad error", ["not an exception"])
class InstanceCreation(unittest.TestCase):
def test_EG_wraps_Exceptions__creates_EG(self):
excs = [ValueError(1), TypeError(2)]
self.assertIs(type(ExceptionGroup("eg", excs)), ExceptionGroup)
def test_BEG_wraps_Exceptions__creates_EG(self):
excs = [ValueError(1), TypeError(2)]
self.assertIs(type(BaseExceptionGroup("beg", excs)), ExceptionGroup)
def test_EG_wraps_BaseException__raises_TypeError(self):
MSG = "Cannot nest BaseExceptions in an ExceptionGroup"
with self.assertRaisesRegex(TypeError, MSG):
ExceptionGroup("eg", [ValueError(1), KeyboardInterrupt(2)])
def test_BEG_wraps_BaseException__creates_BEG(self):
beg = BaseExceptionGroup("beg", [ValueError(1), KeyboardInterrupt(2)])
self.assertIs(type(beg), BaseExceptionGroup)
def test_EG_subclass_wraps_non_base_exceptions(self):
class MyEG(ExceptionGroup):
pass
self.assertIs(type(MyEG("eg", [ValueError(12), TypeError(42)])), MyEG)
@pytest.mark.skipif(
sys.version_info[:3] == (3, 11, 0),
reason="Behavior was made stricter in 3.11.1",
)
def test_EG_subclass_does_not_wrap_base_exceptions(self):
class MyEG(ExceptionGroup):
pass
msg = "Cannot nest BaseExceptions in 'MyEG'"
with self.assertRaisesRegex(TypeError, msg):
MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])
@pytest.mark.skipif(
sys.version_info[:3] == (3, 11, 0),
reason="Behavior was made stricter in 3.11.1",
)
def test_BEG_and_E_subclass_does_not_wrap_base_exceptions(self):
class MyEG(BaseExceptionGroup, ValueError):
pass
msg = "Cannot nest BaseExceptions in 'MyEG'"
with self.assertRaisesRegex(TypeError, msg):
MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])
def create_simple_eg():
excs = []
try:
try:
raise MemoryError("context and cause for ValueError(1)")
except MemoryError as e:
raise ValueError(1) from e
except ValueError as e:
excs.append(e)
try:
try:
raise OSError("context for TypeError")
except OSError:
raise TypeError(int)
except TypeError as e:
excs.append(e)
try:
try:
raise ImportError("context for ValueError(2)")
except ImportError:
raise ValueError(2)
except ValueError as e:
excs.append(e)
try:
raise ExceptionGroup("simple eg", excs)
except ExceptionGroup as e:
return e
class ExceptionGroupFields(unittest.TestCase):
def test_basics_ExceptionGroup_fields(self):
eg = create_simple_eg()
# check msg
self.assertEqual(eg.message, "simple eg")
self.assertEqual(eg.args[0], "simple eg")
# check cause and context
self.assertIsInstance(eg.exceptions[0], ValueError)
self.assertIsInstance(eg.exceptions[0].__cause__, MemoryError)
self.assertIsInstance(eg.exceptions[0].__context__, MemoryError)
self.assertIsInstance(eg.exceptions[1], TypeError)
self.assertIsNone(eg.exceptions[1].__cause__)
self.assertIsInstance(eg.exceptions[1].__context__, OSError)
self.assertIsInstance(eg.exceptions[2], ValueError)
self.assertIsNone(eg.exceptions[2].__cause__)
self.assertIsInstance(eg.exceptions[2].__context__, ImportError)
# check tracebacks
line0 = create_simple_eg.__code__.co_firstlineno
tb_linenos = [line0 + 27, [line0 + 6, line0 + 14, line0 + 22]]
self.assertEqual(eg.__traceback__.tb_lineno, tb_linenos[0])
self.assertIsNone(eg.__traceback__.tb_next)
for i in range(3):
tb = eg.exceptions[i].__traceback__
self.assertIsNone(tb.tb_next)
self.assertEqual(tb.tb_lineno, tb_linenos[1][i])
def test_fields_are_readonly(self):
eg = ExceptionGroup("eg", [TypeError(1), OSError(2)])
self.assertEqual(type(eg.exceptions), tuple)
eg.message
with self.assertRaises(AttributeError):
eg.message = "new msg"
eg.exceptions
with self.assertRaises(AttributeError):
eg.exceptions = [OSError("xyz")]
def test_notes_is_list_of_strings_if_it_exists(self):
eg = create_simple_eg()
note = "This is a happy note for the exception group"
self.assertFalse(hasattr(eg, "__notes__"))
eg.add_note(note)
self.assertEqual(eg.__notes__, [note])
class ExceptionGroupTestBase(unittest.TestCase):
def assertMatchesTemplate(self, exc, exc_type, template):
"""Assert that the exception matches the template
A template describes the shape of exc. If exc is a
leaf exception (i.e., not an exception group) then
template is an exception instance that has the
expected type and args value of exc. If exc is an
exception group, then template is a list of the
templates of its nested exceptions.
"""
if exc_type is not None:
self.assertIs(type(exc), exc_type)
if isinstance(exc, BaseExceptionGroup):
self.assertIsInstance(template, collections.abc.Sequence)
self.assertEqual(len(exc.exceptions), len(template))
for e, t in zip(exc.exceptions, template):
self.assertMatchesTemplate(e, None, t)
else:
self.assertIsInstance(template, BaseException)
self.assertEqual(type(exc), type(template))
self.assertEqual(exc.args, template.args)
class ExceptionGroupSubgroupTests(ExceptionGroupTestBase):
def setUp(self):
self.eg = create_simple_eg()
self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]
def test_basics_subgroup_split__bad_arg_type(self):
bad_args = [
"bad arg",
OSError("instance not type"),
[OSError, TypeError],
(OSError, 42),
]
for arg in bad_args:
with self.assertRaises(TypeError):
self.eg.subgroup(arg)
with self.assertRaises(TypeError):
self.eg.split(arg)
def test_basics_subgroup_by_type__passthrough(self):
eg = self.eg
# self.assertIs(eg, eg.subgroup(BaseException))
# self.assertIs(eg, eg.subgroup(Exception))
self.assertIs(eg, eg.subgroup(BaseExceptionGroup))
self.assertIs(eg, eg.subgroup(ExceptionGroup))
def test_basics_subgroup_by_type__no_match(self):
self.assertIsNone(self.eg.subgroup(OSError))
def test_basics_subgroup_by_type__match(self):
eg = self.eg
testcases = [
# (match_type, result_template)
(ValueError, [ValueError(1), ValueError(2)]),
(TypeError, [TypeError(int)]),
((ValueError, TypeError), self.eg_template),
]
for match_type, template in testcases:
with self.subTest(match=match_type):
subeg = eg.subgroup(match_type)
self.assertEqual(subeg.message, eg.message)
self.assertMatchesTemplate(subeg, ExceptionGroup, template)
def test_basics_subgroup_by_predicate__passthrough(self):
self.assertIs(self.eg, self.eg.subgroup(lambda e: True))
def test_basics_subgroup_by_predicate__no_match(self):
self.assertIsNone(self.eg.subgroup(lambda e: False))
def test_basics_subgroup_by_predicate__match(self):
eg = self.eg
testcases = [
# (match_type, result_template)
(ValueError, [ValueError(1), ValueError(2)]),
(TypeError, [TypeError(int)]),
((ValueError, TypeError), self.eg_template),
]
for match_type, template in testcases:
subeg = eg.subgroup(lambda e: isinstance(e, match_type))
self.assertEqual(subeg.message, eg.message)
self.assertMatchesTemplate(subeg, ExceptionGroup, template)
class ExceptionGroupSplitTests(ExceptionGroupTestBase):
def setUp(self):
self.eg = create_simple_eg()
self.eg_template = [ValueError(1), TypeError(int), ValueError(2)]
def test_basics_split_by_type__passthrough(self):
for E in [BaseException, Exception, BaseExceptionGroup, ExceptionGroup]:
match, rest = self.eg.split(E)
self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template)
self.assertIsNone(rest)
def test_basics_split_by_type__no_match(self):
match, rest = self.eg.split(OSError)
self.assertIsNone(match)
self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template)
def test_basics_split_by_type__match(self):
eg = self.eg
VE = ValueError
TE = TypeError
testcases = [
# (matcher, match_template, rest_template)
(VE, [VE(1), VE(2)], [TE(int)]),
(TE, [TE(int)], [VE(1), VE(2)]),
((VE, TE), self.eg_template, None),
((OSError, VE), [VE(1), VE(2)], [TE(int)]),
]
for match_type, match_template, rest_template in testcases:
match, rest = eg.split(match_type)
self.assertEqual(match.message, eg.message)
self.assertMatchesTemplate(match, ExceptionGroup, match_template)
if rest_template is not None:
self.assertEqual(rest.message, eg.message)
self.assertMatchesTemplate(rest, ExceptionGroup, rest_template)
else:
self.assertIsNone(rest)
def test_basics_split_by_predicate__passthrough(self):
match, rest = self.eg.split(lambda e: True)
self.assertMatchesTemplate(match, ExceptionGroup, self.eg_template)
self.assertIsNone(rest)
def test_basics_split_by_predicate__no_match(self):
match, rest = self.eg.split(lambda e: False)
self.assertIsNone(match)
self.assertMatchesTemplate(rest, ExceptionGroup, self.eg_template)
def test_basics_split_by_predicate__match(self):
eg = self.eg
VE = ValueError
TE = TypeError
testcases = [
# (matcher, match_template, rest_template)
(VE, [VE(1), VE(2)], [TE(int)]),
(TE, [TE(int)], [VE(1), VE(2)]),
((VE, TE), self.eg_template, None),
]
for match_type, match_template, rest_template in testcases:
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:
self.assertEqual(rest.message, eg.message)
self.assertMatchesTemplate(rest, ExceptionGroup, rest_template)
class DeepRecursionInSplitAndSubgroup(unittest.TestCase):
def make_deep_eg(self):
e = TypeError(1)
for _ in range(2000):
e = ExceptionGroup("eg", [e])
return e
def test_deep_split(self):
e = self.make_deep_eg()
with self.assertRaises(RecursionError):
e.split(TypeError)
def test_deep_subgroup(self):
e = self.make_deep_eg()
with self.assertRaises(RecursionError):
e.subgroup(TypeError)
def leaf_generator(exc, tbs=None):
if tbs is None:
tbs = []
tbs.append(exc.__traceback__)
if isinstance(exc, BaseExceptionGroup):
for e in exc.exceptions:
yield from leaf_generator(e, tbs)
else:
# exc is a leaf exception and its traceback
# is the concatenation of the traceback
# segments in tbs
yield exc, tbs
tbs.pop()
class LeafGeneratorTest(unittest.TestCase):
# The leaf_generator is mentioned in PEP 654 as a suggestion
# on how to iterate over leaf nodes of an EG. Is is also
# used below as a test utility. So we test it here.
def test_leaf_generator(self):
eg = create_simple_eg()
self.assertSequenceEqual([e for e, _ in leaf_generator(eg)], eg.exceptions)
for e, tbs in leaf_generator(eg):
self.assertSequenceEqual(tbs, [eg.__traceback__, e.__traceback__])
def create_nested_eg():
excs = []
try:
try:
raise TypeError(bytes)
except TypeError as e:
raise ExceptionGroup("nested", [e])
except ExceptionGroup as e:
excs.append(e)
try:
try:
raise MemoryError("out of memory")
except MemoryError as e:
raise ValueError(1) from e
except ValueError as e:
excs.append(e)
try:
raise ExceptionGroup("root", excs)
except ExceptionGroup as eg:
return eg
class NestedExceptionGroupBasicsTest(ExceptionGroupTestBase):
def test_nested_group_matches_template(self):
eg = create_nested_eg()
self.assertMatchesTemplate(
eg, ExceptionGroup, [[TypeError(bytes)], ValueError(1)]
)
def test_nested_group_chaining(self):
eg = create_nested_eg()
self.assertIsInstance(eg.exceptions[1].__context__, MemoryError)
self.assertIsInstance(eg.exceptions[1].__cause__, MemoryError)
self.assertIsInstance(eg.exceptions[0].__context__, TypeError)
def test_nested_exception_group_tracebacks(self):
eg = create_nested_eg()
line0 = create_nested_eg.__code__.co_firstlineno
for tb, expected in [
(eg.__traceback__, line0 + 19),
(eg.exceptions[0].__traceback__, line0 + 6),
(eg.exceptions[1].__traceback__, line0 + 14),
(eg.exceptions[0].exceptions[0].__traceback__, line0 + 4),
]:
self.assertEqual(tb.tb_lineno, expected)
self.assertIsNone(tb.tb_next)
def test_iteration_full_tracebacks(self):
eg = create_nested_eg()
# check that iteration over leaves
# produces the expected tracebacks
self.assertEqual(len(list(leaf_generator(eg))), 2)
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)):
self.assertSequenceEqual([tb.tb_lineno for tb in tbs], expected_tbs[i])
class ExceptionGroupSplitTestBase(ExceptionGroupTestBase):
def split_exception_group(self, eg, types):
"""Split an EG and do some sanity checks on the result"""
self.assertIsInstance(eg, BaseExceptionGroup)
match, rest = eg.split(types)
sg = eg.subgroup(types)
if match is not None:
self.assertIsInstance(match, BaseExceptionGroup)
for e, _ in leaf_generator(match):
self.assertIsInstance(e, types)
self.assertIsNotNone(sg)
self.assertIsInstance(sg, BaseExceptionGroup)
for e, _ in leaf_generator(sg):
self.assertIsInstance(e, types)
if rest is not None:
self.assertIsInstance(rest, BaseExceptionGroup)
def leaves(exc):
return [] if exc is None else [e for e, _ in leaf_generator(exc)]
# match and subgroup have the same leaves
self.assertSequenceEqual(leaves(match), leaves(sg))
match_leaves = leaves(match)
rest_leaves = leaves(rest)
# each leaf exception of eg is in exactly one of match and rest
self.assertEqual(len(leaves(eg)), len(leaves(match)) + len(leaves(rest)))
for e in leaves(eg):
self.assertNotEqual(match and e in match_leaves, rest and e in rest_leaves)
# message, cause and context, traceback and note equal to eg
for part in [match, rest, sg]:
if part is not None:
self.assertEqual(eg.message, part.message)
self.assertIs(eg.__cause__, part.__cause__)
self.assertIs(eg.__context__, part.__context__)
self.assertIs(eg.__traceback__, part.__traceback__)
self.assertEqual(
getattr(eg, "__notes__", None),
getattr(part, "__notes__", None),
)
def tbs_for_leaf(leaf, eg):
for e, tbs in leaf_generator(eg):
if e is leaf:
return tbs
def tb_linenos(tbs):
return [tb.tb_lineno for tb in tbs if tb]
# full tracebacks match
for part in [match, rest, sg]:
for e in leaves(part):
self.assertSequenceEqual(
tb_linenos(tbs_for_leaf(e, eg)), tb_linenos(tbs_for_leaf(e, part))
)
return match, rest
class NestedExceptionGroupSplitTest(ExceptionGroupSplitTestBase):
def test_split_by_type(self):
class MyExceptionGroup(ExceptionGroup):
pass
def raiseVE(v):
raise ValueError(v)
def raiseTE(t):
raise TypeError(t)
def nested_group():
def level1(i):
excs = []
for f, arg in [(raiseVE, i), (raiseTE, int), (raiseVE, i + 1)]:
try:
f(arg)
except Exception as e:
excs.append(e)
raise ExceptionGroup("msg1", excs)
def level2(i):
excs = []
for f, arg in [(level1, i), (level1, i + 1), (raiseVE, i + 2)]:
try:
f(arg)
except Exception as e:
excs.append(e)
raise MyExceptionGroup("msg2", excs)
def level3(i):
excs = []
for f, arg in [(level2, i + 1), (raiseVE, i + 2)]:
try:
f(arg)
except Exception as e:
excs.append(e)
raise ExceptionGroup("msg3", excs)
level3(5)
try:
nested_group()
except ExceptionGroup as e:
e.add_note(f"the note: {id(e)}")
eg = e
eg_template = [
[
[ValueError(6), TypeError(int), ValueError(7)],
[ValueError(7), TypeError(int), ValueError(8)],
ValueError(8),
],
ValueError(7),
]
valueErrors_template = [
[
[ValueError(6), ValueError(7)],
[ValueError(7), ValueError(8)],
ValueError(8),
],
ValueError(7),
]
typeErrors_template = [[[TypeError(int)], [TypeError(int)]]]
self.assertMatchesTemplate(eg, ExceptionGroup, eg_template)
# Match Nothing
match, rest = self.split_exception_group(eg, SyntaxError)
self.assertIsNone(match)
self.assertMatchesTemplate(rest, ExceptionGroup, eg_template)
# Match Everything
match, rest = self.split_exception_group(eg, BaseException)
self.assertMatchesTemplate(match, ExceptionGroup, eg_template)
self.assertIsNone(rest)
match, rest = self.split_exception_group(eg, (ValueError, TypeError))
self.assertMatchesTemplate(match, ExceptionGroup, eg_template)
self.assertIsNone(rest)
# Match ValueErrors
match, rest = self.split_exception_group(eg, ValueError)
self.assertMatchesTemplate(match, ExceptionGroup, valueErrors_template)
self.assertMatchesTemplate(rest, ExceptionGroup, typeErrors_template)
# Match TypeErrors
match, rest = self.split_exception_group(eg, (TypeError, SyntaxError))
self.assertMatchesTemplate(match, ExceptionGroup, typeErrors_template)
self.assertMatchesTemplate(rest, ExceptionGroup, valueErrors_template)
# Match ExceptionGroup
match, rest = eg.split(ExceptionGroup)
self.assertIs(match, eg)
self.assertIsNone(rest)
# Match MyExceptionGroup (ExceptionGroup subclass)
match, rest = eg.split(MyExceptionGroup)
self.assertMatchesTemplate(match, ExceptionGroup, [eg_template[0]])
self.assertMatchesTemplate(rest, ExceptionGroup, [eg_template[1]])
def test_split_BaseExceptionGroup(self):
def exc(ex):
try:
raise ex
except BaseException as e:
return e
try:
raise BaseExceptionGroup(
"beg", [exc(ValueError(1)), exc(KeyboardInterrupt(2))]
)
except BaseExceptionGroup as e:
beg = e
# Match Nothing
match, rest = self.split_exception_group(beg, TypeError)
self.assertIsNone(match)
self.assertMatchesTemplate(
rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]
)
# Match Everything
match, rest = self.split_exception_group(beg, (ValueError, KeyboardInterrupt))
self.assertMatchesTemplate(
match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]
)
self.assertIsNone(rest)
# Match ValueErrors
match, rest = self.split_exception_group(beg, ValueError)
self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)])
self.assertMatchesTemplate(rest, BaseExceptionGroup, [KeyboardInterrupt(2)])
# Match KeyboardInterrupts
match, rest = self.split_exception_group(beg, KeyboardInterrupt)
self.assertMatchesTemplate(match, BaseExceptionGroup, [KeyboardInterrupt(2)])
self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)])
class NestedExceptionGroupSubclassSplitTest(ExceptionGroupSplitTestBase):
def test_split_ExceptionGroup_subclass_no_derive_no_new_override(self):
class EG(ExceptionGroup):
pass
try:
try:
try:
raise TypeError(2)
except TypeError as te:
raise EG("nested", [te])
except EG as nested:
try:
raise ValueError(1)
except ValueError as ve:
raise EG("eg", [ve, nested])
except EG as e:
eg = e
self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]])
# Match Nothing
match, rest = self.split_exception_group(eg, OSError)
self.assertIsNone(match)
self.assertMatchesTemplate(
rest, ExceptionGroup, [ValueError(1), [TypeError(2)]]
)
# Match Everything
match, rest = self.split_exception_group(eg, (ValueError, TypeError))
self.assertMatchesTemplate(
match, ExceptionGroup, [ValueError(1), [TypeError(2)]]
)
self.assertIsNone(rest)
# Match ValueErrors
match, rest = self.split_exception_group(eg, ValueError)
self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)])
self.assertMatchesTemplate(rest, ExceptionGroup, [[TypeError(2)]])
# Match TypeErrors
match, rest = self.split_exception_group(eg, TypeError)
self.assertMatchesTemplate(match, ExceptionGroup, [[TypeError(2)]])
self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)])
def test_split_BaseExceptionGroup_subclass_no_derive_new_override(self):
class EG(BaseExceptionGroup):
def __new__(cls, message, excs, unused):
# The "unused" arg is here to show that split() doesn't call
# the actual class constructor from the default derive()
# implementation (it would fail on unused arg if so because
# it assumes the BaseExceptionGroup.__new__ signature).
return super().__new__(cls, message, excs)
try:
raise EG("eg", [ValueError(1), KeyboardInterrupt(2)], "unused")
except EG as e:
eg = e
self.assertMatchesTemplate(eg, EG, [ValueError(1), KeyboardInterrupt(2)])
# Match Nothing
match, rest = self.split_exception_group(eg, OSError)
self.assertIsNone(match)
self.assertMatchesTemplate(
rest, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]
)
# Match Everything
match, rest = self.split_exception_group(eg, (ValueError, KeyboardInterrupt))
self.assertMatchesTemplate(
match, BaseExceptionGroup, [ValueError(1), KeyboardInterrupt(2)]
)
self.assertIsNone(rest)
# Match ValueErrors
match, rest = self.split_exception_group(eg, ValueError)
self.assertMatchesTemplate(match, ExceptionGroup, [ValueError(1)])
self.assertMatchesTemplate(rest, BaseExceptionGroup, [KeyboardInterrupt(2)])
# Match KeyboardInterrupt
match, rest = self.split_exception_group(eg, KeyboardInterrupt)
self.assertMatchesTemplate(match, BaseExceptionGroup, [KeyboardInterrupt(2)])
self.assertMatchesTemplate(rest, ExceptionGroup, [ValueError(1)])
def test_split_ExceptionGroup_subclass_derive_and_new_overrides(self):
class EG(ExceptionGroup):
def __new__(cls, message, excs, code):
obj = super().__new__(cls, message, excs)
obj.code = code
return obj
def derive(self, excs):
return EG(self.message, excs, self.code)
try:
try:
try:
raise TypeError(2)
except TypeError as te:
raise EG("nested", [te], 101)
except EG as nested:
try:
raise ValueError(1)
except ValueError as ve:
raise EG("eg", [ve, nested], 42)
except EG as e:
eg = e
self.assertMatchesTemplate(eg, EG, [ValueError(1), [TypeError(2)]])
# Match Nothing
match, rest = self.split_exception_group(eg, OSError)
self.assertIsNone(match)
self.assertMatchesTemplate(rest, EG, [ValueError(1), [TypeError(2)]])
self.assertEqual(rest.code, 42)
self.assertEqual(rest.exceptions[1].code, 101)
# Match Everything
match, rest = self.split_exception_group(eg, (ValueError, TypeError))
self.assertMatchesTemplate(match, EG, [ValueError(1), [TypeError(2)]])
self.assertEqual(match.code, 42)
self.assertEqual(match.exceptions[1].code, 101)
self.assertIsNone(rest)
# Match ValueErrors
match, rest = self.split_exception_group(eg, ValueError)
self.assertMatchesTemplate(match, EG, [ValueError(1)])
self.assertEqual(match.code, 42)
self.assertMatchesTemplate(rest, EG, [[TypeError(2)]])
self.assertEqual(rest.code, 42)
self.assertEqual(rest.exceptions[0].code, 101)
# Match TypeErrors
match, rest = self.split_exception_group(eg, TypeError)
self.assertMatchesTemplate(match, EG, [[TypeError(2)]])
self.assertEqual(match.code, 42)
self.assertEqual(match.exceptions[0].code, 101)
self.assertMatchesTemplate(rest, EG, [ValueError(1)])
self.assertEqual(rest.code, 42)
def test_repr():
group = BaseExceptionGroup("foo", [ValueError(1), KeyboardInterrupt()])
assert repr(group) == (
"BaseExceptionGroup('foo', [ValueError(1), KeyboardInterrupt()])"
)
group = ExceptionGroup("foo", [ValueError(1), RuntimeError("bar")])
assert repr(group) == "ExceptionGroup('foo', [ValueError(1), RuntimeError('bar')])"
exceptiongroup-1.2.0/tests/test_formatting.py 0000664 0000000 0000000 00000043520 14527066330 0021524 0 ustar 00root root 0000000 0000000 import sys
import traceback
from typing import NoReturn
from urllib.error import HTTPError
import pytest
from _pytest.capture import CaptureFixture
from _pytest.fixtures import SubRequest
from _pytest.monkeypatch import MonkeyPatch
from exceptiongroup import ExceptionGroup
def raise_excgroup() -> NoReturn:
exceptions = []
try:
raise ValueError("foo")
except ValueError as exc:
exceptions.append(exc)
try:
raise RuntimeError("bar")
except RuntimeError as exc:
exc.__notes__ = ["Note from bar handler"]
exceptions.append(exc)
exc = ExceptionGroup("test message", exceptions)
exc.add_note("Displays notes attached to the group too")
raise exc
@pytest.fixture(
params=[
pytest.param(True, id="patched"),
pytest.param(
False,
id="unpatched",
marks=[
pytest.mark.skipif(
sys.version_info >= (3, 11),
reason="No patching is done on Python >= 3.11",
)
],
),
],
)
def patched(request: SubRequest) -> bool:
return request.param
@pytest.fixture(
params=[pytest.param(False, id="newstyle"), pytest.param(True, id="oldstyle")]
)
def old_argstyle(request: SubRequest) -> bool:
return request.param
def test_exceptionhook(capsys: CaptureFixture) -> None:
try:
raise_excgroup()
except ExceptionGroup as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)
local_lineno = test_exceptionhook.__code__.co_firstlineno
lineno = raise_excgroup.__code__.co_firstlineno
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {local_lineno + 2}, in test_exceptionhook
| raise_excgroup()
| File "{__file__}", line {lineno + 15}, in raise_excgroup
| raise exc
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in raise_excgroup
| raise ValueError("foo")
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in raise_excgroup
| raise RuntimeError("bar")
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)
def test_exceptiongroup_as_cause(capsys: CaptureFixture) -> None:
try:
raise Exception() from ExceptionGroup("", (Exception(),))
except Exception as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)
lineno = test_exceptiongroup_as_cause.__code__.co_firstlineno
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
| {module_prefix}ExceptionGroup: (1 sub-exception)
+-+---------------- 1 ----------------
| Exception
+------------------------------------
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "{__file__}", line {lineno + 2}, in test_exceptiongroup_as_cause
raise Exception() from ExceptionGroup("", (Exception(),))
Exception
"""
)
def test_exceptiongroup_loop(capsys: CaptureFixture) -> None:
e0 = Exception("e0")
eg0 = ExceptionGroup("eg0", (e0,))
eg1 = ExceptionGroup("eg1", (eg0,))
try:
raise eg0 from eg1
except ExceptionGroup as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)
lineno = test_exceptiongroup_loop.__code__.co_firstlineno + 6
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
| {module_prefix}ExceptionGroup: eg1 (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "{__file__}", line {lineno}, in test_exceptiongroup_loop
| raise eg0 from eg1
| {module_prefix}ExceptionGroup: eg0 (1 sub-exception)
+-+---------------- 1 ----------------
| Exception: e0
+------------------------------------
The above exception was the direct cause of the following exception:
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {lineno}, in test_exceptiongroup_loop
| raise eg0 from eg1
| {module_prefix}ExceptionGroup: eg0 (1 sub-exception)
+-+---------------- 1 ----------------
| Exception: e0
+------------------------------------
"""
)
def test_exceptionhook_format_exception_only(capsys: CaptureFixture) -> None:
try:
raise_excgroup()
except ExceptionGroup as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)
local_lineno = test_exceptionhook_format_exception_only.__code__.co_firstlineno
lineno = raise_excgroup.__code__.co_firstlineno
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {local_lineno + 2}, in \
test_exceptionhook_format_exception_only
| raise_excgroup()
| File "{__file__}", line {lineno + 15}, in raise_excgroup
| raise exc
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in raise_excgroup
| raise ValueError("foo")
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in raise_excgroup
| raise RuntimeError("bar")
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)
def test_formatting_syntax_error(capsys: CaptureFixture) -> None:
try:
exec("//serser")
except SyntaxError as exc:
sys.excepthook(type(exc), exc, exc.__traceback__)
if sys.version_info >= (3, 10):
underline = "\n ^^"
elif sys.version_info >= (3, 8):
underline = "\n ^"
else:
underline = "\n ^"
lineno = test_formatting_syntax_error.__code__.co_firstlineno
output = capsys.readouterr().err
assert output == (
f"""\
Traceback (most recent call last):
File "{__file__}", line {lineno + 2}, \
in test_formatting_syntax_error
exec("//serser")
File "", line 1
//serser{underline}
SyntaxError: invalid syntax
"""
)
def test_format_exception(
patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import format_exception
exceptions = []
try:
raise ValueError("foo")
except ValueError as exc:
exceptions.append(exc)
try:
raise RuntimeError("bar")
except RuntimeError as exc:
exc.__notes__ = ["Note from bar handler"]
exceptions.append(exc)
try:
raise_excgroup()
except ExceptionGroup as exc:
if old_argstyle:
lines = format_exception(type(exc), exc, exc.__traceback__)
else:
lines = format_exception(exc)
local_lineno = test_format_exception.__code__.co_firstlineno
lineno = raise_excgroup.__code__.co_firstlineno
assert isinstance(lines, list)
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
assert "".join(lines) == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {local_lineno + 25}, in test_format_exception
| raise_excgroup()
| File "{__file__}", line {lineno + 15}, in raise_excgroup
| raise exc
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in raise_excgroup
| raise ValueError("foo")
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in raise_excgroup
| raise RuntimeError("bar")
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)
def test_format_nested(monkeypatch: MonkeyPatch) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import format_exception
def raise_exc(max_level: int, level: int = 1) -> NoReturn:
if level == max_level:
raise Exception(f"LEVEL_{level}")
else:
try:
raise_exc(max_level, level + 1)
except Exception:
raise Exception(f"LEVEL_{level}")
try:
raise_exc(3)
except Exception as exc:
lines = format_exception(type(exc), exc, exc.__traceback__)
local_lineno = test_format_nested.__code__.co_firstlineno + 20
raise_exc_lineno1 = raise_exc.__code__.co_firstlineno + 2
raise_exc_lineno2 = raise_exc.__code__.co_firstlineno + 5
raise_exc_lineno3 = raise_exc.__code__.co_firstlineno + 7
assert isinstance(lines, list)
assert "".join(lines) == (
f"""\
Traceback (most recent call last):
File "{__file__}", line {raise_exc_lineno2}, in raise_exc
raise_exc(max_level, level + 1)
File "{__file__}", line {raise_exc_lineno1}, in raise_exc
raise Exception(f"LEVEL_{{level}}")
Exception: LEVEL_3
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "{__file__}", line {raise_exc_lineno2}, in raise_exc
raise_exc(max_level, level + 1)
File "{__file__}", line {raise_exc_lineno3}, in raise_exc
raise Exception(f"LEVEL_{{level}}")
Exception: LEVEL_2
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "{__file__}", line {local_lineno}, in test_format_nested
raise_exc(3)
File "{__file__}", line {raise_exc_lineno3}, in raise_exc
raise Exception(f"LEVEL_{{level}}")
Exception: LEVEL_1
"""
)
def test_format_exception_only(
patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import format_exception_only
try:
raise_excgroup()
except ExceptionGroup as exc:
if old_argstyle:
output = format_exception_only(type(exc), exc)
else:
output = format_exception_only(exc)
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
assert output == [
f"{module_prefix}ExceptionGroup: test message (2 sub-exceptions)\n",
"Displays notes attached to the group too\n",
]
def test_print_exception(
patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import print_exception
try:
raise_excgroup()
except ExceptionGroup as exc:
if old_argstyle:
print_exception(type(exc), exc, exc.__traceback__)
else:
print_exception(exc)
local_lineno = test_print_exception.__code__.co_firstlineno
lineno = raise_excgroup.__code__.co_firstlineno
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {local_lineno + 13}, in test_print_exception
| raise_excgroup()
| File "{__file__}", line {lineno + 15}, in raise_excgroup
| raise exc
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in raise_excgroup
| raise ValueError("foo")
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in raise_excgroup
| raise RuntimeError("bar")
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)
def test_print_exc(
patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import print_exc
try:
raise_excgroup()
except ExceptionGroup:
print_exc()
local_lineno = test_print_exc.__code__.co_firstlineno
lineno = raise_excgroup.__code__.co_firstlineno
module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
output = capsys.readouterr().err
assert output == (
f"""\
+ Exception Group Traceback (most recent call last):
| File "{__file__}", line {local_lineno + 13}, in test_print_exc
| raise_excgroup()
| File "{__file__}", line {lineno + 15}, in raise_excgroup
| raise exc
| {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
| Displays notes attached to the group too
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 3}, in raise_excgroup
| raise ValueError("foo")
| ValueError: foo
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "{__file__}", line {lineno + 8}, in raise_excgroup
| raise RuntimeError("bar")
| RuntimeError: bar
| Note from bar handler
+------------------------------------
"""
)
@pytest.mark.skipif(
not hasattr(NameError, "name") or sys.version_info[:2] == (3, 11),
reason="only works if NameError exposes the missing name",
)
def test_nameerror_suggestions(
patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import print_exc
try:
folder
except NameError:
print_exc()
output = capsys.readouterr().err
assert "Did you mean" in output and "'filter'?" in output
@pytest.mark.skipif(
not hasattr(AttributeError, "name") or sys.version_info[:2] == (3, 11),
reason="only works if AttributeError exposes the missing name",
)
def test_nameerror_suggestions_in_group(
patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import print_exception
try:
[].attend
except AttributeError as e:
eg = ExceptionGroup("a", [e])
print_exception(eg)
output = capsys.readouterr().err
assert "Did you mean" in output and "'append'?" in output
def test_bug_suggestions_attributeerror_no_obj(
patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
) -> None:
if not patched:
# Block monkey patching, then force the module to be re-imported
del sys.modules["traceback"]
del sys.modules["exceptiongroup"]
del sys.modules["exceptiongroup._formatting"]
monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
from exceptiongroup import print_exception
class NamedAttributeError(AttributeError):
def __init__(self, name: str) -> None:
self.name: str = name
try:
raise NamedAttributeError(name="mykey")
except AttributeError as e:
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)
exceptiongroup-1.2.0/tests/test_suppress.py 0000664 0000000 0000000 00000000672 14527066330 0021237 0 ustar 00root root 0000000 0000000 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)