Merge pull request #8437 from bluetech/rm-typevar-prefix

Remove `_` prefix from TypeVars, expose ExceptionInfo
This commit is contained in:
Ran Benita 2021-03-13 19:01:29 +02:00 committed by GitHub
commit db539ed2b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 102 additions and 76 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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``.

View File

@ -793,7 +793,7 @@ Config
ExceptionInfo ExceptionInfo
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
.. autoclass:: _pytest._code.ExceptionInfo .. autoclass:: pytest.ExceptionInfo()
:members: :members:

View File

@ -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
@ -436,26 +437,39 @@ co_equal = compile(
) )
_E = TypeVar("_E", bound=BaseException, covariant=True) 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(
cls, cls,
exc_info: Tuple[Type[_E], _E, TracebackType], exc_info: Tuple[Type[E], E, TracebackType],
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 exc_info tuple.
.. warning:: .. warning::
@ -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(
@ -500,17 +514,17 @@ class ExceptionInfo(Generic[_E]):
return ExceptionInfo.from_exc_info(exc_info, exprinfo) return ExceptionInfo.from_exc_info(exc_info, exprinfo)
@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()``."""
assert self._excinfo is None, "ExceptionInfo was already filled" assert self._excinfo is None, "ExceptionInfo was already filled"
self._excinfo = exc_info self._excinfo = exc_info
@property @property
def type(self) -> Type[_E]: def type(self) -> Type[E]:
"""The exception class.""" """The exception class."""
assert ( assert (
self._excinfo is not None self._excinfo is not None
@ -518,7 +532,7 @@ class ExceptionInfo(Generic[_E]):
return self._excinfo[0] return self._excinfo[0]
@property @property
def value(self) -> _E: def value(self) -> E:
"""The exception value.""" """The exception value."""
assert ( assert (
self._excinfo is not None self._excinfo is not None
@ -562,10 +576,10 @@ class ExceptionInfo(Generic[_E]):
def exconly(self, tryshort: bool = False) -> str: def exconly(self, tryshort: bool = False) -> str:
"""Return the exception as a string. """Return the exception as a string.
When 'tryshort' resolves to True, and the exception is a When 'tryshort' resolves to True, and the exception is an
_pytest._code._AssertionError, only the actual exception part of AssertionError, only the actual exception part of the exception
the exception representation is returned (so 'AssertionError: ' is representation is returned (so 'AssertionError: ' is removed from
removed from the beginning). the beginning).
""" """
lines = format_exception_only(self.type, self.value) lines = format_exception_only(self.type, self.value)
text = "".join(lines) text = "".join(lines)
@ -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
) )

View File

@ -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(

View File

@ -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")

View File

@ -79,18 +79,18 @@ if TYPE_CHECKING:
# The value of the fixture -- return/yield of the fixture function (type variable). # The value of the fixture -- return/yield of the fixture function (type variable).
_FixtureValue = TypeVar("_FixtureValue") FixtureValue = TypeVar("FixtureValue")
# The type of the fixture function (type variable). # The type of the fixture function (type variable).
_FixtureFunction = TypeVar("_FixtureFunction", bound=Callable[..., object]) FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
# The type of a fixture function (type alias generic in fixture value). # The type of a fixture function (type alias generic in fixture value).
_FixtureFunc = Union[ _FixtureFunc = Union[
Callable[..., _FixtureValue], Callable[..., Generator[_FixtureValue, None, None]] Callable[..., FixtureValue], Callable[..., Generator[FixtureValue, None, None]]
] ]
# The type of FixtureDef.cached_result (type alias generic in fixture value). # The type of FixtureDef.cached_result (type alias generic in fixture value).
_FixtureCachedResult = Union[ _FixtureCachedResult = Union[
Tuple[ Tuple[
# The result. # The result.
_FixtureValue, FixtureValue,
# Cache key. # Cache key.
object, object,
None, None,
@ -106,8 +106,8 @@ _FixtureCachedResult = Union[
@attr.s(frozen=True) @attr.s(frozen=True)
class PseudoFixtureDef(Generic[_FixtureValue]): class PseudoFixtureDef(Generic[FixtureValue]):
cached_result = attr.ib(type="_FixtureCachedResult[_FixtureValue]") cached_result = attr.ib(type="_FixtureCachedResult[FixtureValue]")
scope = attr.ib(type="_Scope") scope = attr.ib(type="_Scope")
@ -928,11 +928,11 @@ def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn":
def call_fixture_func( def call_fixture_func(
fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
) -> _FixtureValue: ) -> FixtureValue:
if is_generator(fixturefunc): if is_generator(fixturefunc):
fixturefunc = cast( fixturefunc = cast(
Callable[..., Generator[_FixtureValue, None, None]], fixturefunc Callable[..., Generator[FixtureValue, None, None]], fixturefunc
) )
generator = fixturefunc(**kwargs) generator = fixturefunc(**kwargs)
try: try:
@ -942,7 +942,7 @@ def call_fixture_func(
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
request.addfinalizer(finalizer) request.addfinalizer(finalizer)
else: else:
fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc) fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
fixture_result = fixturefunc(**kwargs) fixture_result = fixturefunc(**kwargs)
return fixture_result return fixture_result
@ -985,7 +985,7 @@ def _eval_scope_callable(
@final @final
class FixtureDef(Generic[_FixtureValue]): class FixtureDef(Generic[FixtureValue]):
"""A container for a factory definition.""" """A container for a factory definition."""
def __init__( def __init__(
@ -993,7 +993,7 @@ class FixtureDef(Generic[_FixtureValue]):
fixturemanager: "FixtureManager", fixturemanager: "FixtureManager",
baseid: Optional[str], baseid: Optional[str],
argname: str, argname: str,
func: "_FixtureFunc[_FixtureValue]", func: "_FixtureFunc[FixtureValue]",
scope: "Union[_Scope, Callable[[str, Config], _Scope]]", scope: "Union[_Scope, Callable[[str, Config], _Scope]]",
params: Optional[Sequence[object]], params: Optional[Sequence[object]],
unittest: bool = False, unittest: bool = False,
@ -1026,7 +1026,7 @@ class FixtureDef(Generic[_FixtureValue]):
) )
self.unittest = unittest self.unittest = unittest
self.ids = ids self.ids = ids
self.cached_result: Optional[_FixtureCachedResult[_FixtureValue]] = None self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
self._finalizers: List[Callable[[], object]] = [] self._finalizers: List[Callable[[], object]] = []
def addfinalizer(self, finalizer: Callable[[], object]) -> None: def addfinalizer(self, finalizer: Callable[[], object]) -> None:
@ -1055,7 +1055,7 @@ class FixtureDef(Generic[_FixtureValue]):
self.cached_result = None self.cached_result = None
self._finalizers = [] self._finalizers = []
def execute(self, request: SubRequest) -> _FixtureValue: def execute(self, request: SubRequest) -> FixtureValue:
# Get required arguments and register our own finish() # Get required arguments and register our own finish()
# with their finalization. # with their finalization.
for argname in self.argnames: for argname in self.argnames:
@ -1096,8 +1096,8 @@ class FixtureDef(Generic[_FixtureValue]):
def resolve_fixture_function( def resolve_fixture_function(
fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
) -> "_FixtureFunc[_FixtureValue]": ) -> "_FixtureFunc[FixtureValue]":
"""Get the actual callable that can be called to obtain the fixture """Get the actual callable that can be called to obtain the fixture
value, dealing with unittest-specific instances and bound methods.""" value, dealing with unittest-specific instances and bound methods."""
fixturefunc = fixturedef.func fixturefunc = fixturedef.func
@ -1123,8 +1123,8 @@ def resolve_fixture_function(
def pytest_fixture_setup( def pytest_fixture_setup(
fixturedef: FixtureDef[_FixtureValue], request: SubRequest fixturedef: FixtureDef[FixtureValue], request: SubRequest
) -> _FixtureValue: ) -> FixtureValue:
"""Execution of fixture setup.""" """Execution of fixture setup."""
kwargs = {} kwargs = {}
for argname in fixturedef.argnames: for argname in fixturedef.argnames:
@ -1174,9 +1174,9 @@ def _params_converter(
def wrap_function_to_error_out_if_called_directly( def wrap_function_to_error_out_if_called_directly(
function: _FixtureFunction, function: FixtureFunction,
fixture_marker: "FixtureFunctionMarker", fixture_marker: "FixtureFunctionMarker",
) -> _FixtureFunction: ) -> FixtureFunction:
"""Wrap the given fixture function so we can raise an error about it being called directly, """Wrap the given fixture function so we can raise an error about it being called directly,
instead of used as an argument in a test function.""" instead of used as an argument in a test function."""
message = ( message = (
@ -1194,7 +1194,7 @@ def wrap_function_to_error_out_if_called_directly(
# further than this point and lose useful wrappings like @mock.patch (#3774). # further than this point and lose useful wrappings like @mock.patch (#3774).
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
return cast(_FixtureFunction, result) return cast(FixtureFunction, result)
@final @final
@ -1213,7 +1213,7 @@ class FixtureFunctionMarker:
) )
name = attr.ib(type=Optional[str], default=None) name = attr.ib(type=Optional[str], default=None)
def __call__(self, function: _FixtureFunction) -> _FixtureFunction: def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function): if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)") raise ValueError("class fixtures not supported (maybe in the future)")
@ -1241,7 +1241,7 @@ class FixtureFunctionMarker:
@overload @overload
def fixture( def fixture(
fixture_function: _FixtureFunction, fixture_function: FixtureFunction,
*, *,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ..., scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ...,
params: Optional[Iterable[object]] = ..., params: Optional[Iterable[object]] = ...,
@ -1253,7 +1253,7 @@ def fixture(
] ]
] = ..., ] = ...,
name: Optional[str] = ..., name: Optional[str] = ...,
) -> _FixtureFunction: ) -> FixtureFunction:
... ...
@ -1276,7 +1276,7 @@ def fixture(
def fixture( def fixture(
fixture_function: Optional[_FixtureFunction] = None, fixture_function: Optional[FixtureFunction] = None,
*, *,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
params: Optional[Iterable[object]] = None, params: Optional[Iterable[object]] = None,
@ -1288,7 +1288,7 @@ def fixture(
] ]
] = None, ] = None,
name: Optional[str] = None, name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, _FixtureFunction]: ) -> Union[FixtureFunctionMarker, FixtureFunction]:
"""Decorator to mark a fixture factory function. """Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a This decorator can be used, with or without parameters, to define a

View File

@ -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"

View File

@ -573,31 +573,31 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]:
# builtin pytest.raises helper # builtin pytest.raises helper
_E = TypeVar("_E", bound=BaseException) E = TypeVar("E", bound=BaseException)
@overload @overload
def raises( def raises(
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], expected_exception: Union[Type[E], Tuple[Type[E], ...]],
*, *,
match: Optional[Union[str, Pattern[str]]] = ..., match: Optional[Union[str, Pattern[str]]] = ...,
) -> "RaisesContext[_E]": ) -> "RaisesContext[E]":
... ...
@overload @overload
def raises( def raises(
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], expected_exception: Union[Type[E], Tuple[Type[E], ...]],
func: Callable[..., Any], func: Callable[..., Any],
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> _pytest._code.ExceptionInfo[_E]: ) -> _pytest._code.ExceptionInfo[E]:
... ...
def raises( def raises(
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], *args: Any, **kwargs: Any expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]: ) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
r"""Assert that a code block/function call raises ``expected_exception`` r"""Assert that a code block/function call raises ``expected_exception``
or raise a failure exception otherwise. or raise a failure exception otherwise.
@ -711,7 +711,7 @@ def raises(
__tracebackhide__ = True __tracebackhide__ = True
if isinstance(expected_exception, type): if isinstance(expected_exception, type):
excepted_exceptions: Tuple[Type[_E], ...] = (expected_exception,) excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
else: else:
excepted_exceptions = expected_exception excepted_exceptions = expected_exception
for exc in excepted_exceptions: for exc in excepted_exceptions:
@ -752,19 +752,19 @@ raises.Exception = fail.Exception # type: ignore
@final @final
class RaisesContext(Generic[_E]): class RaisesContext(Generic[E]):
def __init__( def __init__(
self, self,
expected_exception: Union[Type[_E], Tuple[Type[_E], ...]], expected_exception: Union[Type[E], Tuple[Type[E], ...]],
message: str, message: str,
match_expr: Optional[Union[str, Pattern[str]]] = None, match_expr: Optional[Union[str, Pattern[str]]] = None,
) -> None: ) -> None:
self.expected_exception = expected_exception self.expected_exception = expected_exception
self.message = message self.message = message
self.match_expr = match_expr self.match_expr = match_expr
self.excinfo: Optional[_pytest._code.ExceptionInfo[_E]] = None self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
def __enter__(self) -> _pytest._code.ExceptionInfo[_E]: def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
self.excinfo = _pytest._code.ExceptionInfo.for_later() self.excinfo = _pytest._code.ExceptionInfo.for_later()
return self.excinfo return self.excinfo
@ -781,7 +781,7 @@ class RaisesContext(Generic[_E]):
if not issubclass(exc_type, self.expected_exception): if not issubclass(exc_type, self.expected_exception):
return False return False
# Cast to narrow the exception type now that it's verified. # Cast to narrow the exception type now that it's verified.
exc_info = cast(Tuple[Type[_E], _E, TracebackType], (exc_type, exc_val, exc_tb)) exc_info = cast(Tuple[Type[E], E, TracebackType], (exc_type, exc_val, exc_tb))
self.excinfo.fill_unfilled(exc_info) self.excinfo.fill_unfilled(exc_info)
if self.match_expr is not None: if self.match_expr is not None:
self.excinfo.match(self.match_expr) self.excinfo.match(self.match_expr)

View File

@ -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

View File

@ -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",

View 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
""" """