updates
This commit is contained in:
@@ -1,22 +1,20 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Basic collect and runtest protocol implementations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import bdb
|
||||
from collections.abc import Callable
|
||||
import dataclasses
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import final
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import Literal
|
||||
from typing import List
|
||||
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 .config import Config
|
||||
from .reports import BaseReport
|
||||
from .reports import CollectErrorRepr
|
||||
from .reports import CollectReport
|
||||
@@ -25,10 +23,10 @@ from _pytest import timing
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest.compat import final
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Directory
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.nodes import Node
|
||||
from _pytest.outcomes import Exit
|
||||
@@ -36,11 +34,12 @@ from _pytest.outcomes import OutcomeException
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
from _pytest.main import Session
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
@@ -62,21 +61,19 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--durations-min",
|
||||
action="store",
|
||||
type=float,
|
||||
default=None,
|
||||
default=0.005,
|
||||
metavar="N",
|
||||
help="Minimal duration in seconds for inclusion in slowest list. "
|
||||
"Default: 0.005 (or 0.0 if -vv is given).",
|
||||
"Default: 0.005.",
|
||||
)
|
||||
|
||||
|
||||
def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
|
||||
def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
|
||||
durations = terminalreporter.config.option.durations
|
||||
durations_min = terminalreporter.config.option.durations_min
|
||||
verbose = terminalreporter.config.get_verbosity()
|
||||
verbose = terminalreporter.config.getvalue("verbose")
|
||||
if durations is None:
|
||||
return
|
||||
if durations_min is None:
|
||||
durations_min = 0.005 if verbose < 2 else 0.0
|
||||
tr = terminalreporter
|
||||
dlist = []
|
||||
for replist in tr.stats.values():
|
||||
@@ -85,34 +82,33 @@ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
|
||||
dlist.append(rep)
|
||||
if not dlist:
|
||||
return
|
||||
dlist.sort(key=lambda x: x.duration, reverse=True)
|
||||
dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
|
||||
if not durations:
|
||||
tr.write_sep("=", "slowest durations")
|
||||
else:
|
||||
tr.write_sep("=", f"slowest {durations} durations")
|
||||
tr.write_sep("=", "slowest %s durations" % durations)
|
||||
dlist = dlist[:durations]
|
||||
|
||||
for i, rep in enumerate(dlist):
|
||||
if rep.duration < durations_min:
|
||||
if verbose < 2 and rep.duration < durations_min:
|
||||
tr.write_line("")
|
||||
message = f"({len(dlist) - i} durations < {durations_min:g}s hidden."
|
||||
if terminalreporter.config.option.durations_min is None:
|
||||
message += " Use -vv to show these durations."
|
||||
message += ")"
|
||||
tr.write_line(message)
|
||||
tr.write_line(
|
||||
"(%s durations < %gs hidden. Use -vv to show these durations.)"
|
||||
% (len(dlist) - i, durations_min)
|
||||
)
|
||||
break
|
||||
tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
|
||||
|
||||
|
||||
def pytest_sessionstart(session: Session) -> None:
|
||||
def pytest_sessionstart(session: "Session") -> None:
|
||||
session._setupstate = SetupState()
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: Session) -> None:
|
||||
def pytest_sessionfinish(session: "Session") -> None:
|
||||
session._setupstate.teardown_exact(None)
|
||||
|
||||
|
||||
def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool:
|
||||
def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
|
||||
ihook = item.ihook
|
||||
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
||||
runtestprotocol(item, nextitem=nextitem)
|
||||
@@ -121,8 +117,8 @@ def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool:
|
||||
|
||||
|
||||
def runtestprotocol(
|
||||
item: Item, log: bool = True, nextitem: Item | None = None
|
||||
) -> list[TestReport]:
|
||||
item: Item, log: bool = True, nextitem: Optional[Item] = None
|
||||
) -> List[TestReport]:
|
||||
hasrequest = hasattr(item, "_request")
|
||||
if hasrequest and not item._request: # type: ignore[attr-defined]
|
||||
# This only happens if the item is re-run, as is done by
|
||||
@@ -135,10 +131,6 @@ def runtestprotocol(
|
||||
show_test_item(item)
|
||||
if not item.config.getoption("setuponly", False):
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
# If the session is about to fail or stop, teardown everything - this is
|
||||
# necessary to correctly report fixture teardown errors (see #11706)
|
||||
if item.session.shouldfail or item.session.shouldstop:
|
||||
nextitem = None
|
||||
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
|
||||
# After all teardown hooks have been called
|
||||
# want funcargs and request info to go away.
|
||||
@@ -171,8 +163,6 @@ def pytest_runtest_call(item: Item) -> None:
|
||||
del sys.last_type
|
||||
del sys.last_value
|
||||
del sys.last_traceback
|
||||
if sys.version_info >= (3, 12, 0):
|
||||
del sys.last_exc # type:ignore[attr-defined]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
@@ -181,22 +171,20 @@ def pytest_runtest_call(item: Item) -> None:
|
||||
# Store trace info to allow postmortem debugging
|
||||
sys.last_type = type(e)
|
||||
sys.last_value = e
|
||||
if sys.version_info >= (3, 12, 0):
|
||||
sys.last_exc = e # type:ignore[attr-defined]
|
||||
assert e.__traceback__ is not None
|
||||
# Skip *this* frame
|
||||
sys.last_traceback = e.__traceback__.tb_next
|
||||
raise
|
||||
raise e
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
|
||||
def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
|
||||
_update_current_test_var(item, "teardown")
|
||||
item.session._setupstate.teardown_exact(nextitem)
|
||||
_update_current_test_var(item, None)
|
||||
|
||||
|
||||
def _update_current_test_var(
|
||||
item: Item, when: Literal["setup", "call", "teardown"] | None
|
||||
item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
|
||||
) -> None:
|
||||
"""Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
|
||||
|
||||
@@ -212,7 +200,7 @@ def _update_current_test_var(
|
||||
os.environ.pop(var_name)
|
||||
|
||||
|
||||
def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
|
||||
def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
|
||||
if report.when in ("setup", "teardown"):
|
||||
if report.failed:
|
||||
# category, shortletter, verbose-word
|
||||
@@ -229,40 +217,19 @@ def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
|
||||
|
||||
|
||||
def call_and_report(
|
||||
item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds
|
||||
item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds
|
||||
) -> TestReport:
|
||||
ihook = item.ihook
|
||||
if when == "setup":
|
||||
runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup
|
||||
elif when == "call":
|
||||
runtest_hook = ihook.pytest_runtest_call
|
||||
elif when == "teardown":
|
||||
runtest_hook = ihook.pytest_runtest_teardown
|
||||
else:
|
||||
assert False, f"Unhandled runtest hook case: {when}"
|
||||
|
||||
call = CallInfo.from_call(
|
||||
lambda: runtest_hook(item=item, **kwds),
|
||||
when=when,
|
||||
reraise=get_reraise_exceptions(item.config),
|
||||
)
|
||||
report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call)
|
||||
call = call_runtest_hook(item, when, **kwds)
|
||||
hook = item.ihook
|
||||
report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
|
||||
if log:
|
||||
ihook.pytest_runtest_logreport(report=report)
|
||||
hook.pytest_runtest_logreport(report=report)
|
||||
if check_interactive_exception(call, report):
|
||||
ihook.pytest_exception_interact(node=item, call=call, report=report)
|
||||
hook.pytest_exception_interact(node=item, call=call, report=report)
|
||||
return report
|
||||
|
||||
|
||||
def get_reraise_exceptions(config: Config) -> tuple[type[BaseException], ...]:
|
||||
"""Return exception types that should not be suppressed in general."""
|
||||
reraise: tuple[type[BaseException], ...] = (Exit,)
|
||||
if not config.getoption("usepdb", False):
|
||||
reraise += (KeyboardInterrupt,)
|
||||
return reraise
|
||||
|
||||
|
||||
def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool:
|
||||
def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
|
||||
"""Check whether the call raised an exception that should be reported as
|
||||
interactive."""
|
||||
if call.excinfo is None:
|
||||
@@ -271,12 +238,31 @@ def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> b
|
||||
if hasattr(report, "wasxfail"):
|
||||
# Exception was expected.
|
||||
return False
|
||||
if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit):
|
||||
if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)):
|
||||
# Special control flow exception.
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def call_runtest_hook(
|
||||
item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds
|
||||
) -> "CallInfo[None]":
|
||||
if when == "setup":
|
||||
ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
|
||||
elif when == "call":
|
||||
ihook = item.ihook.pytest_runtest_call
|
||||
elif when == "teardown":
|
||||
ihook = item.ihook.pytest_runtest_teardown
|
||||
else:
|
||||
assert False, f"Unhandled runtest hook case: {when}"
|
||||
reraise: Tuple[Type[BaseException], ...] = (Exit,)
|
||||
if not item.config.getoption("usepdb", False):
|
||||
reraise += (KeyboardInterrupt,)
|
||||
return CallInfo.from_call(
|
||||
lambda: ihook(item=item, **kwds), when=when, reraise=reraise
|
||||
)
|
||||
|
||||
|
||||
TResult = TypeVar("TResult", covariant=True)
|
||||
|
||||
|
||||
@@ -285,9 +271,9 @@ TResult = TypeVar("TResult", covariant=True)
|
||||
class CallInfo(Generic[TResult]):
|
||||
"""Result/Exception info of a function invocation."""
|
||||
|
||||
_result: TResult | None
|
||||
_result: Optional[TResult]
|
||||
#: The captured exception of the call, if it raised.
|
||||
excinfo: ExceptionInfo[BaseException] | None
|
||||
excinfo: Optional[ExceptionInfo[BaseException]]
|
||||
#: The system time when the call started, in seconds since the epoch.
|
||||
start: float
|
||||
#: The system time when the call ended, in seconds since the epoch.
|
||||
@@ -295,16 +281,16 @@ class CallInfo(Generic[TResult]):
|
||||
#: The call duration, in seconds.
|
||||
duration: float
|
||||
#: The context of invocation: "collect", "setup", "call" or "teardown".
|
||||
when: Literal["collect", "setup", "call", "teardown"]
|
||||
when: "Literal['collect', 'setup', 'call', 'teardown']"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
result: TResult | None,
|
||||
excinfo: ExceptionInfo[BaseException] | None,
|
||||
result: Optional[TResult],
|
||||
excinfo: Optional[ExceptionInfo[BaseException]],
|
||||
start: float,
|
||||
stop: float,
|
||||
duration: float,
|
||||
when: Literal["collect", "setup", "call", "teardown"],
|
||||
when: "Literal['collect', 'setup', 'call', 'teardown']",
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
@@ -332,15 +318,16 @@ class CallInfo(Generic[TResult]):
|
||||
@classmethod
|
||||
def from_call(
|
||||
cls,
|
||||
func: Callable[[], TResult],
|
||||
when: Literal["collect", "setup", "call", "teardown"],
|
||||
reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
|
||||
) -> CallInfo[TResult]:
|
||||
func: "Callable[[], TResult]",
|
||||
when: "Literal['collect', 'setup', 'call', 'teardown']",
|
||||
reraise: Optional[
|
||||
Union[Type[BaseException], Tuple[Type[BaseException], ...]]
|
||||
] = None,
|
||||
) -> "CallInfo[TResult]":
|
||||
"""Call func, wrapping the result in a CallInfo.
|
||||
|
||||
:param func:
|
||||
The function to call. Called without arguments.
|
||||
:type func: Callable[[], _pytest.runner.TResult]
|
||||
:param when:
|
||||
The phase in which the function is called.
|
||||
:param reraise:
|
||||
@@ -348,19 +335,23 @@ class CallInfo(Generic[TResult]):
|
||||
function, instead of being wrapped in the CallInfo.
|
||||
"""
|
||||
excinfo = None
|
||||
instant = timing.Instant()
|
||||
start = timing.time()
|
||||
precise_start = timing.perf_counter()
|
||||
try:
|
||||
result: TResult | None = func()
|
||||
result: Optional[TResult] = func()
|
||||
except BaseException:
|
||||
excinfo = ExceptionInfo.from_current()
|
||||
if reraise is not None and isinstance(excinfo.value, reraise):
|
||||
raise
|
||||
result = None
|
||||
duration = instant.elapsed()
|
||||
# use the perf counter
|
||||
precise_stop = timing.perf_counter()
|
||||
duration = precise_stop - precise_start
|
||||
stop = timing.time()
|
||||
return cls(
|
||||
start=duration.start.time,
|
||||
stop=duration.stop.time,
|
||||
duration=duration.seconds,
|
||||
start=start,
|
||||
stop=stop,
|
||||
duration=duration,
|
||||
when=when,
|
||||
result=result,
|
||||
excinfo=excinfo,
|
||||
@@ -378,36 +369,16 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
|
||||
|
||||
|
||||
def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||
def collect() -> list[Item | Collector]:
|
||||
# Before collecting, if this is a Directory, load the conftests.
|
||||
# If a conftest import fails to load, it is considered a collection
|
||||
# error of the Directory collector. This is why it's done inside of the
|
||||
# CallInfo wrapper.
|
||||
#
|
||||
# Note: initial conftests are loaded early, not here.
|
||||
if isinstance(collector, Directory):
|
||||
collector.config.pluginmanager._loadconftestmodules(
|
||||
collector.path,
|
||||
collector.config.getoption("importmode"),
|
||||
rootpath=collector.config.rootpath,
|
||||
consider_namespace_packages=collector.config.getini(
|
||||
"consider_namespace_packages"
|
||||
),
|
||||
)
|
||||
|
||||
return list(collector.collect())
|
||||
|
||||
call = CallInfo.from_call(
|
||||
collect, "collect", reraise=(KeyboardInterrupt, SystemExit)
|
||||
)
|
||||
longrepr: None | tuple[str, int, str] | str | TerminalRepr = None
|
||||
call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
|
||||
longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
|
||||
if not call.excinfo:
|
||||
outcome: Literal["passed", "skipped", "failed"] = "passed"
|
||||
else:
|
||||
skip_exceptions = [Skipped]
|
||||
unittest = sys.modules.get("unittest")
|
||||
if unittest is not None:
|
||||
skip_exceptions.append(unittest.SkipTest)
|
||||
# Type ignored because unittest is loaded dynamically.
|
||||
skip_exceptions.append(unittest.SkipTest) # type: ignore
|
||||
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
|
||||
outcome = "skipped"
|
||||
r_ = collector._repr_failure_py(call.excinfo, "line")
|
||||
@@ -494,13 +465,13 @@ class SetupState:
|
||||
|
||||
def __init__(self) -> None:
|
||||
# The stack is in the dict insertion order.
|
||||
self.stack: dict[
|
||||
self.stack: Dict[
|
||||
Node,
|
||||
tuple[
|
||||
Tuple[
|
||||
# Node's finalizers.
|
||||
list[Callable[[], object]],
|
||||
# Node's exception and original traceback, if its setup raised.
|
||||
tuple[OutcomeException | Exception, types.TracebackType | None] | None,
|
||||
List[Callable[[], object]],
|
||||
# Node's exception, if its setup raised.
|
||||
Optional[Union[OutcomeException, Exception]],
|
||||
],
|
||||
] = {}
|
||||
|
||||
@@ -513,7 +484,7 @@ class SetupState:
|
||||
for col, (finalizers, exc) in self.stack.items():
|
||||
assert col in needed_collectors, "previous item was not torn down properly"
|
||||
if exc:
|
||||
raise exc[0].with_traceback(exc[1])
|
||||
raise exc
|
||||
|
||||
for col in needed_collectors[len(self.stack) :]:
|
||||
assert col not in self.stack
|
||||
@@ -522,8 +493,8 @@ class SetupState:
|
||||
try:
|
||||
col.setup()
|
||||
except TEST_OUTCOME as exc:
|
||||
self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__))
|
||||
raise
|
||||
self.stack[col] = (self.stack[col][0], exc)
|
||||
raise exc
|
||||
|
||||
def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
|
||||
"""Attach a finalizer to the given node.
|
||||
@@ -535,15 +506,15 @@ class SetupState:
|
||||
assert node in self.stack, (node, self.stack)
|
||||
self.stack[node][0].append(finalizer)
|
||||
|
||||
def teardown_exact(self, nextitem: Item | None) -> None:
|
||||
def teardown_exact(self, nextitem: Optional[Item]) -> None:
|
||||
"""Teardown the current stack up until reaching nodes that nextitem
|
||||
also descends from.
|
||||
|
||||
When nextitem is None (meaning we're at the last item), the entire
|
||||
stack is torn down.
|
||||
"""
|
||||
needed_collectors = (nextitem and nextitem.listchain()) or []
|
||||
exceptions: list[BaseException] = []
|
||||
needed_collectors = nextitem and nextitem.listchain() or []
|
||||
exceptions: List[BaseException] = []
|
||||
while self.stack:
|
||||
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user