Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-exceptiongroup for 
openSUSE:Factory checked in at 2023-01-06 17:04:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-exceptiongroup (Old)
 and      /work/SRC/openSUSE:Factory/.python-exceptiongroup.new.1563 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-exceptiongroup"

Fri Jan  6 17:04:40 2023 rev:3 rq:1045050 version:1.1.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-exceptiongroup/python-exceptiongroup.changes  
    2022-10-28 19:28:55.130447339 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-exceptiongroup.new.1563/python-exceptiongroup.changes
    2023-01-06 17:05:06.836032514 +0100
@@ -1,0 +2,32 @@
+Fri Dec 23 15:24:56 UTC 2022 - Ben Greiner <c...@bnavigator.de>
+
+- Update to 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)
+- Fix multibuild
+
+-------------------------------------------------------------------
+Mon Dec 19 10:08:40 UTC 2022 - Dirk Müller <dmuel...@suse.com>
+
+- split tests into multibuild to solve cycle with pytest
+
+-------------------------------------------------------------------
+Thu Dec  1 09:42:08 UTC 2022 - Johannes Kastl <ka...@b1-systems.de>
+
+- update to 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)
+- update to 1.0.3:
+  * Fixed monkey patching breaking suggestions (on a NameError or 
AttributeError) on Python 3.10 (#41; PR by Carl Friedrich Bolz-Tereick)
+- update to 1.0.2:
+  * Updated type annotations to match the ones in typeshed
+- update to 1.0.1:
+  * Fixed formatted traceback missing exceptions beyond 2 nesting levels of 
__context__ or __cause__
+- update to 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
+
+-------------------------------------------------------------------

Old:
----
  exceptiongroup-1.0.0rc9-gh.tar.gz

New:
----
  _multibuild
  exceptiongroup-1.1.0-gh.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-exceptiongroup.spec ++++++
--- /var/tmp/diff_new_pack.1dzVh6/_old  2023-01-06 17:05:07.288035053 +0100
+++ /var/tmp/diff_new_pack.1dzVh6/_new  2023-01-06 17:05:07.292035075 +0100
@@ -1,5 +1,5 @@
 #
-# spec file for package python-exceptiongroup
+# spec file
 #
 # Copyright (c) 2022 SUSE LLC
 #
@@ -16,18 +16,31 @@
 #
 
 
-%define pyversion 1.0.0rc9
-Name:           python-exceptiongroup
-Version:        1.0.0~rc9
+# This is not only because of dependency of testsuite, but mostly
+# because of cyclical dependencies between exceptiongroup and pytest.
+%global flavor @BUILD_FLAVOR@%{nil}
+%if "%{flavor}" == "test"
+%bcond_without test
+%define psuffix -test
+%else
+%bcond_with test
+%define psuffix %{nil}
+%endif
+
+Name:           python-exceptiongroup%{psuffix}
+Version:        1.1.0
 Release:        0
 Summary:        Backport of PEP 654 (exception groups)
 License:        MIT AND Python-2.0
 URL:            https://github.com/agronholm/exceptiongroup
-Source:         
https://github.com/agronholm/exceptiongroup/archive/refs/tags/%{pyversion}.tar.gz#/exceptiongroup-%{pyversion}-gh.tar.gz
+Source:         
https://github.com/agronholm/exceptiongroup/archive/refs/tags/%{version}.tar.gz#/exceptiongroup-%{version}-gh.tar.gz
 BuildRequires:  %{python_module base >= 3.7}
 BuildRequires:  %{python_module flit-scm}
 BuildRequires:  %{python_module pip}
+%if %{with test}
+BuildRequires:  %{python_module exceptiongroup = %{version}}
 BuildRequires:  %{python_module pytest}
+%endif
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 BuildArch:      noarch
@@ -61,23 +74,29 @@
 and the exception hook won't be installed.
 
 %prep
-%setup -q -n exceptiongroup-%{pyversion}
+%setup -q -n exceptiongroup-%{version}
 
+%if !%{with test}
 %build
-export SETUPTOOLS_SCM_PRETEND_VERSION=%{pyversion}
+export SETUPTOOLS_SCM_PRETEND_VERSION=%{version}
 %pyproject_wheel
 
 %install
 %pyproject_install
 %python_expand %fdupes %{buildroot}%{$python_sitelib}
+%endif
 
+%if %{with test}
 %check
 %pytest
+%endif
 
+%if !%{with test}
 %files %{python_files}
 %doc README.rst
 %license LICENSE
 %{python_sitelib}/exceptiongroup
-%{python_sitelib}/exceptiongroup-%{pyversion}*-info
+%{python_sitelib}/exceptiongroup-%{version}.dist-info
+%endif
 
 %changelog

++++++ _multibuild ++++++
<multibuild>
  <package>test</package>
</multibuild>

++++++ exceptiongroup-1.0.0rc9-gh.tar.gz -> exceptiongroup-1.1.0-gh.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/exceptiongroup-1.0.0rc9/.github/workflows/publish.yml 
new/exceptiongroup-1.1.0/.github/workflows/publish.yml
--- old/exceptiongroup-1.0.0rc9/.github/workflows/publish.yml   2022-08-28 
14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/.github/workflows/publish.yml      2022-12-23 
10:04:49.000000000 +0100
@@ -12,9 +12,9 @@
   publish:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.x
     - name: Install dependencies
@@ -22,7 +22,6 @@
     - name: Create packages
       run: python -m build -s -w .
     - name: Upload packages
-      uses: pypa/gh-action-pypi-publish@master
+      uses: pypa/gh-action-pypi-publish@release/v1
       with:
-        user: __token__
         password: ${{ secrets.pypi_token }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/.github/workflows/test.yml 
new/exceptiongroup-1.1.0/.github/workflows/test.yml
--- old/exceptiongroup-1.0.0rc9/.github/workflows/test.yml      2022-08-28 
14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/.github/workflows/test.yml 2022-12-23 
10:04:49.000000000 +0100
@@ -9,17 +9,15 @@
   pyright:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.x
-    - uses: actions/cache@v2
+    - uses: actions/cache@v3
       with:
         path: ~/.cache/pip
         key: pip-pyright
-    - name: Work around a problem with pip 22.1
-      run: pip install "pip>=22.1.1"
     - name: Install dependencies
       run: pip install -e . pyright
     - name: Run pyright
@@ -29,22 +27,20 @@
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev", pypy-3.8]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy-3.8]
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
-    - uses: actions/cache@v2
+    - uses: actions/cache@v3
       with:
         path: ~/.cache/pip
         key: pip-test-${{ matrix.python-version }}-${{ matrix.os }}
-    - name: Work around a problem with pip 22.1
-      run: pip install "pip>=22.1.1"
     - name: Install dependencies
-      run: pip install .[test] coverage[toml] coveralls
+      run: pip install .[test] coveralls coverage[toml]
     - name: Test with pytest
       run: coverage run -m pytest
     - name: Upload Coverage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/.pre-commit-config.yaml 
new/exceptiongroup-1.1.0/.pre-commit-config.yaml
--- old/exceptiongroup-1.0.0rc9/.pre-commit-config.yaml 2022-08-28 
14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/.pre-commit-config.yaml    2022-12-23 
10:04:49.000000000 +0100
@@ -1,6 +1,6 @@
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.3.0
+  rev: v4.4.0
   hooks:
   - id: check-added-large-files
   - id: check-case-conflict
@@ -16,24 +16,24 @@
   - id: trailing-whitespace
 
 - repo: https://github.com/pycqa/isort
-  rev: 5.10.1
+  rev: v5.11.3
   hooks:
   - id: isort
 
 - repo: https://github.com/asottile/pyupgrade
-  rev: v2.37.3
+  rev: v3.3.1
   hooks:
   - id: pyupgrade
     args: ["--py37-plus", "--keep-runtime-typing"]
 
 - repo: https://github.com/psf/black
-  rev: 22.6.0
+  rev: 22.12.0
   hooks:
   - id: black
     exclude: "tests/test_catch_py311.py"
 
 - repo: https://github.com/csachs/pyproject-flake8
-  rev: v0.0.1a5
+  rev: v6.0.0.post1
   hooks:
   - id: pyproject-flake8
     additional_dependencies: [flake8-bugbear]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/CHANGES.rst 
new/exceptiongroup-1.1.0/CHANGES.rst
--- old/exceptiongroup-1.0.0rc9/CHANGES.rst     2022-08-28 14:49:43.000000000 
+0200
+++ new/exceptiongroup-1.1.0/CHANGES.rst        2022-12-23 10:04:49.000000000 
+0100
@@ -3,6 +3,43 @@
 
 This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
 
+**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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/pyproject.toml 
new/exceptiongroup-1.1.0/pyproject.toml
--- old/exceptiongroup-1.0.0rc9/pyproject.toml  2022-08-28 14:49:43.000000000 
+0200
+++ new/exceptiongroup-1.1.0/pyproject.toml     2022-12-23 10:04:49.000000000 
+0100
@@ -71,7 +71,7 @@
 [tox]
 envlist = py37, py38, py39, py310, py311, pypy3
 skip_missing_interpreters = true
-isolated_build = true
+minversion = 4.0
 
 [testenv]
 extras = test
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/exceptiongroup-1.0.0rc9/src/exceptiongroup/_exceptions.py 
new/exceptiongroup-1.1.0/src/exceptiongroup/_exceptions.py
--- old/exceptiongroup-1.0.0rc9/src/exceptiongroup/_exceptions.py       
2022-08-28 14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/src/exceptiongroup/_exceptions.py  2022-12-23 
10:04:49.000000000 +0100
@@ -1,18 +1,17 @@
 from __future__ import annotations
 
-from collections.abc import Sequence
+from collections.abc import Callable, Sequence
 from functools import partial
 from inspect import getmro, isclass
-from typing import Any, Callable, Generic, Tuple, Type, TypeVar, Union, cast
+from typing import TYPE_CHECKING, Generic, Type, TypeVar, cast, overload
 
-T = TypeVar("T", bound="BaseExceptionGroup")
-EBase = TypeVar("EBase", bound=BaseException)
-E = TypeVar("E", bound=Exception)
-_SplitCondition = Union[
-    Type[EBase],
-    Tuple[Type[EBase], ...],
-    Callable[[EBase], bool],
-]
+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(
@@ -25,7 +24,11 @@
     return False
 
 
-def get_condition_filter(condition: _SplitCondition) -> 
Callable[[BaseException], bool]:
+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
     ):
@@ -34,17 +37,17 @@
         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)
+        return cast("Callable[[BaseException], bool]", condition)
 
     raise TypeError("expected a function, exception type or tuple of exception 
types")
 
 
-class BaseExceptionGroup(BaseException, Generic[EBase]):
+class BaseExceptionGroup(BaseException, Generic[_BaseExceptionT_co]):
     """A combination of multiple unrelated exceptions."""
 
     def __new__(
-        cls, __message: str, __exceptions: Sequence[EBase]
-    ) -> BaseExceptionGroup[EBase] | ExceptionGroup[E]:
+        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):
@@ -64,20 +67,32 @@
             if all(isinstance(exc, Exception) for exc in __exceptions):
                 cls = ExceptionGroup
 
-        return super().__new__(cls, __message, __exceptions)
-
-    def __init__(self, __message: str, __exceptions: Sequence[EBase], *args: 
Any):
-        super().__init__(__message, __exceptions, *args)
-        self._message = __message
-        self._exceptions = __exceptions
+        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):
+    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__ = []
+            self.__notes__: list[str] = []
+
         self.__notes__.append(note)
 
     @property
@@ -85,10 +100,29 @@
         return self._message
 
     @property
-    def exceptions(self) -> tuple[EBase, ...]:
+    def exceptions(
+        self,
+    ) -> tuple[_BaseExceptionT_co | BaseExceptionGroup[_BaseExceptionT_co], 
...]:
         return tuple(self._exceptions)
 
-    def subgroup(self: T, __condition: _SplitCondition[EBase]) -> T | None:
+    @overload
+    def subgroup(
+        self, __condition: type[_BaseExceptionT] | 
tuple[type[_BaseExceptionT], ...]
+    ) -> BaseExceptionGroup[_BaseExceptionT] | None:
+        ...
+
+    @overload
+    def subgroup(
+        self: Self, __condition: Callable[[_BaseExceptionT_co], bool]
+    ) -> Self | None:
+        ...
+
+    def subgroup(
+        self: Self,
+        __condition: type[_BaseExceptionT]
+        | tuple[type[_BaseExceptionT], ...]
+        | Callable[[_BaseExceptionT_co], bool],
+    ) -> BaseExceptionGroup[_BaseExceptionT] | Self | None:
         condition = get_condition_filter(__condition)
         modified = False
         if condition(self):
@@ -97,7 +131,7 @@
         exceptions: list[BaseException] = []
         for exc in self.exceptions:
             if isinstance(exc, BaseExceptionGroup):
-                subgroup = exc.subgroup(condition)
+                subgroup = exc.subgroup(__condition)
                 if subgroup is not None:
                     exceptions.append(subgroup)
 
@@ -119,9 +153,27 @@
         else:
             return None
 
+    @overload
+    def split(
+        self: Self,
+        __condition: type[_BaseExceptionT] | tuple[type[_BaseExceptionT], ...],
+    ) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None]:
+        ...
+
+    @overload
+    def split(
+        self: Self, __condition: Callable[[_BaseExceptionT_co], bool]
+    ) -> tuple[Self | None, Self | None]:
+        ...
+
     def split(
-        self: T, __condition: _SplitCondition[EBase]
-    ) -> tuple[T | None, T | None]:
+        self: Self,
+        __condition: type[_BaseExceptionT]
+        | tuple[type[_BaseExceptionT], ...]
+        | Callable[[_BaseExceptionT_co], bool],
+    ) -> tuple[BaseExceptionGroup[_BaseExceptionT] | None, Self | None] | 
tuple[
+        Self | None, Self | None
+    ]:
         condition = get_condition_filter(__condition)
         if condition(self):
             return self, None
@@ -141,14 +193,14 @@
             else:
                 nonmatching_exceptions.append(exc)
 
-        matching_group: T | None = None
+        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: T | None = None
+        nonmatching_group: Self | None = None
         if nonmatching_exceptions:
             nonmatching_group = self.derive(nonmatching_exceptions)
             nonmatching_group.__cause__ = self.__cause__
@@ -157,11 +209,12 @@
 
         return matching_group, nonmatching_group
 
-    def derive(self: T, __excs: Sequence[EBase]) -> T:
+    def derive(self: Self, __excs: Sequence[_BaseExceptionT_co]) -> Self:
         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:
@@ -172,12 +225,56 @@
         return f"{self.__class__.__name__}({self.message!r}, 
{self._exceptions!r})"
 
 
-class ExceptionGroup(BaseExceptionGroup[E], Exception, Generic[E]):
-    def __new__(cls, __message: str, __exceptions: Sequence[E]) -> 
ExceptionGroup[E]:
-        instance: ExceptionGroup[E] = super().__new__(cls, __message, 
__exceptions)
-        if cls is ExceptionGroup:
-            for exc in __exceptions:
-                if not isinstance(exc, Exception):
-                    raise TypeError("Cannot nest BaseExceptions in an 
ExceptionGroup")
+class ExceptionGroup(BaseExceptionGroup[_ExceptionT_co], Exception):
+    def __new__(cls, __message: str, __exceptions: Sequence[_ExceptionT_co]) 
-> Self:
+        return super().__new__(cls, __message, __exceptions)
 
-        return instance
+    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: Self, __condition: Callable[[_ExceptionT_co], bool]
+        ) -> Self | None:
+            ...
+
+        def subgroup(
+            self: Self,
+            __condition: type[_ExceptionT]
+            | tuple[type[_ExceptionT], ...]
+            | Callable[[_ExceptionT_co], bool],
+        ) -> ExceptionGroup[_ExceptionT] | Self | None:
+            return super().subgroup(__condition)
+
+        @overload  # type: ignore[override]
+        def split(
+            self: Self, __condition: type[_ExceptionT] | 
tuple[type[_ExceptionT], ...]
+        ) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None]:
+            ...
+
+        @overload
+        def split(
+            self: Self, __condition: Callable[[_ExceptionT_co], bool]
+        ) -> tuple[Self | None, Self | None]:
+            ...
+
+        def split(
+            self: Self,
+            __condition: type[_ExceptionT]
+            | tuple[type[_ExceptionT], ...]
+            | Callable[[_ExceptionT_co], bool],
+        ) -> tuple[ExceptionGroup[_ExceptionT] | None, Self | None] | tuple[
+            Self | None, Self | None
+        ]:
+            return super().split(__condition)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/exceptiongroup-1.0.0rc9/src/exceptiongroup/_formatting.py 
new/exceptiongroup-1.1.0/src/exceptiongroup/_formatting.py
--- old/exceptiongroup-1.0.0rc9/src/exceptiongroup/_formatting.py       
2022-08-28 14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/src/exceptiongroup/_formatting.py  2022-12-23 
10:04:49.000000000 +0100
@@ -76,7 +76,7 @@
         self,
         exc_type: type[BaseException],
         exc_value: BaseException,
-        exc_traceback: TracebackType,
+        exc_traceback: TracebackType | None,
         *,
         limit: int | None = None,
         lookup_lines: bool = True,
@@ -88,48 +88,123 @@
         if sys.version_info >= (3, 10):
             kwargs["compact"] = compact
 
-        # Capture the original exception and its cause and context as
-        # TracebackExceptions
-        traceback_exception_original_init(
-            self,
-            exc_type,
-            exc_value,
-            exc_traceback,
+        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,
-            _seen=_seen,
-            **kwargs,
         )
+        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")
+        self.__notes__ = getattr(exc_value, "__notes__", None)
+
+        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
 
-        seen_was_none = _seen is None
+        self.__suppress_context__ = (
+            exc_value.__suppress_context__ if exc_value is not None else False
+        )
 
-        if _seen is None:
-            _seen = set()
+        # 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 isinstance(exc_value, BaseExceptionGroup):
-            embedded = []
-            for exc in exc_value.exceptions:
-                if id(exc) not in _seen:
-                    embedded.append(
-                        PatchedTracebackException(
+                # 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,
-                            # copy the set of _seen exceptions so that 
duplicates
-                            # shared between sub-exceptions are not omitted
-                            _seen=None if seen_was_none else set(_seen),
+                            _seen=_seen,
                         )
-                    )
-            self.exceptions = embedded
-            self.msg = exc_value.message
-        else:
-            self.exceptions = None
-        self.__notes__ = getattr(exc_value, "__notes__", ())
+                        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:
@@ -256,7 +331,6 @@
             yield _safe_string(self.__notes__, "__notes__", func=repr)
 
 
-traceback_exception_original_init = traceback.TracebackException.__init__
 traceback_exception_original_format = traceback.TracebackException.format
 traceback_exception_original_format_exception_only = (
     traceback.TracebackException.format_exception_only
@@ -280,7 +354,9 @@
 @singledispatch
 def format_exception_only(__exc: BaseException) -> List[str]:
     return list(
-        PatchedTracebackException(type(__exc), __exc, 
None).format_exception_only()
+        PatchedTracebackException(
+            type(__exc), __exc, None, compact=True
+        ).format_exception_only()
     )
 
 
@@ -297,7 +373,7 @@
 ) -> List[str]:
     return list(
         PatchedTracebackException(
-            type(__exc), __exc, __exc.__traceback__, limit=limit
+            type(__exc), __exc, __exc.__traceback__, limit=limit, compact=True
         ).format(chain=chain)
     )
 
@@ -348,3 +424,131 @@
 ) -> 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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/tests/test_catch_py311.py 
new/exceptiongroup-1.1.0/tests/test_catch_py311.py
--- old/exceptiongroup-1.0.0rc9/tests/test_catch_py311.py       2022-08-28 
14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/tests/test_catch_py311.py  2022-12-23 
10:04:49.000000000 +0100
@@ -1,6 +1,6 @@
 import pytest
 
-from exceptiongroup import BaseExceptionGroup, ExceptionGroup, catch
+from exceptiongroup import ExceptionGroup
 
 
 def test_catch_ungrouped():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/tests/test_exceptions.py 
new/exceptiongroup-1.1.0/tests/test_exceptions.py
--- old/exceptiongroup-1.0.0rc9/tests/test_exceptions.py        2022-08-28 
14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/tests/test_exceptions.py   2022-12-23 
10:04:49.000000000 +0100
@@ -3,6 +3,8 @@
 import sys
 import unittest
 
+import pytest
+
 from exceptiongroup import BaseExceptionGroup, ExceptionGroup
 
 
@@ -90,19 +92,35 @@
         beg = BaseExceptionGroup("beg", [ValueError(1), KeyboardInterrupt(2)])
         self.assertIs(type(beg), BaseExceptionGroup)
 
-    def test_EG_subclass_wraps_anything(self):
+    def test_EG_subclass_wraps_non_base_exceptions(self):
         class MyEG(ExceptionGroup):
             pass
 
         self.assertIs(type(MyEG("eg", [ValueError(12), TypeError(42)])), MyEG)
-        self.assertIs(type(MyEG("eg", [ValueError(12), 
KeyboardInterrupt(42)])), MyEG)
 
-    def test_BEG_subclass_wraps_anything(self):
-        class MyBEG(BaseExceptionGroup):
+    @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
 
-        self.assertIs(type(MyBEG("eg", [ValueError(12), TypeError(42)])), 
MyBEG)
-        self.assertIs(type(MyBEG("eg", [ValueError(12), 
KeyboardInterrupt(42)])), MyBEG)
+        msg = "Cannot nest BaseExceptions in 'MyEG'"
+        with self.assertRaisesRegex(TypeError, msg):
+            MyEG("eg", [ValueError(12), KeyboardInterrupt(42)])
 
 
 def create_simple_eg():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/exceptiongroup-1.0.0rc9/tests/test_formatting.py 
new/exceptiongroup-1.1.0/tests/test_formatting.py
--- old/exceptiongroup-1.0.0rc9/tests/test_formatting.py        2022-08-28 
14:49:43.000000000 +0200
+++ new/exceptiongroup-1.1.0/tests/test_formatting.py   2022-12-23 
10:04:49.000000000 +0100
@@ -88,6 +88,70 @@
     )
 
 
+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()
@@ -213,6 +277,65 @@
         )
 
 
+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:
@@ -332,3 +455,76 @@
     +------------------------------------
 """
         )
+
+
+@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

Reply via email to