From 11f1f792226622fc69a99b2a1b567630130b09f8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 11:36:33 +0300 Subject: [PATCH] Allow creating ExceptionInfo from existing exc_info for better typing This way the ExceptionInfo generic parameter can be inferred from the passed-in exc_info. See for example the replaced cast(). --- src/_pytest/_code/code.py | 47 +++++++++++++++++++++++++----------- src/_pytest/python_api.py | 10 ++++---- testing/code/test_excinfo.py | 10 +++++++- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 07ed8066c..30ab01235 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -396,6 +396,33 @@ class ExceptionInfo(Generic[_E]): _striptext = attr.ib(type=str, default="") _traceback = attr.ib(type=Optional[Traceback], default=None) + @classmethod + def from_exc_info( + cls, + exc_info: Tuple["Type[_E]", "_E", TracebackType], + exprinfo: Optional[str] = None, + ) -> "ExceptionInfo[_E]": + """returns an ExceptionInfo for an existing exc_info tuple. + + .. warning:: + + Experimental API + + + :param exprinfo: a text string helping to determine if we should + strip ``AssertionError`` from the output, defaults + to the exception message/``__str__()`` + """ + _striptext = "" + if exprinfo is None and isinstance(exc_info[1], AssertionError): + exprinfo = getattr(exc_info[1], "msg", None) + if exprinfo is None: + exprinfo = saferepr(exc_info[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + _striptext = "AssertionError: " + + return cls(exc_info, _striptext) + @classmethod def from_current( cls, exprinfo: Optional[str] = None @@ -411,20 +438,12 @@ class ExceptionInfo(Generic[_E]): strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` """ - tup_ = sys.exc_info() - assert tup_[0] is not None, "no current exception" - assert tup_[1] is not None, "no current exception" - assert tup_[2] is not None, "no current exception" - tup = (tup_[0], tup_[1], tup_[2]) - _striptext = "" - if exprinfo is None and isinstance(tup[1], AssertionError): - exprinfo = getattr(tup[1], "msg", None) - if exprinfo is None: - exprinfo = saferepr(tup[1]) - if exprinfo and exprinfo.startswith(cls._assert_start_repr): - _striptext = "AssertionError: " - - return cls(tup, _striptext) + tup = sys.exc_info() + assert tup[0] is not None, "no current exception" + assert tup[1] is not None, "no current exception" + assert tup[2] is not None, "no current exception" + exc_info = (tup[0], tup[1], tup[2]) + return cls.from_exc_info(exc_info) @classmethod def for_later(cls) -> "ExceptionInfo[_E]": diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index e3cb8a970..08426d69c 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -707,11 +707,11 @@ def raises( ) try: func(*args[1:], **kwargs) - except expected_exception: - # Cast to narrow the type to expected_exception (_E). - return cast( - _pytest._code.ExceptionInfo[_E], - _pytest._code.ExceptionInfo.from_current(), + 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__) ) fail(message) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index d7771833a..76f974957 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -58,7 +58,7 @@ class TWMock: fullwidth = 80 -def test_excinfo_simple(): +def test_excinfo_simple() -> None: try: raise ValueError except ValueError: @@ -66,6 +66,14 @@ def test_excinfo_simple(): assert info.type == ValueError +def test_excinfo_from_exc_info_simple(): + try: + raise ValueError + except ValueError as e: + info = _pytest._code.ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) + assert info.type == ValueError + + def test_excinfo_getstatement(): def g(): raise ValueError