Skip to content

Commit 1eef471

Browse files
committed
code: add ExceptionInfo.from_exception
The old-style `sys.exc_info()` triplet is redundant nowadays with `(type(exc), exc, exc.__traceback__)`, and is beginning to get soft-deprecated in Python 3.12. Add a nicer API to ExceptionInfo which takes just the exc instead of the triplet. There are already a few internal uses which benefit.
1 parent 61f7c27 commit 1eef471

File tree

3 files changed

+43
-18
lines changed

3 files changed

+43
-18
lines changed

src/_pytest/_code/code.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -469,12 +469,20 @@ def __init__(
469469
self._traceback = traceback
470470

471471
@classmethod
472-
def from_exc_info(
472+
def from_exception(
473473
cls,
474-
exc_info: Tuple[Type[E], E, TracebackType],
474+
# Ignoring error: "Cannot use a covariant type variable as a parameter".
475+
# This is OK to ignore because this class is (conceptually) readonly.
476+
# See https://github.com/python/mypy/issues/7049.
477+
exception: E, # type: ignore[misc]
475478
exprinfo: Optional[str] = None,
476479
) -> "ExceptionInfo[E]":
477-
"""Return an ExceptionInfo for an existing exc_info tuple.
480+
"""Return an ExceptionInfo for an existing exception.
481+
482+
The exception must have a non-``None`` ``__traceback__`` attribute,
483+
otherwise this function fails with an assertion error. This means that
484+
the exception must have been raised, or added a traceback with the
485+
:func:`BaseException.with_traceback()` method.
478486
479487
.. warning::
480488
@@ -484,7 +492,22 @@ def from_exc_info(
484492
A text string helping to determine if we should strip
485493
``AssertionError`` from the output. Defaults to the exception
486494
message/``__str__()``.
495+
496+
.. versionadded:: 7.4
487497
"""
498+
assert (
499+
exception.__traceback__
500+
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
501+
exc_info = (type(exception), exception, exception.__traceback__)
502+
return cls.from_exc_info(exc_info, exprinfo)
503+
504+
@classmethod
505+
def from_exc_info(
506+
cls,
507+
exc_info: Tuple[Type[E], E, TracebackType],
508+
exprinfo: Optional[str] = None,
509+
) -> "ExceptionInfo[E]":
510+
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
488511
_striptext = ""
489512
if exprinfo is None and isinstance(exc_info[1], AssertionError):
490513
exprinfo = getattr(exc_info[1], "msg", None)
@@ -965,21 +988,13 @@ def repr_excinfo(
965988

966989
if e.__cause__ is not None and self.chain:
967990
e = e.__cause__
968-
excinfo_ = (
969-
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
970-
if e.__traceback__
971-
else None
972-
)
991+
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
973992
descr = "The above exception was the direct cause of the following exception:"
974993
elif (
975994
e.__context__ is not None and not e.__suppress_context__ and self.chain
976995
):
977996
e = e.__context__
978-
excinfo_ = (
979-
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
980-
if e.__traceback__
981-
else None
982-
)
997+
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
983998
descr = "During handling of the above exception, another exception occurred:"
984999
else:
9851000
e = None

src/_pytest/python_api.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -950,11 +950,7 @@ def raises( # noqa: F811
950950
try:
951951
func(*args[1:], **kwargs)
952952
except expected_exception as e:
953-
# We just caught the exception - there is a traceback.
954-
assert e.__traceback__ is not None
955-
return _pytest._code.ExceptionInfo.from_exc_info(
956-
(type(e), e, e.__traceback__)
957-
)
953+
return _pytest._code.ExceptionInfo.from_exception(e)
958954
fail(message)
959955

960956

testing/code/test_excinfo.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
5353
assert info.type == ValueError
5454

5555

56+
def test_excinfo_from_exception_simple() -> None:
57+
try:
58+
raise ValueError
59+
except ValueError as e:
60+
assert e.__traceback__ is not None
61+
info = _pytest._code.ExceptionInfo.from_exception(e)
62+
assert info.type == ValueError
63+
64+
65+
def test_excinfo_from_exception_missing_traceback_assertion() -> None:
66+
with pytest.raises(AssertionError, match=r'must have.*__traceback__'):
67+
_pytest._code.ExceptionInfo.from_exception(ValueError())
68+
69+
5670
def test_excinfo_getstatement():
5771
def g():
5872
raise ValueError

0 commit comments

Comments
 (0)