From ad6d093391d6fe3644e7f081dc8960cf212a587f Mon Sep 17 00:00:00 2001 From: Jeffrey Wright Date: Mon, 13 Nov 2023 15:43:09 -0700 Subject: [PATCH 1/8] Issue 434: Type annotations for basereport.py. --- src/pytest_html/basereport.py | 88 +++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 34 deletions(-) diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index f3aa6455..8ee07935 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -10,36 +10,56 @@ from collections import defaultdict from html import escape from pathlib import Path +from typing import Any +from typing import DefaultDict +from typing import Dict +from typing import List 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._report.title = self._report_path.name + self._reports: DefaultDict = defaultdict(dict) + self._report: ReportData = report_data + self._report.title: str = self._report_path.name @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), @@ -47,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) @@ -68,7 +88,7 @@ def _generate_report(self, self_contained=False): self._write_report(rendered_report) - def _generate_environment(self): + def _generate_environment(self) -> Dict[str, Any]: try: from pytest_metadata.plugin import metadata_key @@ -89,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): @@ -97,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) -> None: pass - def _media_content(self, *args, **kwargs): + def _media_content(self, *args, **kwargs) -> None: 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): @@ -134,12 +154,12 @@ def _process_extras(self, report, test_id): 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: @@ -153,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]: @@ -163,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) @@ -176,7 +196,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"], @@ -187,23 +207,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!" @@ -247,7 +267,7 @@ def pytest_runtest_logreport(self, report): if self._config.getini("generate_report_on_test"): self._generate_report() - def _process_report(self, report, duration): + def _process_report(self, report: TestReport, duration: int) -> None: outcome = _process_outcome(report) try: # hook returns as list for some reason @@ -291,9 +311,9 @@ def _process_report(self, report, duration): self._report.add_test(data, report, outcome, processed_logs) -def _format_duration(duration): +def _format_duration(duration: float) -> str: if duration < 1: - return "{} ms".format(round(duration * 1000)) + return f"{round(duration * 1000)} ms" hours = math.floor(duration / 3600) remaining_seconds = duration % 3600 @@ -304,13 +324,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") @@ -330,7 +350,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"): @@ -342,12 +362,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: From 544913fb85baa86fcdec0bda85b2990040badfc0 Mon Sep 17 00:00:00 2001 From: Jeffrey Wright Date: Mon, 13 Nov 2023 16:14:12 -0700 Subject: [PATCH 2/8] Issue 434: add changelog entry. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) 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) ~~~~~~~~~~~~~~~~~~ From 0d81ecfd0c8bba6b8aafd2de64148b1a062a141e Mon Sep 17 00:00:00 2001 From: Jeffrey Wright Date: Sun, 10 Dec 2023 09:28:40 -0700 Subject: [PATCH 3/8] Issue 434: reconfig precommit for min python version 3.8.0. --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 749720f7..93686d3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,6 +32,7 @@ repos: additional_dependencies: - flake8-builtins==1.5.3 - flake8-typing-imports==1.12.0 + args: ["--min-python-version=3.8.0"] - repo: https://github.com/asottile/reorder-python-imports rev: v3.12.0 From ce5f5779c27c70e30c86762e79c698b43acd2dc3 Mon Sep 17 00:00:00 2001 From: Jeffrey Wright Date: Sun, 10 Dec 2023 10:01:45 -0700 Subject: [PATCH 4/8] Issue 434: rebase from master 2012-12-10. --- .flake8 | 2 ++ src/pytest_html/basereport.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..5f4afd7d --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +min-python-version = 3.8.0 diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index 8ee07935..7baadb3b 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -267,7 +267,9 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: if self._config.getini("generate_report_on_test"): self._generate_report() - def _process_report(self, report: TestReport, duration: int) -> None: + def _process_report( + self, report: TestReport, duration: int, processed_extras: List + ) -> None: outcome = _process_outcome(report) try: # hook returns as list for some reason From 80cc65936aa4cdd5306df5f094c7953a4b9214e8 Mon Sep 17 00:00:00 2001 From: Jeffrey Wright Date: Sun, 10 Dec 2023 10:19:07 -0700 Subject: [PATCH 5/8] Issue 434: remove .flake8; resolve rebase conflicts. --- .flake8 => .flake8-old | 0 src/pytest_html/basereport.py | 2 ++ 2 files changed, 2 insertions(+) rename .flake8 => .flake8-old (100%) diff --git a/.flake8 b/.flake8-old similarity index 100% rename from .flake8 rename to .flake8-old diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index 7baadb3b..3d8c79f6 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -6,6 +6,7 @@ import math import os import re +import time import warnings from collections import defaultdict from html import escape @@ -51,6 +52,7 @@ def __init__( self._reports: DefaultDict = defaultdict(dict) self._report: ReportData = report_data self._report.title: str = self._report_path.name + self._suite_start_time: float = time.time() @property def css(self): From 109657e1a858ab7c0aaa42c584a31fdda8e58411 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 21:30:32 +0100 Subject: [PATCH 6/8] Apply pyupgrade --- src/pytest_html/basereport.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py index 22c47627..c23cc9bd 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -13,8 +13,6 @@ from pathlib import Path from typing import Any from typing import DefaultDict -from typing import Dict -from typing import List import pytest from _pytest.config import Config @@ -90,7 +88,7 @@ def _generate_report(self, self_contained: bool = False) -> None: self._write_report(rendered_report) - def _generate_environment(self) -> Dict[str, Any]: + def _generate_environment(self) -> dict[str, Any]: try: from pytest_metadata.plugin import metadata_key @@ -125,7 +123,7 @@ def _data_content(self, *args, **kwargs) -> None: def _media_content(self, *args, **kwargs) -> None: pass - def _process_extras(self, report: CollectReport, test_id: str) -> List[Any]: + 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): @@ -161,7 +159,7 @@ def _write_report(self, rendered_report: str) -> None: f.write(rendered_report) def _run_count(self) -> str: - relevant_outcomes: List[str] = ["passed", "failed", "xpassed", "xfailed"] + relevant_outcomes: list[str] = ["passed", "failed", "xpassed", "xfailed"] counts = 0 for outcome in self._report.outcomes.keys(): if outcome in relevant_outcomes: @@ -175,7 +173,7 @@ def _run_count(self) -> str: return f"{counts}/{self._report.collected_items} {'tests' if plural else 'test'} done." - def _hydrate_data(self, data: Dict[str, List], cells: List[str]) -> None: + 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]: @@ -280,7 +278,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self._generate_report() def _process_report( - self, report: TestReport, duration: int, processed_extras: List + self, report: TestReport, duration: int, processed_extras: list ) -> None: outcome = _process_outcome(report) try: @@ -345,7 +343,7 @@ def _is_error(report: BaseReport) -> bool: ) -def _process_logs(report) -> List[str]: +def _process_logs(report) -> list[str]: log = [] if report.longreprtext: log.append(escape(report.longreprtext) + "\n") @@ -382,7 +380,7 @@ def _process_links(links) -> str: return "".join([a_tag.format_map(link) for link in links]) -def _fix_py(cells: List[str]) -> List[str]: +def _fix_py(cells: list[str]) -> list[str]: # backwards-compat new_cells = [] for html in cells: From bb465c1070dc5c0facf3de5a3cffd1c7fbb58d1a Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 21:38:06 +0100 Subject: [PATCH 7/8] Some fixes --- pyproject.toml | 6 +++--- src/pytest_html/basereport.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) 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 c23cc9bd..1a88865e 100644 --- a/src/pytest_html/basereport.py +++ b/src/pytest_html/basereport.py @@ -47,9 +47,9 @@ def __init__( config.getini("max_asset_filename_length") ) - self._reports: DefaultDict = defaultdict(dict) + self._reports: DefaultDict = defaultdict(dict) # type: ignore self._report: ReportData = report_data - self._report.title: str = self._report_path.name + self._report.title = self._report_path.name self._suite_start_time: float = time.time() @property @@ -88,7 +88,7 @@ def _generate_report(self, self_contained: bool = False) -> None: self._write_report(rendered_report) - def _generate_environment(self) -> dict[str, Any]: + def _generate_environment(self) -> Any: try: from pytest_metadata.plugin import metadata_key @@ -117,10 +117,10 @@ def _is_redactable_environment_variable(self, environment_variable: str) -> bool return False - def _data_content(self, *args, **kwargs) -> None: + def _data_content(self, *args, **kwargs): # type: ignore[no-untyped-def] pass - def _media_content(self, *args, **kwargs) -> None: + def _media_content(self, *args, **kwargs): # type: ignore[no-untyped-def] pass def _process_extras(self, report: CollectReport, test_id: str) -> list[Any]: @@ -136,19 +136,19 @@ def _process_extras(self, report: CollectReport, test_id: str) -> list[Any]: ) 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"] ) From 8c795126cf07beea57f6a08c687e163076c36561 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 21:42:29 +0100 Subject: [PATCH 8/8] Proper min version for flake8 --- .flake8 | 2 ++ .flake8-old | 2 -- .pre-commit-config.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 .flake8 delete mode 100644 .flake8-old 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/.flake8-old b/.flake8-old deleted file mode 100644 index 5f4afd7d..00000000 --- a/.flake8-old +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -min-python-version = 3.8.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33a0df52..b8e4efbb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: additional_dependencies: - flake8-builtins==1.5.3 - flake8-typing-imports==1.12.0 - args: ["--min-python-version=3.8.0"] + args: ["--min-python-version=3.9.0"] - repo: https://github.com/asottile/reorder-python-imports rev: v3.14.0