Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isssue434 - typing #772

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
min-python-version = 3.9.0
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
~~~~~~~~~~~~~~~~~~

Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 56 additions & 36 deletions src/pytest_html/basereport.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,63 @@
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),
str(test_index),
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)
Expand All @@ -70,7 +88,7 @@ def _generate_report(self, self_contained=False):

self._write_report(rendered_report)

def _generate_environment(self):
def _generate_environment(self) -> Any:
try:
from pytest_metadata.plugin import metadata_key

Expand All @@ -91,21 +109,21 @@ def _generate_environment(self):

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):
return True

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):
Expand All @@ -118,30 +136,30 @@ def _process_extras(self, report, test_id):
)
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:
Expand All @@ -155,7 +173,7 @@ def _run_count(self):

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]:
Expand All @@ -165,7 +183,7 @@ def _hydrate_data(self, data, cells):
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)
Expand All @@ -179,7 +197,7 @@ def pytest_sessionstart(self, session):
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"],
Expand All @@ -192,23 +210,23 @@ def pytest_sessionfinish(self, session):
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!"
Expand Down Expand Up @@ -259,7 +277,9 @@ def pytest_runtest_logreport(self, report):
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
Expand Down Expand Up @@ -304,7 +324,7 @@ def _process_report(self, report, duration, processed_extras):
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"

Expand All @@ -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")
Expand All @@ -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"):
Expand All @@ -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:
Expand Down
Loading