updates
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
from io import StringIO
|
||||
import os
|
||||
from io import StringIO
|
||||
from pprint import pprint
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import final
|
||||
from typing import Literal
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
@@ -29,19 +29,14 @@ from _pytest._code.code import ReprLocals
|
||||
from _pytest._code.code import ReprTraceback
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import skip
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest.runner import CallInfo
|
||||
|
||||
@@ -51,29 +46,33 @@ def getworkerinfoline(node):
|
||||
return node._workerinfocache
|
||||
except AttributeError:
|
||||
d = node.workerinfo
|
||||
ver = "{}.{}.{}".format(*d["version_info"][:3])
|
||||
ver = "%s.%s.%s" % d["version_info"][:3]
|
||||
node._workerinfocache = s = "[{}] {} -- Python {} {}".format(
|
||||
d["id"], d["sysplatform"], ver, d["executable"]
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
_R = TypeVar("_R", bound="BaseReport")
|
||||
|
||||
|
||||
class BaseReport:
|
||||
when: str | None
|
||||
location: tuple[str, int | None, str] | None
|
||||
longrepr: (
|
||||
None | ExceptionInfo[BaseException] | tuple[str, int, str] | str | TerminalRepr
|
||||
)
|
||||
sections: list[tuple[str, str]]
|
||||
when: Optional[str]
|
||||
location: Optional[Tuple[str, Optional[int], str]]
|
||||
longrepr: Union[
|
||||
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
|
||||
]
|
||||
sections: List[Tuple[str, str]]
|
||||
nodeid: str
|
||||
outcome: Literal["passed", "failed", "skipped"]
|
||||
outcome: "Literal['passed', 'failed', 'skipped']"
|
||||
|
||||
def __init__(self, **kw: Any) -> None:
|
||||
self.__dict__.update(kw)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Can have arbitrary fields given to __init__().
|
||||
def __getattr__(self, key: str) -> Any: ...
|
||||
def __getattr__(self, key: str) -> Any:
|
||||
...
|
||||
|
||||
def toterminal(self, out: TerminalWriter) -> None:
|
||||
if hasattr(self, "node"):
|
||||
@@ -95,7 +94,7 @@ class BaseReport:
|
||||
s = "<unprintable longrepr>"
|
||||
out.line(s)
|
||||
|
||||
def get_sections(self, prefix: str) -> Iterator[tuple[str, str]]:
|
||||
def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]:
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
@@ -177,7 +176,7 @@ class BaseReport:
|
||||
return True
|
||||
|
||||
@property
|
||||
def head_line(self) -> str | None:
|
||||
def head_line(self) -> Optional[str]:
|
||||
"""**Experimental** The head line shown with longrepr output for this
|
||||
report, more commonly during traceback representation during
|
||||
failures::
|
||||
@@ -193,32 +192,17 @@ class BaseReport:
|
||||
even in patch releases.
|
||||
"""
|
||||
if self.location is not None:
|
||||
_fspath, _lineno, domain = self.location
|
||||
fspath, lineno, domain = self.location
|
||||
return domain
|
||||
return None
|
||||
|
||||
def _get_verbose_word_with_markup(
|
||||
self, config: Config, default_markup: Mapping[str, bool]
|
||||
) -> tuple[str, Mapping[str, bool]]:
|
||||
def _get_verbose_word(self, config: Config):
|
||||
_category, _short, verbose = config.hook.pytest_report_teststatus(
|
||||
report=self, config=config
|
||||
)
|
||||
return verbose
|
||||
|
||||
if isinstance(verbose, str):
|
||||
return verbose, default_markup
|
||||
|
||||
if isinstance(verbose, Sequence) and len(verbose) == 2:
|
||||
word, markup = verbose
|
||||
if isinstance(word, str) and isinstance(markup, Mapping):
|
||||
return word, markup
|
||||
|
||||
fail( # pragma: no cover
|
||||
"pytest_report_teststatus() hook (from a plugin) returned "
|
||||
f"an invalid verbose value: {verbose!r}.\nExpected either a string "
|
||||
"or a tuple of (word, markup)."
|
||||
)
|
||||
|
||||
def _to_json(self) -> dict[str, Any]:
|
||||
def _to_json(self) -> Dict[str, Any]:
|
||||
"""Return the contents of this report as a dict of builtin entries,
|
||||
suitable for serialization.
|
||||
|
||||
@@ -229,7 +213,7 @@ class BaseReport:
|
||||
return _report_to_json(self)
|
||||
|
||||
@classmethod
|
||||
def _from_json(cls, reportdict: dict[str, object]) -> Self:
|
||||
def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R:
|
||||
"""Create either a TestReport or CollectReport, depending on the calling class.
|
||||
|
||||
It is the callers responsibility to know which class to pass here.
|
||||
@@ -243,65 +227,20 @@ class BaseReport:
|
||||
|
||||
|
||||
def _report_unserialization_failure(
|
||||
type_name: str, report_class: type[BaseReport], reportdict
|
||||
type_name: str, report_class: Type[BaseReport], reportdict
|
||||
) -> NoReturn:
|
||||
url = "https://github.com/pytest-dev/pytest/issues"
|
||||
stream = StringIO()
|
||||
pprint("-" * 100, stream=stream)
|
||||
pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream)
|
||||
pprint(f"report_name: {report_class}", stream=stream)
|
||||
pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream)
|
||||
pprint("report_name: %s" % report_class, stream=stream)
|
||||
pprint(reportdict, stream=stream)
|
||||
pprint(f"Please report this bug at {url}", stream=stream)
|
||||
pprint("Please report this bug at %s" % url, stream=stream)
|
||||
pprint("-" * 100, stream=stream)
|
||||
raise RuntimeError(stream.getvalue())
|
||||
|
||||
|
||||
def _format_failed_longrepr(
|
||||
item: Item, call: CallInfo[None], excinfo: ExceptionInfo[BaseException]
|
||||
):
|
||||
if call.when == "call":
|
||||
longrepr = item.repr_failure(excinfo)
|
||||
else:
|
||||
# Exception in setup or teardown.
|
||||
longrepr = item._repr_failure_py(
|
||||
excinfo, style=item.config.getoption("tbstyle", "auto")
|
||||
)
|
||||
return longrepr
|
||||
|
||||
|
||||
def _format_exception_group_all_skipped_longrepr(
|
||||
item: Item,
|
||||
excinfo: ExceptionInfo[BaseExceptionGroup[BaseException | BaseExceptionGroup]],
|
||||
) -> tuple[str, int, str]:
|
||||
r = excinfo._getreprcrash()
|
||||
assert r is not None, (
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
if all(
|
||||
getattr(skip, "_use_item_location", False) for skip in excinfo.value.exceptions
|
||||
):
|
||||
path, line = item.reportinfo()[:2]
|
||||
assert line is not None
|
||||
loc = (os.fspath(path), line + 1)
|
||||
default_msg = "skipped"
|
||||
else:
|
||||
loc = (str(r.path), r.lineno)
|
||||
default_msg = r.message
|
||||
|
||||
# Get all unique skip messages.
|
||||
msgs: list[str] = []
|
||||
for exception in excinfo.value.exceptions:
|
||||
m = getattr(exception, "msg", None) or (
|
||||
exception.args[0] if exception.args else None
|
||||
)
|
||||
if m and m not in msgs:
|
||||
msgs.append(m)
|
||||
|
||||
reason = "; ".join(msgs) if msgs else default_msg
|
||||
longrepr = (*loc, reason)
|
||||
return longrepr
|
||||
|
||||
|
||||
@final
|
||||
class TestReport(BaseReport):
|
||||
"""Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
@@ -311,27 +250,21 @@ class TestReport(BaseReport):
|
||||
|
||||
__test__ = False
|
||||
|
||||
# Defined by skipping plugin.
|
||||
# xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish.
|
||||
wasxfail: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nodeid: str,
|
||||
location: tuple[str, int | None, str],
|
||||
location: Tuple[str, Optional[int], str],
|
||||
keywords: Mapping[str, Any],
|
||||
outcome: Literal["passed", "failed", "skipped"],
|
||||
longrepr: None
|
||||
| ExceptionInfo[BaseException]
|
||||
| tuple[str, int, str]
|
||||
| str
|
||||
| TerminalRepr,
|
||||
when: Literal["setup", "call", "teardown"],
|
||||
sections: Iterable[tuple[str, str]] = (),
|
||||
outcome: "Literal['passed', 'failed', 'skipped']",
|
||||
longrepr: Union[
|
||||
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
|
||||
],
|
||||
when: "Literal['setup', 'call', 'teardown']",
|
||||
sections: Iterable[Tuple[str, str]] = (),
|
||||
duration: float = 0,
|
||||
start: float = 0,
|
||||
stop: float = 0,
|
||||
user_properties: Iterable[tuple[str, object]] | None = None,
|
||||
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
||||
**extra,
|
||||
) -> None:
|
||||
#: Normalized collection nodeid.
|
||||
@@ -342,7 +275,7 @@ class TestReport(BaseReport):
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
#: The filesystempath may be relative to ``config.rootdir``.
|
||||
#: The line number is 0-based.
|
||||
self.location: tuple[str, int | None, str] = location
|
||||
self.location: Tuple[str, Optional[int], str] = location
|
||||
|
||||
#: A name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
@@ -355,7 +288,7 @@ class TestReport(BaseReport):
|
||||
self.longrepr = longrepr
|
||||
|
||||
#: One of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when: Literal["setup", "call", "teardown"] = when
|
||||
self.when = when
|
||||
|
||||
#: User properties is a list of tuples (name, value) that holds user
|
||||
#: defined properties of the test.
|
||||
@@ -378,10 +311,12 @@ class TestReport(BaseReport):
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>"
|
||||
return "<{} {!r} when={!r} outcome={!r}>".format(
|
||||
self.__class__.__name__, self.nodeid, self.when, self.outcome
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport:
|
||||
def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
|
||||
"""Create and fill a TestReport with standard item and call info.
|
||||
|
||||
:param item: The item.
|
||||
@@ -398,13 +333,13 @@ class TestReport(BaseReport):
|
||||
sections = []
|
||||
if not call.excinfo:
|
||||
outcome: Literal["passed", "failed", "skipped"] = "passed"
|
||||
longrepr: (
|
||||
None
|
||||
| ExceptionInfo[BaseException]
|
||||
| tuple[str, int, str]
|
||||
| str
|
||||
| TerminalRepr
|
||||
) = None
|
||||
longrepr: Union[
|
||||
None,
|
||||
ExceptionInfo[BaseException],
|
||||
Tuple[str, int, str],
|
||||
str,
|
||||
TerminalRepr,
|
||||
] = None
|
||||
else:
|
||||
if not isinstance(excinfo, ExceptionInfo):
|
||||
outcome = "failed"
|
||||
@@ -412,30 +347,23 @@ class TestReport(BaseReport):
|
||||
elif isinstance(excinfo.value, skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
assert r is not None, (
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
assert (
|
||||
r is not None
|
||||
), "There should always be a traceback entry for skipping a test."
|
||||
if excinfo.value._use_item_location:
|
||||
path, line = item.reportinfo()[:2]
|
||||
assert line is not None
|
||||
longrepr = (os.fspath(path), line + 1, r.message)
|
||||
longrepr = os.fspath(path), line + 1, r.message
|
||||
else:
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
elif isinstance(excinfo.value, BaseExceptionGroup) and (
|
||||
excinfo.value.split(skip.Exception)[1] is None
|
||||
):
|
||||
# All exceptions in the group are skip exceptions.
|
||||
outcome = "skipped"
|
||||
excinfo = cast(
|
||||
ExceptionInfo[
|
||||
BaseExceptionGroup[BaseException | BaseExceptionGroup]
|
||||
],
|
||||
excinfo,
|
||||
)
|
||||
longrepr = _format_exception_group_all_skipped_longrepr(item, excinfo)
|
||||
else:
|
||||
outcome = "failed"
|
||||
longrepr = _format_failed_longrepr(item, call, excinfo)
|
||||
if call.when == "call":
|
||||
longrepr = item.repr_failure(excinfo)
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(
|
||||
excinfo, style=item.config.getoption("tbstyle", "auto")
|
||||
)
|
||||
for rwhen, key, content in item._report_sections:
|
||||
sections.append((f"Captured {key} {rwhen}", content))
|
||||
return cls(
|
||||
@@ -465,14 +393,12 @@ class CollectReport(BaseReport):
|
||||
def __init__(
|
||||
self,
|
||||
nodeid: str,
|
||||
outcome: Literal["passed", "failed", "skipped"],
|
||||
longrepr: None
|
||||
| ExceptionInfo[BaseException]
|
||||
| tuple[str, int, str]
|
||||
| str
|
||||
| TerminalRepr,
|
||||
result: list[Item | Collector] | None,
|
||||
sections: Iterable[tuple[str, str]] = (),
|
||||
outcome: "Literal['passed', 'failed', 'skipped']",
|
||||
longrepr: Union[
|
||||
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
|
||||
],
|
||||
result: Optional[List[Union[Item, Collector]]],
|
||||
sections: Iterable[Tuple[str, str]] = (),
|
||||
**extra,
|
||||
) -> None:
|
||||
#: Normalized collection nodeid.
|
||||
@@ -498,11 +424,13 @@ class CollectReport(BaseReport):
|
||||
@property
|
||||
def location( # type:ignore[override]
|
||||
self,
|
||||
) -> tuple[str, int | None, str] | None:
|
||||
) -> Optional[Tuple[str, Optional[int], str]]:
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<CollectReport {self.nodeid!r} lenresult={len(self.result)} outcome={self.outcome!r}>"
|
||||
return "<CollectReport {!r} lenresult={} outcome={!r}>".format(
|
||||
self.nodeid, len(self.result), self.outcome
|
||||
)
|
||||
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
@@ -514,9 +442,9 @@ class CollectErrorRepr(TerminalRepr):
|
||||
|
||||
|
||||
def pytest_report_to_serializable(
|
||||
report: CollectReport | TestReport,
|
||||
) -> dict[str, Any] | None:
|
||||
if isinstance(report, TestReport | CollectReport):
|
||||
report: Union[CollectReport, TestReport]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if isinstance(report, (TestReport, CollectReport)):
|
||||
data = report._to_json()
|
||||
data["$report_type"] = report.__class__.__name__
|
||||
return data
|
||||
@@ -525,8 +453,8 @@ def pytest_report_to_serializable(
|
||||
|
||||
|
||||
def pytest_report_from_serializable(
|
||||
data: dict[str, Any],
|
||||
) -> CollectReport | TestReport | None:
|
||||
data: Dict[str, Any],
|
||||
) -> Optional[Union[CollectReport, TestReport]]:
|
||||
if "$report_type" in data:
|
||||
if data["$report_type"] == "TestReport":
|
||||
return TestReport._from_json(data)
|
||||
@@ -538,7 +466,7 @@ def pytest_report_from_serializable(
|
||||
return None
|
||||
|
||||
|
||||
def _report_to_json(report: BaseReport) -> dict[str, Any]:
|
||||
def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
||||
"""Return the contents of this report as a dict of builtin entries,
|
||||
suitable for serialization.
|
||||
|
||||
@@ -546,8 +474,8 @@ def _report_to_json(report: BaseReport) -> dict[str, Any]:
|
||||
"""
|
||||
|
||||
def serialize_repr_entry(
|
||||
entry: ReprEntry | ReprEntryNative,
|
||||
) -> dict[str, Any]:
|
||||
entry: Union[ReprEntry, ReprEntryNative]
|
||||
) -> Dict[str, Any]:
|
||||
data = dataclasses.asdict(entry)
|
||||
for key, value in data.items():
|
||||
if hasattr(value, "__dict__"):
|
||||
@@ -555,7 +483,7 @@ def _report_to_json(report: BaseReport) -> dict[str, Any]:
|
||||
entry_data = {"type": type(entry).__name__, "data": data}
|
||||
return entry_data
|
||||
|
||||
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> dict[str, Any]:
|
||||
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
|
||||
result = dataclasses.asdict(reprtraceback)
|
||||
result["reprentries"] = [
|
||||
serialize_repr_entry(x) for x in reprtraceback.reprentries
|
||||
@@ -563,18 +491,18 @@ def _report_to_json(report: BaseReport) -> dict[str, Any]:
|
||||
return result
|
||||
|
||||
def serialize_repr_crash(
|
||||
reprcrash: ReprFileLocation | None,
|
||||
) -> dict[str, Any] | None:
|
||||
reprcrash: Optional[ReprFileLocation],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if reprcrash is not None:
|
||||
return dataclasses.asdict(reprcrash)
|
||||
else:
|
||||
return None
|
||||
|
||||
def serialize_exception_longrepr(rep: BaseReport) -> dict[str, Any]:
|
||||
def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
|
||||
assert rep.longrepr is not None
|
||||
# TODO: Investigate whether the duck typing is really necessary here.
|
||||
longrepr = cast(ExceptionRepr, rep.longrepr)
|
||||
result: dict[str, Any] = {
|
||||
result: Dict[str, Any] = {
|
||||
"reprcrash": serialize_repr_crash(longrepr.reprcrash),
|
||||
"reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
|
||||
"sections": longrepr.sections,
|
||||
@@ -611,7 +539,7 @@ def _report_to_json(report: BaseReport) -> dict[str, Any]:
|
||||
return d
|
||||
|
||||
|
||||
def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]:
|
||||
def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Return **kwargs that can be used to construct a TestReport or
|
||||
CollectReport instance.
|
||||
|
||||
@@ -632,7 +560,7 @@ def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]:
|
||||
if data["reprlocals"]:
|
||||
reprlocals = ReprLocals(data["reprlocals"]["lines"])
|
||||
|
||||
reprentry: ReprEntry | ReprEntryNative = ReprEntry(
|
||||
reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
|
||||
lines=data["lines"],
|
||||
reprfuncargs=reprfuncargs,
|
||||
reprlocals=reprlocals,
|
||||
@@ -651,7 +579,7 @@ def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]:
|
||||
]
|
||||
return ReprTraceback(**repr_traceback_dict)
|
||||
|
||||
def deserialize_repr_crash(repr_crash_dict: dict[str, Any] | None):
|
||||
def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
|
||||
if repr_crash_dict is not None:
|
||||
return ReprFileLocation(**repr_crash_dict)
|
||||
else:
|
||||
@@ -678,9 +606,9 @@ def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]:
|
||||
description,
|
||||
)
|
||||
)
|
||||
exception_info: ExceptionChainRepr | ReprExceptionInfo = ExceptionChainRepr(
|
||||
chain
|
||||
)
|
||||
exception_info: Union[
|
||||
ExceptionChainRepr, ReprExceptionInfo
|
||||
] = ExceptionChainRepr(chain)
|
||||
else:
|
||||
exception_info = ReprExceptionInfo(
|
||||
reprtraceback=reprtraceback,
|
||||
|
||||
Reference in New Issue
Block a user