Pytest impl hooks to alter terminal output
27-05-2025
API:
- https://docs.pytest.org/en/stable/reference/reference.html#pytest.Item.add_report_section
- https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_terminal_summary
- https://docs.pytest.org/en/latest/reference/reference.html#pytest.hookspec.pytest_report_teststatus
Hooks:
- https://docs.pytest.org/en/stable/how-to/writing_hook_functions.html#
- https://stackoverflow.com/questions/56865065/how-to-add-custom-sections-to-terminal-report-in-pytest
Terminal reporter:
Refs:
- https://paragkamble.medium.com/understanding-hooks-in-pytest-892e91edbdb7
- https://pytest-with-eric.com/hooks/pytest-hooks/
- https://pytest-with-eric.com/pytest-best-practices/pytest-plugins/
import warnings
from collections import defaultdict
import pytest
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter
@pytest.hookimpl()
def pytest_sessionstart(session):
print("Hello from `pytest_sessionstart`")
@pytest.hookimpl()
def pytest_sessionfinish(session, exitstatus):
print("Hello from `pytest_sessionfinish` hook {exitstatus=}")
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport(item, call: CallInfo):
# Let's ensure we are dealing with a test report
if call.when == "call":
outcome = call.excinfo
try:
# Access the test outcome (passed, failed, etc.)
test_outcome = "failed" if outcome else "passed"
# Access the test duration
test_duration = call.duration
# Access the test ID (nodeid)
test_id = item.nodeid
# Print Test Outcome and Duration
print(f"Test: {test_id}=, {test_outcome=}, {test_duration=:.5f} seconds")
except Exception as e:
print("ERROR:", e)
@pytest.hookimpl()
def pytest_warning_recorded(warning_message: warnings.WarningMessage, nodeid: str):
"""
This hook is called when a warning is recorded during test execution.
It allows us to capture and process warnings.
"""
# print(nodeid)
return None
@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(terminalreporter: TerminalReporter, exitstatus, config):
terminal_warnings = terminalreporter.stats.get("warnings", [])
if len(terminal_warnings) == 0:
return None
warning_location_to_nodeid = defaultdict(set)
warning_count_by_nodeid = defaultdict(int)
for report in terminal_warnings:
nodeid = report.nodeid
warning_count_by_nodeid[nodeid] += 1
warning_location = report.fslocation
warning_location_to_nodeid[warning_location].add(nodeid)
# sort the warning_count_by_nodeid by count
sorted_warnings = sorted(warning_count_by_nodeid.items(), key=lambda x: x[1], reverse=True)
terminalreporter.write_line("Warnings Summary by nodeid:", bold=True)
for nodeid, count in sorted_warnings:
terminalreporter.write_line(f"{nodeid}: {count} warnings", yellow=True)
sorted_location_warnings = sorted(
warning_location_to_nodeid.items(), key=lambda x: len(x[1]), reverse=True
)
terminalreporter.write_line("Warnings Summary by type:", bold=True)
for warning_location, nodeids in sorted_location_warnings:
warning_location_str = "::".join([str(w) for w in warning_location])
terminalreporter.write_line(f"{warning_location_str}: {len(nodeids)} warnings", yellow=True)
for nodeid in nodeids:
terminalreporter.write_line(f" - {nodeid}")