Merge 8c795126cf
into 4b88ee41bd
This commit is contained in:
commit
34d1950bf2
|
@ -39,6 +39,7 @@ repos:
|
|||
additional_dependencies:
|
||||
- flake8-builtins==1.5.3
|
||||
- flake8-typing-imports==1.12.0
|
||||
args: ["--min-python-version=3.9.0"]
|
||||
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.14.0
|
||||
|
|
|
@ -6,6 +6,11 @@ Versions follow `Semantic Versioning`_ (``<major>.<minor>.<patch>``).
|
|||
Version History
|
||||
---------------
|
||||
|
||||
Unreleased (2023-11-13)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* Type annotations for basereport.py (issue 434)
|
||||
|
||||
|
||||
4.1.1 (2023-11-07)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -96,12 +96,12 @@ version-file = "src/pytest_html/__version.py"
|
|||
path = "scripts/npm.py"
|
||||
|
||||
[tool.mypy]
|
||||
check_untyped_defs = false # TODO
|
||||
check_untyped_defs = false # TODO
|
||||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = false # TODO
|
||||
disallow_untyped_decorators = false # TODO
|
||||
disallow_untyped_defs = false # TODO
|
||||
ignore_missing_imports = true
|
||||
no_implicit_optional = true
|
||||
no_implicit_reexport = true
|
||||
|
|
|
@ -11,37 +11,55 @@ import warnings
|
|||
from collections import defaultdict
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import DefaultDict
|
||||
|
||||
import pytest
|
||||
from _pytest.config import Config
|
||||
from _pytest.main import Session
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from jinja2.environment import Template
|
||||
|
||||
from pytest_html import __version__
|
||||
from pytest_html import extras
|
||||
from pytest_html.report_data import ReportData
|
||||
|
||||
|
||||
class BaseReport:
|
||||
def __init__(self, report_path, config, report_data, template, css):
|
||||
self._report_path = (
|
||||
def __init__(
|
||||
self,
|
||||
report_path: Path,
|
||||
config: Config,
|
||||
report_data: ReportData,
|
||||
template: Template,
|
||||
css: str,
|
||||
) -> None:
|
||||
self._report_path: Path = (
|
||||
Path.cwd() / Path(os.path.expandvars(report_path)).expanduser()
|
||||
)
|
||||
self._report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._config = config
|
||||
self._template = template
|
||||
self._css = css
|
||||
self._max_asset_filename_length = int(
|
||||
self._config: Config = config
|
||||
self._template: Template = template
|
||||
self._css: str = css
|
||||
self._max_asset_filename_length: int = int(
|
||||
config.getini("max_asset_filename_length")
|
||||
)
|
||||
|
||||
self._reports = defaultdict(dict)
|
||||
self._report = report_data
|
||||
self._reports: DefaultDict = defaultdict(dict) # type: ignore
|
||||
self._report: ReportData = report_data
|
||||
self._report.title = self._report_path.name
|
||||
self._suite_start_time = time.time()
|
||||
self._suite_start_time: float = time.time()
|
||||
|
||||
@property
|
||||
def css(self):
|
||||
# implement in subclasses
|
||||
return
|
||||
|
||||
def _asset_filename(self, test_id, extra_index, test_index, file_extension):
|
||||
def _asset_filename(
|
||||
self, test_id: str, extra_index: int, test_index: int, file_extension: str
|
||||
) -> str:
|
||||
return "{}_{}_{}.{}".format(
|
||||
re.sub(r"[^\w.]", "_", test_id),
|
||||
str(extra_index),
|
||||
|
@ -49,7 +67,7 @@ class BaseReport:
|
|||
file_extension,
|
||||
)[-self._max_asset_filename_length :]
|
||||
|
||||
def _generate_report(self, self_contained=False):
|
||||
def _generate_report(self, self_contained: bool = False) -> None:
|
||||
generated = datetime.datetime.now()
|
||||
test_data = self._report.data
|
||||
test_data = json.dumps(test_data)
|
||||
|
@ -70,7 +88,7 @@ class BaseReport:
|
|||
|
||||
self._write_report(rendered_report)
|
||||
|
||||
def _generate_environment(self):
|
||||
def _generate_environment(self) -> Any:
|
||||
try:
|
||||
from pytest_metadata.plugin import metadata_key
|
||||
|
||||
|
@ -91,7 +109,7 @@ class BaseReport:
|
|||
|
||||
return metadata
|
||||
|
||||
def _is_redactable_environment_variable(self, environment_variable):
|
||||
def _is_redactable_environment_variable(self, environment_variable: str) -> bool:
|
||||
redactable_regexes = self._config.getini("environment_table_redact_list")
|
||||
for redactable_regex in redactable_regexes:
|
||||
if re.match(redactable_regex, environment_variable):
|
||||
|
@ -99,13 +117,13 @@ class BaseReport:
|
|||
|
||||
return False
|
||||
|
||||
def _data_content(self, *args, **kwargs):
|
||||
def _data_content(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||
pass
|
||||
|
||||
def _media_content(self, *args, **kwargs):
|
||||
def _media_content(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
||||
pass
|
||||
|
||||
def _process_extras(self, report, test_id):
|
||||
def _process_extras(self, report: CollectReport, test_id: str) -> list[Any]:
|
||||
test_index = hasattr(report, "rerun") and report.rerun + 1 or 0
|
||||
report_extras = getattr(report, "extras", [])
|
||||
for extra_index, extra in enumerate(report_extras):
|
||||
|
@ -118,30 +136,30 @@ class BaseReport:
|
|||
)
|
||||
if extra["format_type"] == extras.FORMAT_JSON:
|
||||
content = json.dumps(content)
|
||||
extra["content"] = self._data_content(
|
||||
extra["content"] = self._data_content( # type: ignore[no-untyped-call]
|
||||
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
||||
)
|
||||
|
||||
if extra["format_type"] == extras.FORMAT_TEXT:
|
||||
if isinstance(content, bytes):
|
||||
content = content.decode("utf-8")
|
||||
extra["content"] = self._data_content(
|
||||
extra["content"] = self._data_content( # type: ignore[no-untyped-call]
|
||||
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
||||
)
|
||||
|
||||
if extra["format_type"] in [extras.FORMAT_IMAGE, extras.FORMAT_VIDEO]:
|
||||
extra["content"] = self._media_content(
|
||||
extra["content"] = self._media_content( # type: ignore[no-untyped-call]
|
||||
content, asset_name=asset_name, mime_type=extra["mime_type"]
|
||||
)
|
||||
|
||||
return report_extras
|
||||
|
||||
def _write_report(self, rendered_report):
|
||||
def _write_report(self, rendered_report: str) -> None:
|
||||
with self._report_path.open("w", encoding="utf-8") as f:
|
||||
f.write(rendered_report)
|
||||
|
||||
def _run_count(self):
|
||||
relevant_outcomes = ["passed", "failed", "xpassed", "xfailed"]
|
||||
def _run_count(self) -> str:
|
||||
relevant_outcomes: list[str] = ["passed", "failed", "xpassed", "xfailed"]
|
||||
counts = 0
|
||||
for outcome in self._report.outcomes.keys():
|
||||
if outcome in relevant_outcomes:
|
||||
|
@ -155,7 +173,7 @@ class BaseReport:
|
|||
|
||||
return f"{counts}/{self._report.collected_items} {'tests' if plural else 'test'} done."
|
||||
|
||||
def _hydrate_data(self, data, cells):
|
||||
def _hydrate_data(self, data: dict[str, list], cells: list[str]) -> None:
|
||||
for index, cell in enumerate(cells):
|
||||
# extract column name and data if column is sortable
|
||||
if "sortable" in self._report.table_header[index]:
|
||||
|
@ -165,7 +183,7 @@ class BaseReport:
|
|||
data[name_match.group(1)] = data_match.group(1)
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session):
|
||||
def pytest_sessionstart(self, session: Session) -> None:
|
||||
self._report.set_data("environment", self._generate_environment())
|
||||
|
||||
session.config.hook.pytest_html_report_title(report=self._report)
|
||||
|
@ -179,7 +197,7 @@ class BaseReport:
|
|||
self._generate_report()
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionfinish(self, session):
|
||||
def pytest_sessionfinish(self, session: Session) -> None:
|
||||
session.config.hook.pytest_html_results_summary(
|
||||
prefix=self._report.additional_summary["prefix"],
|
||||
summary=self._report.additional_summary["summary"],
|
||||
|
@ -192,23 +210,23 @@ class BaseReport:
|
|||
self._generate_report()
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
|
||||
terminalreporter.write_sep(
|
||||
"-",
|
||||
f"Generated html report: {self._report_path.as_uri()}",
|
||||
)
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_collectreport(self, report):
|
||||
def pytest_collectreport(self, report: CollectReport) -> None:
|
||||
if report.failed:
|
||||
self._process_report(report, 0, [])
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_collection_finish(self, session):
|
||||
def pytest_collection_finish(self, session: Session) -> None:
|
||||
self._report.collected_items = len(session.items)
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_runtest_logreport(self, report):
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||
if hasattr(report, "duration_formatter"):
|
||||
warnings.warn(
|
||||
"'duration_formatter' has been removed and no longer has any effect!"
|
||||
|
@ -259,7 +277,9 @@ class BaseReport:
|
|||
if self._config.getini("generate_report_on_test"):
|
||||
self._generate_report()
|
||||
|
||||
def _process_report(self, report, duration, processed_extras):
|
||||
def _process_report(
|
||||
self, report: TestReport, duration: int, processed_extras: list
|
||||
) -> None:
|
||||
outcome = _process_outcome(report)
|
||||
try:
|
||||
# hook returns as list for some reason
|
||||
|
@ -304,7 +324,7 @@ class BaseReport:
|
|||
self._report.add_test(data, report, outcome, processed_logs)
|
||||
|
||||
|
||||
def _format_duration(duration):
|
||||
def _format_duration(duration: float) -> str:
|
||||
if duration < 1:
|
||||
return f"{round(duration * 1000)} ms"
|
||||
|
||||
|
@ -317,13 +337,13 @@ def _format_duration(duration):
|
|||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def _is_error(report):
|
||||
def _is_error(report: BaseReport) -> bool:
|
||||
return (
|
||||
report.when in ["setup", "teardown", "collect"] and report.outcome == "failed"
|
||||
)
|
||||
|
||||
|
||||
def _process_logs(report):
|
||||
def _process_logs(report) -> list[str]:
|
||||
log = []
|
||||
if report.longreprtext:
|
||||
log.append(escape(report.longreprtext) + "\n")
|
||||
|
@ -343,7 +363,7 @@ def _process_logs(report):
|
|||
return log
|
||||
|
||||
|
||||
def _process_outcome(report):
|
||||
def _process_outcome(report: TestReport) -> str:
|
||||
if _is_error(report):
|
||||
return "Error"
|
||||
if hasattr(report, "wasxfail"):
|
||||
|
@ -355,12 +375,12 @@ def _process_outcome(report):
|
|||
return report.outcome.capitalize()
|
||||
|
||||
|
||||
def _process_links(links):
|
||||
def _process_links(links) -> str:
|
||||
a_tag = '<a target="_blank" href="{content}" class="col-links__extra {format_type}">{name}</a>'
|
||||
return "".join([a_tag.format_map(link) for link in links])
|
||||
|
||||
|
||||
def _fix_py(cells):
|
||||
def _fix_py(cells: list[str]) -> list[str]:
|
||||
# backwards-compat
|
||||
new_cells = []
|
||||
for html in cells:
|
||||
|
|
Loading…
Reference in New Issue