This commit is contained in:
Iliyan Angelov
2025-12-06 03:27:35 +02:00
parent 7667eb5eda
commit 5a8ca3c475
2211 changed files with 28086 additions and 37066 deletions

View File

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