Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/7469.deprecation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ Directly constructing the following classes is now deprecated:
- ``_pytest.mark.structures.MarkGenerator``
- ``_pytest.python.Metafunc``
- ``_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.
5 changes: 3 additions & 2 deletions changelog/7469.feature.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ The newly-exported types are:
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
- ``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.runner.CallInfo`` for the :class:`CallInfo <pytest.CallInfo>` type passed to various hooks.
- ``pytest.Metafunc`` for the :class:`metafunc <pytest.MarkGenerator>` argument to the :func:`pytest_generate_tests <pytest.hookspec.pytest_generate_tests>` hook.
- ``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.
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
Expand Down
2 changes: 1 addition & 1 deletion doc/en/how-to/assert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ and if you need to have access to the actual exception info you may use:
f()
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
``.type``, ``.value`` and ``.traceback``.

Expand Down
2 changes: 1 addition & 1 deletion doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ Config
ExceptionInfo
~~~~~~~~~~~~~

.. autoclass:: _pytest._code.ExceptionInfo
.. autoclass:: pytest.ExceptionInfo()
:members:


Expand Down
54 changes: 34 additions & 20 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from _pytest._io.saferepr import saferepr
from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.deprecated import check_ispytest
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath

Expand Down Expand Up @@ -436,26 +437,39 @@ def recursionindex(self) -> Optional[int]:
)


_E = TypeVar("_E", bound=BaseException, covariant=True)
E = TypeVar("E", bound=BaseException, covariant=True)


@final
@attr.s(repr=False)
class ExceptionInfo(Generic[_E]):
@attr.s(repr=False, init=False)
class ExceptionInfo(Generic[E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""

_assert_start_repr = "AssertionError('assert "

_excinfo = attr.ib(type=Optional[Tuple[Type["_E"], "_E", TracebackType]])
_striptext = attr.ib(type=str, default="")
_traceback = attr.ib(type=Optional[Traceback], default=None)
_excinfo = attr.ib(type=Optional[Tuple[Type["E"], "E", TracebackType]])
_striptext = attr.ib(type=str)
_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
def from_exc_info(
cls,
exc_info: Tuple[Type[_E], _E, TracebackType],
exc_info: Tuple[Type[E], E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[_E]":
) -> "ExceptionInfo[E]":
"""Return an ExceptionInfo for an existing exc_info tuple.

.. warning::
Expand All @@ -475,7 +489,7 @@ def from_exc_info(
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
_striptext = "AssertionError: "

return cls(exc_info, _striptext)
return cls(exc_info, _striptext, _ispytest=True)

@classmethod
def from_current(
Expand All @@ -500,25 +514,25 @@ def from_current(
return ExceptionInfo.from_exc_info(exc_info, exprinfo)

@classmethod
def for_later(cls) -> "ExceptionInfo[_E]":
def for_later(cls) -> "ExceptionInfo[E]":
"""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()``."""
assert self._excinfo is None, "ExceptionInfo was already filled"
self._excinfo = exc_info

@property
def type(self) -> Type[_E]:
def type(self) -> Type[E]:
"""The exception class."""
assert (
self._excinfo is not None
), ".type can only be used after the context manager exits"
return self._excinfo[0]

@property
def value(self) -> _E:
def value(self) -> E:
"""The exception value."""
assert (
self._excinfo is not None
Expand Down Expand Up @@ -562,10 +576,10 @@ def __repr__(self) -> str:
def exconly(self, tryshort: bool = False) -> str:
"""Return the exception as a string.

When 'tryshort' resolves to True, and the exception is a
_pytest._code._AssertionError, only the actual exception part of
the exception representation is returned (so 'AssertionError: ' is
removed from the beginning).
When 'tryshort' resolves to True, and the exception is an
AssertionError, only the actual exception part of the exception
representation is returned (so 'AssertionError: ' is removed from
the beginning).
"""
lines = format_exception_only(self.type, self.value)
text = "".join(lines)
Expand Down Expand Up @@ -922,7 +936,7 @@ def repr_excinfo(
if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
ExceptionInfo((type(e), e, e.__traceback__))
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
Expand All @@ -932,7 +946,7 @@ def repr_excinfo(
):
e = e.__context__
excinfo_ = (
ExceptionInfo((type(e), e, e.__traceback__))
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def main(
try:
config = _prepareconfig(args, plugins)
except ConftestImportFailure as e:
exc_info = ExceptionInfo(e.excinfo)
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
tw = TerminalWriter(sys.stderr)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter(
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def repr_failure( # type: ignore[override]
example, failure.got, report_choice
).split("\n")
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 += [
x.strip("\n")
Expand Down
52 changes: 26 additions & 26 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,18 @@


# 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).
_FixtureFunction = TypeVar("_FixtureFunction", bound=Callable[..., object])
FixtureFunction = TypeVar("FixtureFunction", bound=Callable[..., object])
# The type of a fixture function (type alias generic in fixture value).
_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).
_FixtureCachedResult = Union[
Tuple[
# The result.
_FixtureValue,
FixtureValue,
# Cache key.
object,
None,
Expand All @@ -106,8 +106,8 @@


@attr.s(frozen=True)
class PseudoFixtureDef(Generic[_FixtureValue]):
cached_result = attr.ib(type="_FixtureCachedResult[_FixtureValue]")
class PseudoFixtureDef(Generic[FixtureValue]):
cached_result = attr.ib(type="_FixtureCachedResult[FixtureValue]")
scope = attr.ib(type="_Scope")


Expand Down Expand Up @@ -928,11 +928,11 @@ def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn":


def call_fixture_func(
fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs
) -> _FixtureValue:
fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
) -> FixtureValue:
if is_generator(fixturefunc):
fixturefunc = cast(
Callable[..., Generator[_FixtureValue, None, None]], fixturefunc
Callable[..., Generator[FixtureValue, None, None]], fixturefunc
)
generator = fixturefunc(**kwargs)
try:
Expand All @@ -942,7 +942,7 @@ def call_fixture_func(
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
request.addfinalizer(finalizer)
else:
fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc)
fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
fixture_result = fixturefunc(**kwargs)
return fixture_result

Expand Down Expand Up @@ -985,15 +985,15 @@ def _eval_scope_callable(


@final
class FixtureDef(Generic[_FixtureValue]):
class FixtureDef(Generic[FixtureValue]):
"""A container for a factory definition."""

def __init__(
self,
fixturemanager: "FixtureManager",
baseid: Optional[str],
argname: str,
func: "_FixtureFunc[_FixtureValue]",
func: "_FixtureFunc[FixtureValue]",
scope: "Union[_Scope, Callable[[str, Config], _Scope]]",
params: Optional[Sequence[object]],
unittest: bool = False,
Expand Down Expand Up @@ -1026,7 +1026,7 @@ def __init__(
)
self.unittest = unittest
self.ids = ids
self.cached_result: Optional[_FixtureCachedResult[_FixtureValue]] = None
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
self._finalizers: List[Callable[[], object]] = []

def addfinalizer(self, finalizer: Callable[[], object]) -> None:
Expand Down Expand Up @@ -1055,7 +1055,7 @@ def finish(self, request: SubRequest) -> None:
self.cached_result = None
self._finalizers = []

def execute(self, request: SubRequest) -> _FixtureValue:
def execute(self, request: SubRequest) -> FixtureValue:
# Get required arguments and register our own finish()
# with their finalization.
for argname in self.argnames:
Expand Down Expand Up @@ -1096,8 +1096,8 @@ def __repr__(self) -> str:


def resolve_fixture_function(
fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest
) -> "_FixtureFunc[_FixtureValue]":
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
) -> "_FixtureFunc[FixtureValue]":
"""Get the actual callable that can be called to obtain the fixture
value, dealing with unittest-specific instances and bound methods."""
fixturefunc = fixturedef.func
Expand All @@ -1123,8 +1123,8 @@ def resolve_fixture_function(


def pytest_fixture_setup(
fixturedef: FixtureDef[_FixtureValue], request: SubRequest
) -> _FixtureValue:
fixturedef: FixtureDef[FixtureValue], request: SubRequest
) -> FixtureValue:
"""Execution of fixture setup."""
kwargs = {}
for argname in fixturedef.argnames:
Expand Down Expand Up @@ -1174,9 +1174,9 @@ def _params_converter(


def wrap_function_to_error_out_if_called_directly(
function: _FixtureFunction,
function: FixtureFunction,
fixture_marker: "FixtureFunctionMarker",
) -> _FixtureFunction:
) -> FixtureFunction:
"""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."""
message = (
Expand All @@ -1194,7 +1194,7 @@ def result(*args, **kwargs):
# further than this point and lose useful wrappings like @mock.patch (#3774).
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]

return cast(_FixtureFunction, result)
return cast(FixtureFunction, result)


@final
Expand All @@ -1213,7 +1213,7 @@ class FixtureFunctionMarker:
)
name = attr.ib(type=Optional[str], default=None)

def __call__(self, function: _FixtureFunction) -> _FixtureFunction:
def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)")

Expand Down Expand Up @@ -1241,7 +1241,7 @@ def __call__(self, function: _FixtureFunction) -> _FixtureFunction:

@overload
def fixture(
fixture_function: _FixtureFunction,
fixture_function: FixtureFunction,
*,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ...,
params: Optional[Iterable[object]] = ...,
Expand All @@ -1253,7 +1253,7 @@ def fixture(
]
] = ...,
name: Optional[str] = ...,
) -> _FixtureFunction:
) -> FixtureFunction:
...


Expand All @@ -1276,7 +1276,7 @@ def fixture(


def fixture(
fixture_function: Optional[_FixtureFunction] = None,
fixture_function: Optional[FixtureFunction] = None,
*,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
params: Optional[Iterable[object]] = None,
Expand All @@ -1288,7 +1288,7 @@ def fixture(
]
] = None,
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, _FixtureFunction]:
) -> Union[FixtureFunctionMarker, FixtureFunction]:
"""Decorator to mark a fixture factory function.

This decorator can be used, with or without parameters, to define a
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ def _repr_failure_py(
from _pytest.fixtures import FixtureLookupError

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 not excinfo.value.pytrace:
style = "value"
Expand Down
Loading