diff --git a/changelog/10901.feature.rst b/changelog/10901.feature.rst new file mode 100644 index 000000000..0d99d66f6 --- /dev/null +++ b/changelog/10901.feature.rst @@ -0,0 +1,2 @@ +Added :func:`ExceptionInfo.from_exception() `, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception. +This can replace :func:`ExceptionInfo.from_exc_info() ` for most uses. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 032b83beb..e2451bcdf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -469,22 +469,41 @@ class ExceptionInfo(Generic[E]): 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. - .. warning:: - - Experimental API + 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: 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) @@ -965,21 +984,13 @@ class FormattedExcinfo: 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 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 descr = "During handling of the above exception, another exception occurred:" else: e = None diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index b03a251ab..4213bd098 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -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) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 918c97276..8ac8a42ab 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -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