diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..91ad4348 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +min-python-version = 3.9.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 582e6b1c..b8e4efbb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/docs/changelog.rst b/docs/changelog.rst index 85508fa8..3c6420ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,11 @@ Versions follow `Semantic Versioning`_ (``..``). Version History --------------- +Unreleased (2023-11-13) +~~~~~~~~~~~~~~~~~~~~~~~ +* Type annotations for basereport.py (issue 434) + + 4.1.1 (2023-11-07) ~~~~~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 2d494788..c110feaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index cfcc74b2..1a88865e 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -11,37 +11,55 @@ 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 @@ def _asset_filename(self, test_id, extra_index, test_index, file_extension): 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 @@ 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 @@ -91,7 +109,7 @@ 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): @@ -99,13 +117,13 @@ def _is_redactable_environment_variable(self, environment_variable): 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 @@ 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: @@ -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]: @@ -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) @@ -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"], @@ -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!" @@ -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 @@ -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" @@ -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 = '{name}' 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: