From 2b05faff0a0172dbc74b81f47528e56ad608839e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 1 May 2020 14:40:17 +0300 Subject: [PATCH] Improve types around repr_failure() --- src/_pytest/_code/code.py | 2 +- src/_pytest/doctest.py | 5 ++++- src/_pytest/nodes.py | 24 +++++++++++++++--------- src/_pytest/python.py | 6 +++++- src/_pytest/runner.py | 6 ++++-- src/_pytest/skipping.py | 3 ++- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 09b2c1af5..a40b23470 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -47,7 +47,7 @@ if TYPE_CHECKING: from typing_extensions import Literal from weakref import ReferenceType - _TracebackStyle = Literal["long", "short", "line", "no", "native", "value"] + _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] class Code: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index ab8085982..7aaacb481 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -300,7 +300,10 @@ class DoctestItem(pytest.Item): sys.stdout.write(out) sys.stderr.write(err) - def repr_failure(self, excinfo): + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] # noqa: F821 + self, excinfo: ExceptionInfo[BaseException], + ) -> Union[str, TerminalRepr]: import doctest failures = ( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 15f91343f..3757e0b27 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -17,9 +17,8 @@ import py import _pytest._code from _pytest._code import getfslineno -from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo -from _pytest._code.code import ReprExceptionInfo +from _pytest._code.code import TerminalRepr from _pytest.compat import cached_property from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING @@ -29,7 +28,6 @@ from _pytest.config import PytestPluginManager from _pytest.deprecated import NODE_USE_FROM_PARENT from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError -from _pytest.fixtures import FixtureLookupErrorRepr from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords @@ -43,6 +41,7 @@ if TYPE_CHECKING: # Imported here due to circular import. from _pytest.main import Session from _pytest.warning_types import PytestWarning + from _pytest._code.code import _TracebackStyle SEP = "/" @@ -355,8 +354,10 @@ class Node(metaclass=NodeMeta): pass def _repr_failure_py( - self, excinfo: ExceptionInfo[BaseException], style=None, - ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + self, + excinfo: ExceptionInfo[BaseException], + style: "Optional[_TracebackStyle]" = None, + ) -> TerminalRepr: if isinstance(excinfo.value, ConftestImportFailure): excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): @@ -406,8 +407,10 @@ class Node(metaclass=NodeMeta): ) def repr_failure( - self, excinfo, style=None - ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + self, + excinfo: ExceptionInfo[BaseException], + style: "Optional[_TracebackStyle]" = None, + ) -> Union[str, TerminalRepr]: """ Return a representation of a collection or test failure. @@ -453,13 +456,16 @@ class Collector(Node): """ raise NotImplementedError("abstract") - def repr_failure(self, excinfo): + # TODO: This omits the style= parameter which breaks Liskov Substitution. + def repr_failure( # type: ignore[override] # noqa: F821 + self, excinfo: ExceptionInfo[BaseException] + ) -> Union[str, TerminalRepr]: """ Return a representation of a collection failure. :param excinfo: Exception information for the failure. """ - if excinfo.errisinstance(self.CollectError) and not self.config.getoption( + if isinstance(excinfo.value, self.CollectError) and not self.config.getoption( "fulltrace", False ): exc = excinfo.value diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 41dd8b292..4b716c616 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -60,6 +60,7 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.pathlib import parts +from _pytest.reports import TerminalRepr from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning @@ -1591,7 +1592,10 @@ class Function(PyobjMixin, nodes.Item): for entry in excinfo.traceback[1:-1]: entry.set_repr_style("short") - def repr_failure(self, excinfo, outerr=None): + # TODO: Type ignored -- breaks Liskov Substitution. + def repr_failure( # type: ignore[override] # noqa: F821 + self, excinfo: ExceptionInfo[BaseException], outerr: None = None + ) -> Union[str, TerminalRepr]: assert outerr is None, "XXX outerr usage is deprecated" style = self.config.getoption("tbstyle", "auto") if style == "auto": diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f89b67399..3ca8d7ea4 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -2,6 +2,7 @@ import bdb import os import sys +from typing import Any from typing import Callable from typing import cast from typing import Dict @@ -256,7 +257,7 @@ class CallInfo(Generic[_T]): """ _result = attr.ib(type="Optional[_T]") - excinfo = attr.ib(type=Optional[ExceptionInfo]) + excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]]) start = attr.ib(type=float) stop = attr.ib(type=float) duration = attr.ib(type=float) @@ -313,7 +314,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call(lambda: list(collector.collect()), "collect") - longrepr = None + # TODO: Better typing for longrepr. + longrepr = None # type: Optional[Any] if not call.excinfo: outcome = "passed" # type: Literal["passed", "skipped", "failed"] else: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 54621f111..bbd4593fd 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -148,7 +148,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): elif item.config.option.runxfail: pass # don't interfere - elif call.excinfo and call.excinfo.errisinstance(xfail.Exception): + elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): + assert call.excinfo.value.msg is not None rep.wasxfail = "reason: " + call.excinfo.value.msg rep.outcome = "skipped" elif evalxfail and not rep.skipped and evalxfail.wasvalid() and evalxfail.istrue():