Skip to content

Commit

Permalink
code: add ExceptionInfo.from_exception
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bluetech committed Apr 12, 2023
1 parent 61f7c27 commit dc5dd8c
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 18 deletions.
2 changes: 2 additions & 0 deletions changelog/10901.feature.rst
@@ -0,0 +1,2 @@
Added :func:`~pytest.ExceptionInfo.from_exception()`, a simpler way to create an :type:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`~~pytest.ExceptionInfo.from_exc_info()` for most uses.
41 changes: 28 additions & 13 deletions src/_pytest/_code/code.py
Expand Up @@ -469,12 +469,20 @@ def __init__(
self._traceback = traceback

@classmethod
def from_exc_info(
def from_exception(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
# Ignoring error: "Cannot use a covariant type variable as a parameter".
# This is OK to ignore because this class is (conceptually) readonly.
# See https://github.com/python/mypy/issues/7049.
exception: E, # type: ignore[misc]
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Return an ExceptionInfo for an existing exc_info tuple.
"""Return an ExceptionInfo for an existing exception.
The exception must have a non-``None`` ``__traceback__`` attribute,
otherwise this function fails with an assertion error. This means that
the exception must have been raised, or added a traceback with the
:func:`BaseException.with_traceback()` method.
.. warning::
Expand All @@ -484,7 +492,22 @@ def from_exc_info(
A text string helping to determine if we should strip
``AssertionError`` from the output. Defaults to the exception
message/``__str__()``.
.. versionadded:: 7.4
"""
assert (
exception.__traceback__
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
exc_info = (type(exception), exception, exception.__traceback__)
return cls.from_exc_info(exc_info, exprinfo)

@classmethod
def from_exc_info(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
_striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None)
Expand Down Expand Up @@ -965,21 +988,13 @@ def repr_excinfo(

if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None

Check warning on line 991 in src/_pytest/_code/code.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/_code/code.py#L991

Added line #L991 was not covered by tests
descr = "The above exception was the direct cause of the following exception:"
elif (
e.__context__ is not None and not e.__suppress_context__ and self.chain
):
e = e.__context__
excinfo_ = (
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None

Check warning on line 997 in src/_pytest/_code/code.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/_code/code.py#L997

Added line #L997 was not covered by tests
descr = "During handling of the above exception, another exception occurred:"
else:
e = None
Expand Down
6 changes: 1 addition & 5 deletions src/_pytest/python_api.py
Expand Up @@ -950,11 +950,7 @@ def raises( # noqa: F811
try:
func(*args[1:], **kwargs)
except expected_exception as e:
# We just caught the exception - there is a traceback.
assert e.__traceback__ is not None
return _pytest._code.ExceptionInfo.from_exc_info(
(type(e), e, e.__traceback__)
)
return _pytest._code.ExceptionInfo.from_exception(e)
fail(message)


Expand Down
14 changes: 14 additions & 0 deletions testing/code/test_excinfo.py
Expand Up @@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
assert info.type == ValueError


def test_excinfo_from_exception_simple() -> None:
try:
raise ValueError
except ValueError as e:
assert e.__traceback__ is not None
info = _pytest._code.ExceptionInfo.from_exception(e)
assert info.type == ValueError


def test_excinfo_from_exception_missing_traceback_assertion() -> None:
with pytest.raises(AssertionError, match=r"must have.*__traceback__"):
_pytest._code.ExceptionInfo.from_exception(ValueError())


def test_excinfo_getstatement():
def g():
raise ValueError
Expand Down

0 comments on commit dc5dd8c

Please sign in to comment.