code: export ExceptionInfo for typing purposes
This type is most prominent in `pytest.raises` and we should allow to refer to it by a public name. The type is not in a perfectly "exposable" state. In particular: - The `traceback` property with type `Traceback` which is derived from the `py.code` API and exposes a bunch more types transitively. This stuff is *not* exported and probably won't be. - The `getrepr` method which probably should be private. But they're already used in the wild so no point in just hiding them now. The __init__ API is hidden -- the public API for this are the `from_*` classmethods.
This commit is contained in:
parent
96ef7d678b
commit
f2d65c85f4
|
@ -5,5 +5,6 @@ Directly constructing the following classes is now deprecated:
|
||||||
- ``_pytest.mark.structures.MarkGenerator``
|
- ``_pytest.mark.structures.MarkGenerator``
|
||||||
- ``_pytest.python.Metafunc``
|
- ``_pytest.python.Metafunc``
|
||||||
- ``_pytest.runner.CallInfo``
|
- ``_pytest.runner.CallInfo``
|
||||||
|
- ``_pytest._code.ExceptionInfo``
|
||||||
|
|
||||||
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.
|
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.
|
||||||
|
|
|
@ -5,8 +5,9 @@ The newly-exported types are:
|
||||||
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
|
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
|
||||||
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
|
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
|
||||||
- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
|
- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
|
||||||
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the `pytest_generate_tests <pytest.hookspec.pytest_generate_tests>` hook.
|
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the :func:`pytest_generate_tests <pytest.hookspec.pytest_generate_tests>` hook.
|
||||||
- ``pytest.runner.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
|
- ``pytest.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
|
||||||
|
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
|
||||||
|
|
||||||
Constructing them directly is not supported; they are only meant for use in type annotations.
|
Constructing them directly is not supported; they are only meant for use in type annotations.
|
||||||
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
|
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
|
||||||
|
|
|
@ -98,7 +98,7 @@ and if you need to have access to the actual exception info you may use:
|
||||||
f()
|
f()
|
||||||
assert "maximum recursion" in str(excinfo.value)
|
assert "maximum recursion" in str(excinfo.value)
|
||||||
|
|
||||||
``excinfo`` is an ``ExceptionInfo`` instance, which is a wrapper around
|
``excinfo`` is an :class:`~pytest.ExceptionInfo` instance, which is a wrapper around
|
||||||
the actual exception raised. The main attributes of interest are
|
the actual exception raised. The main attributes of interest are
|
||||||
``.type``, ``.value`` and ``.traceback``.
|
``.type``, ``.value`` and ``.traceback``.
|
||||||
|
|
||||||
|
|
|
@ -793,7 +793,7 @@ Config
|
||||||
ExceptionInfo
|
ExceptionInfo
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: _pytest._code.ExceptionInfo
|
.. autoclass:: pytest.ExceptionInfo()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ from _pytest._io.saferepr import safeformat
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
from _pytest.compat import get_real_func
|
from _pytest.compat import get_real_func
|
||||||
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
|
|
||||||
|
@ -440,15 +441,28 @@ E = TypeVar("E", bound=BaseException, covariant=True)
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(repr=False)
|
@attr.s(repr=False, init=False)
|
||||||
class ExceptionInfo(Generic[E]):
|
class ExceptionInfo(Generic[E]):
|
||||||
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
|
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
|
||||||
|
|
||||||
_assert_start_repr = "AssertionError('assert "
|
_assert_start_repr = "AssertionError('assert "
|
||||||
|
|
||||||
_excinfo = attr.ib(type=Optional[Tuple[Type["E"], "E", TracebackType]])
|
_excinfo = attr.ib(type=Optional[Tuple[Type["E"], "E", TracebackType]])
|
||||||
_striptext = attr.ib(type=str, default="")
|
_striptext = attr.ib(type=str)
|
||||||
_traceback = attr.ib(type=Optional[Traceback], default=None)
|
_traceback = attr.ib(type=Optional[Traceback])
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
|
||||||
|
striptext: str = "",
|
||||||
|
traceback: Optional[Traceback] = None,
|
||||||
|
*,
|
||||||
|
_ispytest: bool = False,
|
||||||
|
) -> None:
|
||||||
|
check_ispytest(_ispytest)
|
||||||
|
self._excinfo = excinfo
|
||||||
|
self._striptext = striptext
|
||||||
|
self._traceback = traceback
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_exc_info(
|
def from_exc_info(
|
||||||
|
@ -475,7 +489,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
|
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
|
||||||
_striptext = "AssertionError: "
|
_striptext = "AssertionError: "
|
||||||
|
|
||||||
return cls(exc_info, _striptext)
|
return cls(exc_info, _striptext, _ispytest=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_current(
|
def from_current(
|
||||||
|
@ -502,7 +516,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_later(cls) -> "ExceptionInfo[E]":
|
def for_later(cls) -> "ExceptionInfo[E]":
|
||||||
"""Return an unfilled ExceptionInfo."""
|
"""Return an unfilled ExceptionInfo."""
|
||||||
return cls(None)
|
return cls(None, _ispytest=True)
|
||||||
|
|
||||||
def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
|
def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
|
||||||
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
|
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
|
||||||
|
@ -922,7 +936,7 @@ class FormattedExcinfo:
|
||||||
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((type(e), e, e.__traceback__))
|
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||||
if e.__traceback__
|
if e.__traceback__
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
@ -932,7 +946,7 @@ class FormattedExcinfo:
|
||||||
):
|
):
|
||||||
e = e.__context__
|
e = e.__context__
|
||||||
excinfo_ = (
|
excinfo_ = (
|
||||||
ExceptionInfo((type(e), e, e.__traceback__))
|
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||||
if e.__traceback__
|
if e.__traceback__
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
|
@ -145,7 +145,7 @@ def main(
|
||||||
try:
|
try:
|
||||||
config = _prepareconfig(args, plugins)
|
config = _prepareconfig(args, plugins)
|
||||||
except ConftestImportFailure as e:
|
except ConftestImportFailure as e:
|
||||||
exc_info = ExceptionInfo(e.excinfo)
|
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
|
||||||
tw = TerminalWriter(sys.stderr)
|
tw = TerminalWriter(sys.stderr)
|
||||||
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
||||||
exc_info.traceback = exc_info.traceback.filter(
|
exc_info.traceback = exc_info.traceback.filter(
|
||||||
|
|
|
@ -365,7 +365,7 @@ class DoctestItem(pytest.Item):
|
||||||
example, failure.got, report_choice
|
example, failure.got, report_choice
|
||||||
).split("\n")
|
).split("\n")
|
||||||
else:
|
else:
|
||||||
inner_excinfo = ExceptionInfo(failure.exc_info)
|
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
|
||||||
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
||||||
lines += [
|
lines += [
|
||||||
x.strip("\n")
|
x.strip("\n")
|
||||||
|
|
|
@ -396,7 +396,7 @@ class Node(metaclass=NodeMeta):
|
||||||
from _pytest.fixtures import FixtureLookupError
|
from _pytest.fixtures import FixtureLookupError
|
||||||
|
|
||||||
if isinstance(excinfo.value, ConftestImportFailure):
|
if isinstance(excinfo.value, ConftestImportFailure):
|
||||||
excinfo = ExceptionInfo(excinfo.value.excinfo)
|
excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
|
||||||
if isinstance(excinfo.value, fail.Exception):
|
if isinstance(excinfo.value, fail.Exception):
|
||||||
if not excinfo.value.pytrace:
|
if not excinfo.value.pytrace:
|
||||||
style = "value"
|
style = "value"
|
||||||
|
|
|
@ -209,7 +209,7 @@ class TestCaseFunction(Function):
|
||||||
# Unwrap potential exception info (see twisted trial support below).
|
# Unwrap potential exception info (see twisted trial support below).
|
||||||
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
|
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
|
||||||
try:
|
try:
|
||||||
excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type]
|
excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type]
|
||||||
# Invoke the attributes to trigger storing the traceback
|
# Invoke the attributes to trigger storing the traceback
|
||||||
# trial causes some issue there.
|
# trial causes some issue there.
|
||||||
excinfo.value
|
excinfo.value
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"""pytest: unit and functional testing with Python."""
|
"""pytest: unit and functional testing with Python."""
|
||||||
from . import collect
|
from . import collect
|
||||||
from _pytest import __version__
|
from _pytest import __version__
|
||||||
|
from _pytest._code import ExceptionInfo
|
||||||
from _pytest.assertion import register_assert_rewrite
|
from _pytest.assertion import register_assert_rewrite
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
from _pytest.capture import CaptureFixture
|
from _pytest.capture import CaptureFixture
|
||||||
|
@ -79,6 +80,7 @@ __all__ = [
|
||||||
"console_main",
|
"console_main",
|
||||||
"deprecated_call",
|
"deprecated_call",
|
||||||
"exit",
|
"exit",
|
||||||
|
"ExceptionInfo",
|
||||||
"ExitCode",
|
"ExitCode",
|
||||||
"fail",
|
"fail",
|
||||||
"File",
|
"File",
|
||||||
|
|
|
@ -377,24 +377,32 @@ def test_testcase_adderrorandfailure_defers(pytester: Pytester, type: str) -> No
|
||||||
def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None:
|
def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None:
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
from typing import Generic, TypeVar
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
import py, pytest
|
import pytest, _pytest._code
|
||||||
import _pytest._code
|
|
||||||
class MyTestCase(TestCase):
|
class MyTestCase(TestCase):
|
||||||
def run(self, result):
|
def run(self, result):
|
||||||
excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0)
|
excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0)
|
||||||
# we fake an incompatible exception info
|
# We fake an incompatible exception info.
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
class FakeExceptionInfo(Generic[TypeVar("E")]):
|
||||||
mp = MonkeyPatch()
|
def __init__(self, *args, **kwargs):
|
||||||
def t(*args):
|
|
||||||
mp.undo()
|
mp.undo()
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
mp.setattr(_pytest._code, 'ExceptionInfo', t)
|
@classmethod
|
||||||
|
def from_current(cls):
|
||||||
|
return cls()
|
||||||
|
@classmethod
|
||||||
|
def from_exc_info(cls, *args, **kwargs):
|
||||||
|
return cls()
|
||||||
|
mp = pytest.MonkeyPatch()
|
||||||
|
mp.setattr(_pytest._code, 'ExceptionInfo', FakeExceptionInfo)
|
||||||
try:
|
try:
|
||||||
excinfo = excinfo._excinfo
|
excinfo = excinfo._excinfo
|
||||||
result.add%(type)s(self, excinfo)
|
result.add%(type)s(self, excinfo)
|
||||||
finally:
|
finally:
|
||||||
mp.undo()
|
mp.undo()
|
||||||
|
|
||||||
def test_hello(self):
|
def test_hello(self):
|
||||||
pass
|
pass
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue