From 927096a933427ca7143a6cce6797c6cbc359a7ba Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Mon, 21 Dec 2020 00:05:43 -0500 Subject: [PATCH 1/7] Add mypy to CI pipeline and begin typing effort --- .pre-commit-config.yaml | 5 +++++ setup.cfg | 17 +++++++++++++++ src/pytest_html/__init__.py | 2 +- src/pytest_html/extras.py | 40 ++++++++++++++++++++++++---------- src/pytest_html/hooks.py | 1 + src/pytest_html/html_report.py | 1 + src/pytest_html/outcome.py | 15 ++++++++++--- src/pytest_html/plugin.py | 1 + src/pytest_html/result.py | 1 + src/pytest_html/util.py | 6 +++-- testing/test_pytest_html.py | 1 + 11 files changed, 73 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5757483..2300f57f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,6 +44,11 @@ repos: additional_dependencies: - eslint@7.13.0 args: [src] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.790 + hooks: + - id: mypy + files: ^(src/pytest_html|testing) - repo: local hooks: - id: rst diff --git a/setup.cfg b/setup.cfg index 526aeb28..9629125b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,19 @@ [bdist_wheel] universal = 0 + +[mypy] +check_untyped_defs = True +disallow_any_generics = True +disallow_incomplete_defs = True +disallow_untyped_calls = True +disallow_untyped_decorators = True +disallow_untyped_defs = True +ignore_missing_imports = True +no_implicit_optional = True +no_implicit_reexport = True +show_error_codes = True +strict_equality = True +warn_redundant_casts = True +warn_return_any = True +warn_unreachable = True +warn_unused_configs = True diff --git a/src/pytest_html/__init__.py b/src/pytest_html/__init__.py index 4dcb81e2..e6f80ef5 100644 --- a/src/pytest_html/__init__.py +++ b/src/pytest_html/__init__.py @@ -1,5 +1,5 @@ try: - from . import __version + from . import __version # type: ignore __version__ = __version.version except ImportError: diff --git a/src/pytest_html/extras.py b/src/pytest_html/extras.py index f64a0eb5..abf7e4c8 100644 --- a/src/pytest_html/extras.py +++ b/src/pytest_html/extras.py @@ -1,6 +1,8 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +from typing import Dict +from typing import Optional FORMAT_HTML = "html" FORMAT_IMAGE = "image" @@ -10,7 +12,13 @@ FORMAT_VIDEO = "video" -def extra(content, format_type, name=None, mime_type=None, extension=None): +def extra( + content: str, + format_type: str, + name: Optional[str] = None, + mime_type: Optional[str] = None, + extension: Optional[str] = None, +) -> Dict[str, Optional[str]]: return { "name": name, "format_type": format_type, @@ -20,41 +28,51 @@ def extra(content, format_type, name=None, mime_type=None, extension=None): } -def html(content): +def html(content: str) -> Dict[str, Optional[str]]: return extra(content, FORMAT_HTML) -def image(content, name="Image", mime_type="image/png", extension="png"): +def image( + content: str, + name: str = "Image", + mime_type: str = "image/png", + extension: str = "png", +) -> Dict[str, Optional[str]]: return extra(content, FORMAT_IMAGE, name, mime_type, extension) -def png(content, name="Image"): +def png(content: str, name: str = "Image") -> Dict[str, Optional[str]]: return image(content, name, mime_type="image/png", extension="png") -def jpg(content, name="Image"): +def jpg(content: str, name: str = "Image") -> Dict[str, Optional[str]]: return image(content, name, mime_type="image/jpeg", extension="jpg") -def svg(content, name="Image"): +def svg(content: str, name: str = "Image") -> Dict[str, Optional[str]]: return image(content, name, mime_type="image/svg+xml", extension="svg") -def json(content, name="JSON"): +def json(content: str, name: str = "JSON") -> Dict[str, Optional[str]]: return extra(content, FORMAT_JSON, name, "application/json", "json") -def text(content, name="Text"): +def text(content: str, name: str = "Text") -> Dict[str, Optional[str]]: return extra(content, FORMAT_TEXT, name, "text/plain", "txt") -def url(content, name="URL"): +def url(content: str, name: str = "URL") -> Dict[str, Optional[str]]: return extra(content, FORMAT_URL, name) -def video(content, name="Video", mime_type="video/mp4", extension="mp4"): +def video( + content: str, + name: str = "Video", + mime_type: str = "video/mp4", + extension: str = "mp4", +) -> Dict[str, Optional[str]]: return extra(content, FORMAT_VIDEO, name, mime_type, extension) -def mp4(content, name="Video"): +def mp4(content: str, name: str = "Video") -> Dict[str, Optional[str]]: return video(content, name) diff --git a/src/pytest_html/hooks.py b/src/pytest_html/hooks.py index 7b75120b..da1daf4f 100644 --- a/src/pytest_html/hooks.py +++ b/src/pytest_html/hooks.py @@ -1,6 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +# type: ignore def pytest_html_report_title(report): diff --git a/src/pytest_html/html_report.py b/src/pytest_html/html_report.py index e1aa3973..ee5106ca 100644 --- a/src/pytest_html/html_report.py +++ b/src/pytest_html/html_report.py @@ -1,3 +1,4 @@ +# type: ignore import bisect import datetime import json diff --git a/src/pytest_html/outcome.py b/src/pytest_html/outcome.py index 1bb71acd..8056bb72 100644 --- a/src/pytest_html/outcome.py +++ b/src/pytest_html/outcome.py @@ -1,8 +1,17 @@ +from typing import Optional + from py.xml import html class Outcome: - def __init__(self, outcome, total=0, label=None, test_result=None, class_html=None): + def __init__( + self, + outcome: str, + total: int = 0, + label: Optional[str] = None, + test_result: Optional[str] = None, + class_html: Optional[str] = None, + ) -> None: self.outcome = outcome self.label = label or outcome self.class_html = class_html or outcome @@ -12,7 +21,7 @@ def __init__(self, outcome, total=0, label=None, test_result=None, class_html=No self.generate_checkbox() self.generate_summary_item() - def generate_checkbox(self): + def generate_checkbox(self) -> None: checkbox_kwargs = {"data-test-result": self.test_result.lower()} if self.total == 0: checkbox_kwargs["disabled"] = "true" @@ -27,7 +36,7 @@ def generate_checkbox(self): **checkbox_kwargs, ) - def generate_summary_item(self): + def generate_summary_item(self) -> None: self.summary_item = html.span( f"{self.total} {self.label}", class_=self.class_html ) diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py index 6a0ef7a4..4d68bb14 100644 --- a/src/pytest_html/plugin.py +++ b/src/pytest_html/plugin.py @@ -1,6 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +# type: ignore import os import pytest diff --git a/src/pytest_html/result.py b/src/pytest_html/result.py index f791e6d7..9748ac61 100644 --- a/src/pytest_html/result.py +++ b/src/pytest_html/result.py @@ -1,3 +1,4 @@ +# type: ignore import json import os import re diff --git a/src/pytest_html/util.py b/src/pytest_html/util.py index 37259ec7..ae65c4a6 100644 --- a/src/pytest_html/util.py +++ b/src/pytest_html/util.py @@ -1,12 +1,14 @@ import importlib from functools import lru_cache +from types import ModuleType +from typing import Optional @lru_cache() -def ansi_support(): +def ansi_support() -> Optional[ModuleType]: try: # from ansi2html import Ansi2HTMLConverter, style # NOQA return importlib.import_module("ansi2html") except ImportError: # ansi2html is not installed - pass + return None diff --git a/testing/test_pytest_html.py b/testing/test_pytest_html.py index b2d23af9..b163891a 100644 --- a/testing/test_pytest_html.py +++ b/testing/test_pytest_html.py @@ -1,6 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +# type: ignore import builtins import json import os From 64900e7a82d298c1ddd0c63e02164fc72cea6152 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 15:08:03 +0100 Subject: [PATCH 2/7] move from setup.cfg to pyproject.toml --- .pre-commit-config.yaml | 2 +- pyproject.toml | 17 +++++++++++++++++ setup.cfg | 19 ------------------- 3 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b982eb6d..d7e14ef6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: - eslint-config-google@0.14.0 args: ["--fix"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v1.13.0 hooks: - id: mypy files: ^(src/pytest_html|testing) diff --git a/pyproject.toml b/pyproject.toml index 27fc0261..a6418ffa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,3 +98,20 @@ version-file = "src/pytest_html/__version.py" [tool.hatch.build.hooks.custom] path = "scripts/npm.py" + +[tool.mypy] +check_untyped_defs = false +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +ignore_missing_imports = true +no_implicit_optional = true +no_implicit_reexport = true +show_error_codes = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 9629125b..00000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[bdist_wheel] -universal = 0 - -[mypy] -check_untyped_defs = True -disallow_any_generics = True -disallow_incomplete_defs = True -disallow_untyped_calls = True -disallow_untyped_decorators = True -disallow_untyped_defs = True -ignore_missing_imports = True -no_implicit_optional = True -no_implicit_reexport = True -show_error_codes = True -strict_equality = True -warn_redundant_casts = True -warn_return_any = True -warn_unreachable = True -warn_unused_configs = True From 6f567e7eb144ada62f8b07615fde01a36d26afe2 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 15:29:34 +0100 Subject: [PATCH 3/7] Add noqa in testing/test_integration.py --- testing/test_integration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_integration.py b/testing/test_integration.py index 891c8877..2939830a 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,3 +1,4 @@ +# type: ignore import base64 import importlib.resources import json From e04622fade65318f12b7a07907cd94242429f8a0 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 15:33:47 +0100 Subject: [PATCH 4/7] Make some hard options false to begin with something --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a6418ffa..921feabb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,12 +100,12 @@ version-file = "src/pytest_html/__version.py" path = "scripts/npm.py" [tool.mypy] -check_untyped_defs = false +check_untyped_defs = false # TODO disallow_any_generics = true disallow_incomplete_defs = true disallow_untyped_calls = true disallow_untyped_decorators = true -disallow_untyped_defs = true +disallow_untyped_defs = false # TODO ignore_missing_imports = true no_implicit_optional = true no_implicit_reexport = true From 49e404e1692eb2ffa3636a5c1731430c9351fa2b Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 15:33:59 +0100 Subject: [PATCH 5/7] Revert "Add noqa in testing/test_integration.py" This reverts commit 6f567e7eb144ada62f8b07615fde01a36d26afe2. --- .pre-commit-config.yaml | 2 ++ testing/test_integration.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7e14ef6..d17d93c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,8 @@ repos: hooks: - id: mypy files: ^(src/pytest_html|testing) + additional_dependencies: + - types-setuptools - repo: local hooks: - id: rst diff --git a/testing/test_integration.py b/testing/test_integration.py index 2939830a..891c8877 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -1,4 +1,3 @@ -# type: ignore import base64 import importlib.resources import json From f1426c478d7addf2d1105a19891321636869c993 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 15:39:23 +0100 Subject: [PATCH 6/7] Fix merge conflict badly fixed --- src/pytest_html/html_report.py | 330 --------------------------------- src/pytest_html/outcome.py | 42 ----- src/pytest_html/result.py | 288 ---------------------------- 3 files changed, 660 deletions(-) delete mode 100644 src/pytest_html/html_report.py delete mode 100644 src/pytest_html/outcome.py delete mode 100644 src/pytest_html/result.py diff --git a/src/pytest_html/html_report.py b/src/pytest_html/html_report.py deleted file mode 100644 index ee5106ca..00000000 --- a/src/pytest_html/html_report.py +++ /dev/null @@ -1,330 +0,0 @@ -# type: ignore -import bisect -import datetime -import json -import os -import time -from collections import defaultdict -from collections import OrderedDict - -from py.xml import html -from py.xml import raw - -from . import __pypi_url__ -from . import __version__ -from .outcome import Outcome -from .result import TestResult -from .util import ansi_support - - -class HTMLReport: - def __init__(self, logfile, config): - logfile = os.path.expanduser(os.path.expandvars(logfile)) - self.logfile = os.path.abspath(logfile) - self.test_logs = [] - self.title = os.path.basename(self.logfile) - self.results = [] - self.errors = self.failed = 0 - self.passed = self.skipped = 0 - self.xfailed = self.xpassed = 0 - has_rerun = config.pluginmanager.hasplugin("rerunfailures") - self.rerun = 0 if has_rerun else None - self.self_contained = config.getoption("self_contained_html") - self.config = config - self.reports = defaultdict(list) - - def _appendrow(self, outcome, report): - result = TestResult(outcome, report, self.logfile, self.config) - if result.row_table is not None: - index = bisect.bisect_right(self.results, result) - self.results.insert(index, result) - tbody = html.tbody( - result.row_table, - class_="{} results-table-row".format(result.outcome.lower()), - ) - if result.row_extra is not None: - tbody.append(result.row_extra) - self.test_logs.insert(index, tbody) - - def append_passed(self, report): - if report.when == "call": - if hasattr(report, "wasxfail"): - self.xpassed += 1 - self._appendrow("XPassed", report) - else: - self.passed += 1 - self._appendrow("Passed", report) - - def append_failed(self, report): - if getattr(report, "when", None) == "call": - if hasattr(report, "wasxfail"): - # pytest < 3.0 marked xpasses as failures - self.xpassed += 1 - self._appendrow("XPassed", report) - else: - self.failed += 1 - self._appendrow("Failed", report) - else: - self.errors += 1 - self._appendrow("Error", report) - - def append_rerun(self, report): - self.rerun += 1 - self._appendrow("Rerun", report) - - def append_skipped(self, report): - if hasattr(report, "wasxfail"): - self.xfailed += 1 - self._appendrow("XFailed", report) - else: - self.skipped += 1 - self._appendrow("Skipped", report) - - def _generate_report(self, session): - suite_stop_time = time.time() - suite_time_delta = suite_stop_time - self.suite_start_time - numtests = self.passed + self.failed + self.xpassed + self.xfailed - generated = datetime.datetime.now() - - with open( - os.path.join(os.path.dirname(__file__), "resources", "style.css") - ) as style_css_fp: - self.style_css = style_css_fp.read() - - if ansi_support(): - ansi_css = [ - "\n/******************************", - " * ANSI2HTML STYLES", - " ******************************/\n", - ] - ansi_css.extend([str(r) for r in ansi_support().style.get_styles()]) - self.style_css += "\n".join(ansi_css) - - # Add user-provided CSS - for path in self.config.getoption("css"): - self.style_css += "\n/******************************" - self.style_css += "\n * CUSTOM CSS" - self.style_css += f"\n * {path}" - self.style_css += "\n ******************************/\n\n" - with open(path) as f: - self.style_css += f.read() - - css_href = "assets/style.css" - html_css = html.link(href=css_href, rel="stylesheet", type="text/css") - if self.self_contained: - html_css = html.style(raw(self.style_css)) - - session.config.hook.pytest_html_report_title(report=self) - - head = html.head(html.meta(charset="utf-8"), html.title(self.title), html_css) - - outcomes = [ - Outcome("passed", self.passed), - Outcome("skipped", self.skipped), - Outcome("failed", self.failed), - Outcome("error", self.errors, label="errors"), - Outcome("xfailed", self.xfailed, label="expected failures"), - Outcome("xpassed", self.xpassed, label="unexpected passes"), - ] - - if self.rerun is not None: - outcomes.append(Outcome("rerun", self.rerun)) - - summary = [ - html.p(f"{numtests} tests ran in {suite_time_delta:.2f} seconds. "), - html.p( - "(Un)check the boxes to filter the results.", - class_="filter", - hidden="true", - ), - ] - - for i, outcome in enumerate(outcomes, start=1): - summary.append(outcome.checkbox) - summary.append(outcome.summary_item) - if i < len(outcomes): - summary.append(", ") - - cells = [ - html.th("Result", class_="sortable result initial-sort", col="result"), - html.th("Test", class_="sortable", col="name"), - html.th("Duration", class_="sortable", col="duration"), - html.th("Links", class_="sortable links", col="links"), - ] - session.config.hook.pytest_html_results_table_header(cells=cells) - - results = [ - html.h2("Results"), - html.table( - [ - html.thead( - html.tr(cells), - html.tr( - [ - html.th( - "No results found. Try to check the filters", - colspan=len(cells), - ) - ], - id="not-found-message", - hidden="true", - ), - id="results-table-head", - ), - self.test_logs, - ], - id="results-table", - ), - ] - - with open( - os.path.join(os.path.dirname(__file__), "resources", "main.js") - ) as main_js_fp: - main_js = main_js_fp.read() - - body = html.body( - html.script(raw(main_js)), - html.h1(self.title), - html.p( - "Report generated on {} at {} by ".format( - generated.strftime("%d-%b-%Y"), generated.strftime("%H:%M:%S") - ), - html.a("pytest-html", href=__pypi_url__), - f" v{__version__}", - ), - onLoad="init()", - ) - - body.extend(self._generate_environment(session.config)) - - summary_prefix, summary_postfix = [], [] - session.config.hook.pytest_html_results_summary( - prefix=summary_prefix, summary=summary, postfix=summary_postfix - ) - body.extend([html.h2("Summary")] + summary_prefix + summary + summary_postfix) - - body.extend(results) - - doc = html.html(head, body) - - unicode_doc = "\n{}".format(doc.unicode(indent=2)) - - # Fix encoding issues, e.g. with surrogates - unicode_doc = unicode_doc.encode("utf-8", errors="xmlcharrefreplace") - return unicode_doc.decode("utf-8") - - def _generate_environment(self, config): - if not hasattr(config, "_metadata") or config._metadata is None: - return [] - - metadata = config._metadata - environment = [html.h2("Environment")] - rows = [] - - keys = [k for k in metadata.keys()] - if not isinstance(metadata, OrderedDict): - keys.sort() - - for key in keys: - value = metadata[key] - if isinstance(value, str) and value.startswith("http"): - value = html.a(value, href=value, target="_blank") - elif isinstance(value, (list, tuple, set)): - value = ", ".join(str(i) for i in sorted(map(str, value))) - elif isinstance(value, dict): - sorted_dict = {k: value[k] for k in sorted(value)} - value = json.dumps(sorted_dict) - raw_value_string = raw(str(value)) - rows.append(html.tr(html.td(key), html.td(raw_value_string))) - - environment.append(html.table(rows, id="environment")) - return environment - - def _save_report(self, report_content): - dir_name = os.path.dirname(self.logfile) - assets_dir = os.path.join(dir_name, "assets") - - os.makedirs(dir_name, exist_ok=True) - if not self.self_contained: - os.makedirs(assets_dir, exist_ok=True) - - with open(self.logfile, "w", encoding="utf-8") as f: - f.write(report_content) - if not self.self_contained: - style_path = os.path.join(assets_dir, "style.css") - with open(style_path, "w", encoding="utf-8") as f: - f.write(self.style_css) - - def _post_process_reports(self): - for test_name, test_reports in self.reports.items(): - report_outcome = "passed" - wasxfail = False - failure_when = None - full_text = "" - extras = [] - duration = 0.0 - - # in theory the last one should have all logs so we just go - # through them all to figure out the outcome, xfail, duration, - # extras, and when it swapped from pass - for test_report in test_reports: - if test_report.outcome == "rerun": - # reruns are separate test runs for all intensive purposes - self.append_rerun(test_report) - else: - full_text += test_report.longreprtext - extras.extend(getattr(test_report, "extra", [])) - duration += getattr(test_report, "duration", 0.0) - - if ( - test_report.outcome not in ("passed", "rerun") - and report_outcome == "passed" - ): - report_outcome = test_report.outcome - failure_when = test_report.when - - if hasattr(test_report, "wasxfail"): - wasxfail = True - - # the following test_report. = settings come at the end of us - # looping through all test_reports that make up a single - # case. - - # outcome on the right comes from the outcome of the various - # test_reports that make up this test case - # we are just carrying it over to the final report. - test_report.outcome = report_outcome - test_report.when = "call" - test_report.nodeid = test_name - test_report.longrepr = full_text - test_report.extra = extras - test_report.duration = duration - - if wasxfail: - test_report.wasxfail = True - - if test_report.outcome == "passed": - self.append_passed(test_report) - elif test_report.outcome == "skipped": - self.append_skipped(test_report) - elif test_report.outcome == "failed": - test_report.when = failure_when - self.append_failed(test_report) - - def pytest_runtest_logreport(self, report): - self.reports[report.nodeid].append(report) - - def pytest_collectreport(self, report): - if report.failed: - self.append_failed(report) - - def pytest_sessionstart(self, session): - self.suite_start_time = time.time() - - def pytest_sessionfinish(self, session): - self._post_process_reports() - report_content = self._generate_report(session) - self._save_report(report_content) - - def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep("-", f"generated html file: file://{self.logfile}") diff --git a/src/pytest_html/outcome.py b/src/pytest_html/outcome.py deleted file mode 100644 index 8056bb72..00000000 --- a/src/pytest_html/outcome.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Optional - -from py.xml import html - - -class Outcome: - def __init__( - self, - outcome: str, - total: int = 0, - label: Optional[str] = None, - test_result: Optional[str] = None, - class_html: Optional[str] = None, - ) -> None: - self.outcome = outcome - self.label = label or outcome - self.class_html = class_html or outcome - self.total = total - self.test_result = test_result or outcome - - self.generate_checkbox() - self.generate_summary_item() - - def generate_checkbox(self) -> None: - checkbox_kwargs = {"data-test-result": self.test_result.lower()} - if self.total == 0: - checkbox_kwargs["disabled"] = "true" - - self.checkbox = html.input( - type="checkbox", - checked="true", - onChange="filterTable(this)", - name="filter_checkbox", - class_="filter", - hidden="true", - **checkbox_kwargs, - ) - - def generate_summary_item(self) -> None: - self.summary_item = html.span( - f"{self.total} {self.label}", class_=self.class_html - ) diff --git a/src/pytest_html/result.py b/src/pytest_html/result.py deleted file mode 100644 index 9748ac61..00000000 --- a/src/pytest_html/result.py +++ /dev/null @@ -1,288 +0,0 @@ -# type: ignore -import json -import os -import re -import time -import warnings -from base64 import b64decode -from base64 import b64encode -from html import escape -from os.path import isfile - -from _pytest.logging import _remove_ansi_escape_sequences -from py.xml import html -from py.xml import raw - -from . import extras -from .util import ansi_support - - -class TestResult: - def __init__(self, outcome, report, logfile, config): - self.test_id = report.nodeid.encode("utf-8").decode("unicode_escape") - if getattr(report, "when", "call") != "call": - self.test_id = "::".join([report.nodeid, report.when]) - self.time = getattr(report, "duration", 0.0) - self.formatted_time = self._format_time(report) - self.outcome = outcome - self.additional_html = [] - self.links_html = [] - self.self_contained = config.getoption("self_contained_html") - self.max_asset_filename_length = int(config.getini("max_asset_filename_length")) - self.logfile = logfile - self.config = config - self.row_table = self.row_extra = None - - test_index = hasattr(report, "rerun") and report.rerun + 1 or 0 - - for extra_index, extra in enumerate(getattr(report, "extra", [])): - self.append_extra_html(extra, extra_index, test_index) - - self.append_log_html( - report, - self.additional_html, - config.option.capture, - config.option.showcapture, - ) - - cells = [ - html.td(self.outcome, class_="col-result"), - html.td(self.test_id, class_="col-name"), - html.td(self.formatted_time, class_="col-duration"), - html.td(self.links_html, class_="col-links"), - ] - - self.config.hook.pytest_html_results_table_row(report=report, cells=cells) - - self.config.hook.pytest_html_results_table_html( - report=report, data=self.additional_html - ) - - if len(cells) > 0: - tr_class = None - if self.config.getini("render_collapsed"): - tr_class = "collapsed" - self.row_table = html.tr(cells) - self.row_extra = html.tr( - html.td(self.additional_html, class_="extra", colspan=len(cells)), - class_=tr_class, - ) - - def __lt__(self, other): - order = ( - "Error", - "Failed", - "Rerun", - "XFailed", - "XPassed", - "Skipped", - "Passed", - ) - return order.index(self.outcome) < order.index(other.outcome) - - def create_asset(self, content, extra_index, test_index, file_extension, mode="w"): - asset_file_name = "{}_{}_{}.{}".format( - re.sub(r"[^\w\.]", "_", self.test_id), - str(extra_index), - str(test_index), - file_extension, - )[-self.max_asset_filename_length :] - asset_path = os.path.join( - os.path.dirname(self.logfile), "assets", asset_file_name - ) - - os.makedirs(os.path.dirname(asset_path), exist_ok=True) - - relative_path = f"assets/{asset_file_name}" - - kwargs = {"encoding": "utf-8"} if "b" not in mode else {} - with open(asset_path, mode, **kwargs) as f: - f.write(content) - return relative_path - - def append_extra_html(self, extra, extra_index, test_index): - href = None - if extra.get("format_type") == extras.FORMAT_IMAGE: - self._append_image(extra, extra_index, test_index) - - elif extra.get("format_type") == extras.FORMAT_HTML: - self.additional_html.append(html.div(raw(extra.get("content")))) - - elif extra.get("format_type") == extras.FORMAT_JSON: - content = json.dumps(extra.get("content")) - if self.self_contained: - href = self._data_uri(content, mime_type=extra.get("mime_type")) - else: - href = self.create_asset( - content, extra_index, test_index, extra.get("extension") - ) - - elif extra.get("format_type") == extras.FORMAT_TEXT: - content = extra.get("content") - if isinstance(content, bytes): - content = content.decode("utf-8") - if self.self_contained: - href = self._data_uri(content) - else: - href = self.create_asset( - content, extra_index, test_index, extra.get("extension") - ) - - elif extra.get("format_type") == extras.FORMAT_URL: - href = extra.get("content") - - elif extra.get("format_type") == extras.FORMAT_VIDEO: - self._append_video(extra, extra_index, test_index) - - if href is not None: - self.links_html.append( - html.a( - extra.get("name"), - class_=extra.get("format_type"), - href=href, - target="_blank", - ) - ) - self.links_html.append(" ") - - def _format_time(self, report): - # parse the report duration into its display version and return - # it to the caller - duration = getattr(report, "duration", None) - if duration is None: - return "" - - duration_formatter = getattr(report, "duration_formatter", None) - string_duration = str(duration) - if duration_formatter is None: - if "." in string_duration: - split_duration = string_duration.split(".") - split_duration[1] = split_duration[1][0:2] - - string_duration = ".".join(split_duration) - - return string_duration - else: - # support %f, since time.strftime doesn't support it out of the box - # keep a precision of 2 for legacy reasons - formatted_milliseconds = "00" - if "." in string_duration: - milliseconds = string_duration.split(".")[1] - formatted_milliseconds = milliseconds[0:2] - - duration_formatter = duration_formatter.replace( - "%f", formatted_milliseconds - ) - duration_as_gmtime = time.gmtime(report.duration) - return time.strftime(duration_formatter, duration_as_gmtime) - - def _populate_html_log_div(self, log, report): - if report.longrepr: - # longreprtext is only filled out on failure by pytest - # otherwise will be None. - # Use full_text if longreprtext is None-ish - # we added full_text elsewhere in this file. - text = report.longreprtext or report.full_text - for line in text.splitlines(): - separator = line.startswith("_ " * 10) - if separator: - log.append(line[:80]) - else: - exception = line.startswith("E ") - if exception: - log.append(html.span(raw(escape(line)), class_="error")) - else: - log.append(raw(escape(line))) - log.append(html.br()) - - for section in report.sections: - header, content = map(escape, section) - log.append(f" {header:-^80} ") - log.append(html.br()) - - if ansi_support(): - converter = ansi_support().Ansi2HTMLConverter( - inline=False, escaped=False - ) - content = converter.convert(content, full=False) - else: - content = _remove_ansi_escape_sequences(content) - - log.append(raw(content)) - log.append(html.br()) - - def append_log_html( - self, - report, - additional_html, - pytest_capture_value, - pytest_show_capture_value, - ): - log = html.div(class_="log") - - should_skip_captured_output = pytest_capture_value == "no" - if report.outcome == "failed" and not should_skip_captured_output: - should_skip_captured_output = pytest_show_capture_value == "no" - if not should_skip_captured_output: - self._populate_html_log_div(log, report) - - if len(log) == 0: - log = html.div(class_="empty log") - log.append("No log output captured.") - - additional_html.append(log) - - def _make_media_html_div( - self, extra, extra_index, test_index, base_extra_string, base_extra_class - ): - content = extra.get("content") - try: - is_uri_or_path = content.startswith(("file", "http")) or isfile(content) - except ValueError: - # On Windows, os.path.isfile throws this exception when - # passed a b64 encoded image. - is_uri_or_path = False - if is_uri_or_path: - if self.self_contained: - warnings.warn( - "Self-contained HTML report " - "includes link to external " - f"resource: {content}" - ) - - html_div = html.a( - raw(base_extra_string.format(extra.get("content"))), href=content - ) - elif self.self_contained: - src = f"data:{extra.get('mime_type')};base64,{content}" - html_div = raw(base_extra_string.format(src)) - else: - content = b64decode(content.encode("utf-8")) - href = src = self.create_asset( - content, extra_index, test_index, extra.get("extension"), "wb" - ) - html_div = html.a( - raw(base_extra_string.format(src)), - class_=base_extra_class, - target="_blank", - href=href, - ) - return html_div - - def _append_image(self, extra, extra_index, test_index): - image_base = '' - html_div = self._make_media_html_div( - extra, extra_index, test_index, image_base, "image" - ) - self.additional_html.append(html.div(html_div, class_="image")) - - def _append_video(self, extra, extra_index, test_index): - video_base = '' - html_div = self._make_media_html_div( - extra, extra_index, test_index, video_base, "video" - ) - self.additional_html.append(html.div(html_div, class_="video")) - - def _data_uri(self, content, mime_type="text/plain", charset="utf-8"): - data = b64encode(content.encode(charset)).decode("ascii") - return f"data:{mime_type};charset={charset};base64,{data}" From 9f23c75a410e08594deb619835020ea68d724bb2 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 20 Nov 2024 15:43:24 +0100 Subject: [PATCH 7/7] Remove some type:ignore --- src/pytest_html/hooks.py | 1 - src/pytest_html/plugin.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pytest_html/hooks.py b/src/pytest_html/hooks.py index c82b6f54..38ddb560 100644 --- a/src/pytest_html/hooks.py +++ b/src/pytest_html/hooks.py @@ -1,7 +1,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -# type: ignore def pytest_html_report_title(report): diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py index 1e0fcca7..949a6ffa 100644 --- a/src/pytest_html/plugin.py +++ b/src/pytest_html/plugin.py @@ -1,7 +1,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -# type: ignore import os import warnings from pathlib import Path