From 163201e56b0236b166b21373590cb3765087dc11 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 12 Aug 2022 16:28:46 +0200
Subject: [PATCH] Typed ExceptionGroupTypes and changed to BaseExceptionGroup,
fixed exceptionchain (excinfo->excinfo_, set reprcrash. Extended tests,
though they're wip.
---
AUTHORS | 1 +
src/_pytest/_code/code.py | 41 +++++++-------
testing/code/test_excinfo.py | 106 ++++++++++++++++++++++++++---------
tox.ini | 5 +-
4 files changed, 107 insertions(+), 46 deletions(-)
diff --git a/AUTHORS b/AUTHORS
index e797f2146..ca2872f32 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -168,6 +168,7 @@ Jeff Rackauckas
Jeff Widman
Jenni Rinker
John Eddie Ayson
+John Litborn
John Towler
Jon Parise
Jon Sonesen
diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py
index b1991f1aa..0c4041e84 100644
--- a/src/_pytest/_code/code.py
+++ b/src/_pytest/_code/code.py
@@ -56,17 +56,18 @@ if TYPE_CHECKING:
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
-ExceptionGroupTypes: tuple = () # type: ignore
-try:
- ExceptionGroupTypes += (ExceptionGroup,) # type: ignore
-except NameError:
- pass # Is missing for `python<3.10`
+ExceptionGroupTypes: Tuple[Type[BaseException], ...] = ()
+
+if sys.version_info >= (3, 11):
+ ExceptionGroupTypes = (BaseExceptionGroup,) # type: ignore # noqa: F821
try:
import exceptiongroup
- ExceptionGroupTypes += (exceptiongroup.ExceptionGroup,)
+ ExceptionGroupTypes += (exceptiongroup.BaseExceptionGroup,)
except ModuleNotFoundError:
- pass # No backport is installed
+ # no backport installed - if <3.11 that means programs can't raise exceptiongroups
+ # so we don't need to handle it
+ pass
class Code:
@@ -935,20 +936,22 @@ class FormattedExcinfo:
seen: Set[int] = set()
while e is not None and id(e) not in seen:
seen.add(id(e))
- if isinstance(e, ExceptionGroupTypes):
- reprtraceback: Union[
- ReprTracebackNative, ReprTraceback
- ] = ReprTracebackNative(
- traceback.format_exception(
- type(excinfo.value),
- excinfo.value,
- excinfo.traceback[0]._rawentry,
+ if excinfo_:
+ if isinstance(e, tuple(ExceptionGroupTypes)):
+ reprtraceback: Union[
+ ReprTracebackNative, ReprTraceback
+ ] = ReprTracebackNative(
+ traceback.format_exception(
+ type(excinfo_.value),
+ excinfo_.value,
+ excinfo_.traceback[0]._rawentry,
+ )
)
+ else:
+ reprtraceback = self.repr_traceback(excinfo_)
+ reprcrash: Optional[ReprFileLocation] = (
+ excinfo_._getreprcrash() if self.style != "value" else None
)
- reprcrash: Optional[ReprFileLocation] = None
- elif excinfo_:
- reprtraceback = self.repr_traceback(excinfo_)
- reprcrash = excinfo_._getreprcrash() if self.style != "value" else None
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py
index 78f777718..2a032b021 100644
--- a/testing/code/test_excinfo.py
+++ b/testing/code/test_excinfo.py
@@ -11,6 +11,11 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
+try:
+ import exceptiongroup # noqa (referred to in strings)
+except ModuleNotFoundError:
+ pass
+
import _pytest
import pytest
from _pytest._code.code import ExceptionChainRepr
@@ -23,7 +28,6 @@ from _pytest.pathlib import import_path
from _pytest.pytester import LineMatcher
from _pytest.pytester import Pytester
-
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
@@ -1472,32 +1476,84 @@ def test_no_recursion_index_on_recursion_error():
assert "maximum recursion" in str(excinfo.getrepr())
-def test_exceptiongroup(pytester: Pytester) -> None:
- pytester.makepyfile(
- """
- def f(): raise ValueError("From f()")
- def g(): raise RuntimeError("From g()")
+def _exceptiongroup_common(
+ pytester: Pytester,
+ outer_chain: str,
+ inner_chain: str,
+ native: bool,
+) -> None:
+ pre = "exceptiongroup." if not native else ""
+ pre2 = pre if sys.version_info < (3, 11) else ""
+ filestr = f"""
+ {"import exceptiongroup" if not native else ""}
+ import pytest
- def main():
- excs = []
- for callback in [f, g]:
- try:
- callback()
- except Exception as err:
- excs.append(err)
- if excs:
- raise ExceptionGroup("Oops", excs)
+ def f(): raise ValueError("From f()")
+ def g(): raise BaseException("From g()")
- def test():
- main()
+ def inner(inner_chain):
+ excs = []
+ for callback in [f, g]:
+ try:
+ callback()
+ except BaseException as err:
+ excs.append(err)
+ if excs:
+ if inner_chain == "none":
+ raise {pre}BaseExceptionGroup("Oops", excs)
+ try:
+ raise SyntaxError()
+ except SyntaxError as e:
+ if inner_chain == "from":
+ raise {pre}BaseExceptionGroup("Oops", excs) from e
+ else:
+ raise {pre}BaseExceptionGroup("Oops", excs)
+
+ def outer(outer_chain, inner_chain):
+ try:
+ inner(inner_chain)
+ except {pre2}BaseExceptionGroup as e:
+ if outer_chain == "none":
+ raise
+ if outer_chain == "from":
+ raise IndexError() from e
+ else:
+ raise IndexError()
+
+
+ def test():
+ outer("{outer_chain}", "{inner_chain}")
"""
- )
+ pytester.makepyfile(test_excgroup=filestr)
result = pytester.runpytest()
- assert result.ret != 0
-
- match = [
- r" | ExceptionGroup: Oops (2 sub-exceptions)",
- r" | ValueError: From f()",
- r" | RuntimeError: From g()",
+ match_lines = [
+ rf" \| {pre2}BaseExceptionGroup: Oops \(2 sub-exceptions\)",
+ r" \| ValueError: From f\(\)",
+ r" \| BaseException: From g\(\)",
+ r"=* short test summary info =*",
]
- result.stdout.re_match_lines(match)
+ if outer_chain in ("another", "from"):
+ match_lines.append(r"FAILED test_excgroup.py::test - IndexError")
+ else:
+ match_lines.append(
+ rf"FAILED test_excgroup.py::test - {pre2}BaseExceptionGroup: Oops \(2 su.*"
+ )
+ result.stdout.re_match_lines(match_lines)
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 11), reason="Native ExceptionGroup not implemented"
+)
+@pytest.mark.parametrize("outer_chain", ["none", "from", "another"])
+@pytest.mark.parametrize("inner_chain", ["none", "from", "another"])
+def test_native_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
+ _exceptiongroup_common(pytester, outer_chain, inner_chain, native=True)
+
+
+@pytest.mark.skipif(
+ "exceptiongroup" not in sys.modules, reason="exceptiongroup not installed"
+)
+@pytest.mark.parametrize("outer_chain", ["none", "from", "another"])
+@pytest.mark.parametrize("inner_chain", ["none", "from", "another"])
+def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
+ _exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
diff --git a/tox.ini b/tox.ini
index fd2ff1ba1..a80a76782 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,8 +9,9 @@ envlist =
py39
py310
py311
+ py311-exceptiongroup
pypy3
- py37-{pexpect,xdist,unittestextras,numpy,pluggymain}
+ py37-{pexpect,xdist,unittestextras,numpy,pluggymain,exceptiongroup}
doctesting
plugins
py37-freeze
@@ -46,7 +47,7 @@ setenv =
extras = testing
deps =
doctesting: PyYAML
- exceptiongroup: exceptiongroup>=1.0.0
+ exceptiongroup: exceptiongroup>=1.0.0rc8
numpy: numpy>=1.19.4
pexpect: pexpect>=4.8.0
pluggymain: pluggy @ git+https://github.com/pytest-dev/pluggy.git