pax_global_header00006660000000000000000000000064145270663300014520gustar00rootroot0000000000000052 comment=4e2df2190d3c61a2e7940d6908b32658dfadab3f exceptiongroup-1.2.0/000077500000000000000000000000001452706633000145735ustar00rootroot00000000000000exceptiongroup-1.2.0/.github/000077500000000000000000000000001452706633000161335ustar00rootroot00000000000000exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001452706633000203165ustar00rootroot00000000000000exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000032651452706633000233600ustar00rootroot00000000000000name: 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.yml000066400000000000000000000000341452706633000223030ustar00rootroot00000000000000blank_issues_enabled: false exceptiongroup-1.2.0/.github/ISSUE_TEMPLATE/features_request.yaml000066400000000000000000000020341452706633000245670ustar00rootroot00000000000000name: 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/000077500000000000000000000000001452706633000201705ustar00rootroot00000000000000exceptiongroup-1.2.0/.github/workflows/publish.yml000066400000000000000000000025771452706633000223740ustar00rootroot00000000000000name: 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.yml000066400000000000000000000025711452706633000216770ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000002561452706633000165660ustar00rootroot00000000000000*.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.yaml000066400000000000000000000011231452706633000210510ustar00rootroot00000000000000repos: - 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.rst000066400000000000000000000105411452706633000163760ustar00rootroot00000000000000Version 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/LICENSE000066400000000000000000000071701452706633000156050ustar00rootroot00000000000000The 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.rst000066400000000000000000000132441452706633000162660ustar00rootroot00000000000000.. 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.toml000066400000000000000000000042101452706633000175040ustar00rootroot00000000000000[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/000077500000000000000000000000001452706633000153625ustar00rootroot00000000000000exceptiongroup-1.2.0/src/exceptiongroup/000077500000000000000000000000001452706633000204355ustar00rootroot00000000000000exceptiongroup-1.2.0/src/exceptiongroup/__init__.py000066400000000000000000000020311452706633000225420ustar00rootroot00000000000000__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.py000066400000000000000000000110751452706633000222340ustar00rootroot00000000000000from __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.py000066400000000000000000000254021452706633000233320ustar00rootroot00000000000000from __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.py000066400000000000000000000510151452706633000233220ustar00rootroot00000000000000# 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.py000066400000000000000000000024721452706633000230370ustar00rootroot00000000000000import 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.typed000066400000000000000000000000001452706633000221220ustar00rootroot00000000000000exceptiongroup-1.2.0/tests/000077500000000000000000000000001452706633000157355ustar00rootroot00000000000000exceptiongroup-1.2.0/tests/__init__.py000066400000000000000000000000001452706633000200340ustar00rootroot00000000000000exceptiongroup-1.2.0/tests/apport_excepthook.py000066400000000000000000000007371452706633000220540ustar00rootroot00000000000000# 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.py000066400000000000000000000001241452706633000201310ustar00rootroot00000000000000import sys if sys.version_info < (3, 11): collect_ignore_glob = ["*_py311.py"] exceptiongroup-1.2.0/tests/test_apport_monkeypatching.py000066400000000000000000000036051452706633000237570ustar00rootroot00000000000000from __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.py000066400000000000000000000157731452706633000204450ustar00rootroot00000000000000import 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.py000066400000000000000000000135061452706633000213720ustar00rootroot00000000000000import 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.py000066400000000000000000000737131452706633000215420ustar00rootroot00000000000000# 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.py000066400000000000000000000435201452706633000215240ustar00rootroot00000000000000import 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.py000066400000000000000000000006721452706633000212370ustar00rootroot00000000000000import 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)