Skip to content

Commit f2d65c8

Browse files
committed
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.
1 parent 96ef7d6 commit f2d65c8

File tree

11 files changed

+50
-24
lines changed

11 files changed

+50
-24
lines changed

changelog/7469.deprecation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ Directly constructing the following classes is now deprecated:
55
- ``_pytest.mark.structures.MarkGenerator``
66
- ``_pytest.python.Metafunc``
77
- ``_pytest.runner.CallInfo``
8+
- ``_pytest._code.ExceptionInfo``
89

910
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.

changelog/7469.feature.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ The newly-exported types are:
55
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
66
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
77
- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
8-
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the `pytest_generate_tests <pytest.hookspec.pytest_generate_tests>` hook.
9-
- ``pytest.runner.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
8+
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the :func:`pytest_generate_tests <pytest.hookspec.pytest_generate_tests>` hook.
9+
- ``pytest.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
10+
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
1011

1112
Constructing them directly is not supported; they are only meant for use in type annotations.
1213
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.

doc/en/how-to/assert.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ and if you need to have access to the actual exception info you may use:
9898
f()
9999
assert "maximum recursion" in str(excinfo.value)
100100
101-
``excinfo`` is an ``ExceptionInfo`` instance, which is a wrapper around
101+
``excinfo`` is an :class:`~pytest.ExceptionInfo` instance, which is a wrapper around
102102
the actual exception raised. The main attributes of interest are
103103
``.type``, ``.value`` and ``.traceback``.
104104

doc/en/reference/reference.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ Config
793793
ExceptionInfo
794794
~~~~~~~~~~~~~
795795

796-
.. autoclass:: _pytest._code.ExceptionInfo
796+
.. autoclass:: pytest.ExceptionInfo()
797797
:members:
798798

799799

src/_pytest/_code/code.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from _pytest._io.saferepr import saferepr
4343
from _pytest.compat import final
4444
from _pytest.compat import get_real_func
45+
from _pytest.deprecated import check_ispytest
4546
from _pytest.pathlib import absolutepath
4647
from _pytest.pathlib import bestrelpath
4748

@@ -440,15 +441,28 @@ def recursionindex(self) -> Optional[int]:
440441

441442

442443
@final
443-
@attr.s(repr=False)
444+
@attr.s(repr=False, init=False)
444445
class ExceptionInfo(Generic[E]):
445446
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
446447

447448
_assert_start_repr = "AssertionError('assert "
448449

449450
_excinfo = attr.ib(type=Optional[Tuple[Type["E"], "E", TracebackType]])
450-
_striptext = attr.ib(type=str, default="")
451-
_traceback = attr.ib(type=Optional[Traceback], default=None)
451+
_striptext = attr.ib(type=str)
452+
_traceback = attr.ib(type=Optional[Traceback])
453+
454+
def __init__(
455+
self,
456+
excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
457+
striptext: str = "",
458+
traceback: Optional[Traceback] = None,
459+
*,
460+
_ispytest: bool = False,
461+
) -> None:
462+
check_ispytest(_ispytest)
463+
self._excinfo = excinfo
464+
self._striptext = striptext
465+
self._traceback = traceback
452466

453467
@classmethod
454468
def from_exc_info(
@@ -475,7 +489,7 @@ def from_exc_info(
475489
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
476490
_striptext = "AssertionError: "
477491

478-
return cls(exc_info, _striptext)
492+
return cls(exc_info, _striptext, _ispytest=True)
479493

480494
@classmethod
481495
def from_current(
@@ -502,7 +516,7 @@ def from_current(
502516
@classmethod
503517
def for_later(cls) -> "ExceptionInfo[E]":
504518
"""Return an unfilled ExceptionInfo."""
505-
return cls(None)
519+
return cls(None, _ispytest=True)
506520

507521
def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
508522
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
@@ -922,7 +936,7 @@ def repr_excinfo(
922936
if e.__cause__ is not None and self.chain:
923937
e = e.__cause__
924938
excinfo_ = (
925-
ExceptionInfo((type(e), e, e.__traceback__))
939+
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
926940
if e.__traceback__
927941
else None
928942
)
@@ -932,7 +946,7 @@ def repr_excinfo(
932946
):
933947
e = e.__context__
934948
excinfo_ = (
935-
ExceptionInfo((type(e), e, e.__traceback__))
949+
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
936950
if e.__traceback__
937951
else None
938952
)

src/_pytest/config/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def main(
145145
try:
146146
config = _prepareconfig(args, plugins)
147147
except ConftestImportFailure as e:
148-
exc_info = ExceptionInfo(e.excinfo)
148+
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
149149
tw = TerminalWriter(sys.stderr)
150150
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
151151
exc_info.traceback = exc_info.traceback.filter(

src/_pytest/doctest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def repr_failure( # type: ignore[override]
365365
example, failure.got, report_choice
366366
).split("\n")
367367
else:
368-
inner_excinfo = ExceptionInfo(failure.exc_info)
368+
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
369369
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
370370
lines += [
371371
x.strip("\n")

src/_pytest/nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ def _repr_failure_py(
396396
from _pytest.fixtures import FixtureLookupError
397397

398398
if isinstance(excinfo.value, ConftestImportFailure):
399-
excinfo = ExceptionInfo(excinfo.value.excinfo)
399+
excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
400400
if isinstance(excinfo.value, fail.Exception):
401401
if not excinfo.value.pytrace:
402402
style = "value"

src/_pytest/unittest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
209209
# Unwrap potential exception info (see twisted trial support below).
210210
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
211211
try:
212-
excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type]
212+
excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(rawexcinfo) # type: ignore[arg-type]
213213
# Invoke the attributes to trigger storing the traceback
214214
# trial causes some issue there.
215215
excinfo.value

src/pytest/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""pytest: unit and functional testing with Python."""
33
from . import collect
44
from _pytest import __version__
5+
from _pytest._code import ExceptionInfo
56
from _pytest.assertion import register_assert_rewrite
67
from _pytest.cacheprovider import Cache
78
from _pytest.capture import CaptureFixture
@@ -79,6 +80,7 @@
7980
"console_main",
8081
"deprecated_call",
8182
"exit",
83+
"ExceptionInfo",
8284
"ExitCode",
8385
"fail",
8486
"File",

0 commit comments

Comments
 (0)