Merge pull request #10901 from bluetech/exceptioninfo-from-exception

code: add `ExceptionInfo.from_exception`
This commit is contained in:
Ran Benita 2023-04-13 16:04:48 +03:00 committed by GitHub
commit 5d1385320f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 18 deletions

View File

@ -0,0 +1,2 @@
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.

View File

@ -469,18 +469,41 @@ class ExceptionInfo(Generic[E]):
self._traceback = traceback self._traceback = traceback
@classmethod @classmethod
def from_exc_info( def from_exception(
cls, 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, exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]": ) -> "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
:py:meth:`~BaseException.with_traceback()` method.
:param exprinfo: :param exprinfo:
A text string helping to determine if we should strip A text string helping to determine if we should strip
``AssertionError`` from the output. Defaults to the exception ``AssertionError`` from the output. Defaults to the exception
message/``__str__()``. 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 = "" _striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError): if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None) exprinfo = getattr(exc_info[1], "msg", None)
@ -954,21 +977,13 @@ class FormattedExcinfo:
repr_chain += [(reprtraceback, reprcrash, descr)] repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain: if e.__cause__ is not None and self.chain:
e = e.__cause__ e = e.__cause__
excinfo_ = ( excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
descr = "The above exception was the direct cause of the following exception:" descr = "The above exception was the direct cause of the following exception:"
elif ( elif (
e.__context__ is not None and not e.__suppress_context__ and self.chain e.__context__ is not None and not e.__suppress_context__ and self.chain
): ):
e = e.__context__ e = e.__context__
excinfo_ = ( excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
descr = "During handling of the above exception, another exception occurred:" descr = "During handling of the above exception, another exception occurred:"
else: else:
e = None e = None

View File

@ -950,11 +950,7 @@ def raises( # noqa: F811
try: try:
func(*args[1:], **kwargs) func(*args[1:], **kwargs)
except expected_exception as e: except expected_exception as e:
# We just caught the exception - there is a traceback. return _pytest._code.ExceptionInfo.from_exception(e)
assert e.__traceback__ is not None
return _pytest._code.ExceptionInfo.from_exc_info(
(type(e), e, e.__traceback__)
)
fail(message) fail(message)

View File

@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
assert info.type == ValueError 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 test_excinfo_getstatement():
def g(): def g():
raise ValueError raise ValueError