diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py
index f9aba421..89817d1b 100644
--- a/src/pytest_html/basereport.py
+++ b/src/pytest_html/basereport.py
@@ -3,8 +3,6 @@
import os
import re
import warnings
-from collections import defaultdict
-from functools import partial
from pathlib import Path
import pytest
@@ -17,99 +15,12 @@
from pytest_html.table import Header
from pytest_html.table import Html
from pytest_html.table import Row
+from pytest_html.util import _ansi_styles
from pytest_html.util import cleanup_unserializable
-try:
- from ansi2html import Ansi2HTMLConverter, style
-
- converter = Ansi2HTMLConverter(inline=False, escaped=False)
- _handle_ansi = partial(converter.convert, full=False)
- _ansi_styles = style.get_styles()
-except ImportError:
- from _pytest.logging import _remove_ansi_escape_sequences
-
- _handle_ansi = _remove_ansi_escape_sequences
- _ansi_styles = []
-
class BaseReport:
- class ReportData:
- def __init__(self, title, config):
- self._config = config
- self._data = {
- "title": title,
- "collectedItems": 0,
- "runningState": "not_started",
- "environment": {},
- "tests": defaultdict(list),
- "resultsTableHeader": {},
- "additionalSummary": defaultdict(list),
- }
-
- collapsed = config.getini("render_collapsed")
- if collapsed:
- if collapsed.lower() == "true":
- warnings.warn(
- "'render_collapsed = True' is deprecated and support "
- "will be removed in the next major release. "
- "Please use 'render_collapsed = all' instead.",
- DeprecationWarning,
- )
- self.set_data(
- "collapsed", [outcome.lower() for outcome in collapsed.split(",")]
- )
-
- @property
- def title(self):
- return self._data["title"]
-
- @title.setter
- def title(self, title):
- self._data["title"] = title
-
- @property
- def config(self):
- return self._config
-
- @property
- def data(self):
- return self._data
-
- def set_data(self, key, value):
- self._data[key] = value
-
- def add_test(self, test_data, report, row, remove_log=False):
- for sortable, value in row.sortables.items():
- test_data[sortable] = value
-
- # regardless of pass or fail we must add teardown logging to "call"
- if report.when == "teardown" and not remove_log:
- self.update_test_log(report)
-
- # passed "setup" and "teardown" are not added to the html
- if report.when == "call" or (
- report.when in ["setup", "teardown"] and report.outcome != "passed"
- ):
- if not remove_log:
- processed_logs = _process_logs(report)
- test_data["log"] = _handle_ansi(processed_logs)
- self._data["tests"][report.nodeid].append(test_data)
- return True
-
- return False
-
- def update_test_log(self, report):
- log = []
- for test in self._data["tests"][report.nodeid]:
- if test["testId"] == report.nodeid and "log" in test:
- for section in report.sections:
- header, content = section
- if "teardown" in header:
- log.append(f"{' ' + header + ' ':-^80}")
- log.append(content)
- test["log"] += _handle_ansi("\n".join(log))
-
- def __init__(self, report_path, config, default_css="style.css"):
+ def __init__(self, report_path, config, report_data, default_css="style.css"):
self._report_path = Path(os.path.expandvars(report_path)).expanduser()
self._report_path.parent.mkdir(parents=True, exist_ok=True)
self._resources_path = Path(__file__).parent.joinpath("resources")
@@ -122,7 +33,8 @@ def __init__(self, report_path, config, default_css="style.css"):
config.getini("max_asset_filename_length")
)
- self._report = self.ReportData(self._report_path.name, config)
+ self._report = report_data
+ self._report.title = self._report_path.name
@property
def css(self):
@@ -336,25 +248,6 @@ def _is_error(report):
return report.when in ["setup", "teardown"] and report.outcome == "failed"
-def _process_logs(report):
- log = []
- if report.longreprtext:
- log.append(report.longreprtext.replace("<", "<").replace(">", ">") + "\n")
- for section in report.sections:
- header, content = section
- log.append(f"{' ' + header + ' ':-^80}")
- log.append(content)
-
- # weird formatting related to logs
- if "log" in header:
- log.append("")
- if "call" in header:
- log.append("")
- if not log:
- log.append("No log output captured.")
- return "\n".join(log)
-
-
def _process_outcome(report):
if _is_error(report):
return "Error"
diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py
index a8d0778c..fd43055b 100644
--- a/src/pytest_html/plugin.py
+++ b/src/pytest_html/plugin.py
@@ -8,6 +8,7 @@
from pytest_html.basereport import BaseReport as HTMLReport # noqa: F401
from pytest_html.report import Report
+from pytest_html.report_data import ReportData
from pytest_html.selfcontained_report import SelfContainedReport
@@ -80,10 +81,11 @@ def pytest_configure(config):
if not hasattr(config, "workerinput"):
# prevent opening html_path on worker nodes (xdist)
+ report_data = ReportData(config)
if config.getoption("self_contained_html"):
- html = SelfContainedReport(html_path, config)
+ html = SelfContainedReport(html_path, config, report_data)
else:
- html = Report(html_path, config)
+ html = Report(html_path, config, report_data)
config.pluginmanager.register(html)
diff --git a/src/pytest_html/report.py b/src/pytest_html/report.py
index 3ed35bfb..ed1cd9ed 100644
--- a/src/pytest_html/report.py
+++ b/src/pytest_html/report.py
@@ -6,8 +6,8 @@
class Report(BaseReport):
- def __init__(self, report_path, config):
- super().__init__(report_path, config)
+ def __init__(self, report_path, config, report_data):
+ super().__init__(report_path, config, report_data)
self._assets_path = Path(self._report_path.parent, "assets")
self._assets_path.mkdir(parents=True, exist_ok=True)
self._css_path = Path(self._assets_path, "style.css")
diff --git a/src/pytest_html/report_data.py b/src/pytest_html/report_data.py
new file mode 100644
index 00000000..1a966ed5
--- /dev/null
+++ b/src/pytest_html/report_data.py
@@ -0,0 +1,100 @@
+import warnings
+from collections import defaultdict
+
+from pytest_html.util import _handle_ansi
+
+
+class ReportData:
+ def __init__(self, config):
+ self._config = config
+ self._data = {
+ "title": "",
+ "collectedItems": 0,
+ "runningState": "not_started",
+ "environment": {},
+ "tests": defaultdict(list),
+ "resultsTableHeader": {},
+ "additionalSummary": defaultdict(list),
+ }
+
+ collapsed = config.getini("render_collapsed")
+ if collapsed:
+ if collapsed.lower() == "true":
+ warnings.warn(
+ "'render_collapsed = True' is deprecated and support "
+ "will be removed in the next major release. "
+ "Please use 'render_collapsed = all' instead.",
+ DeprecationWarning,
+ )
+ self.set_data(
+ "collapsed", [outcome.lower() for outcome in collapsed.split(",")]
+ )
+
+ @property
+ def title(self):
+ return self._data["title"]
+
+ @title.setter
+ def title(self, title):
+ self._data["title"] = title
+
+ @property
+ def config(self):
+ return self._config
+
+ @property
+ def data(self):
+ return self._data
+
+ def set_data(self, key, value):
+ self._data[key] = value
+
+ def add_test(self, test_data, report, row, remove_log=False):
+ for sortable, value in row.sortables.items():
+ test_data[sortable] = value
+
+ # regardless of pass or fail we must add teardown logging to "call"
+ if report.when == "teardown" and not remove_log:
+ self.update_test_log(report)
+
+ # passed "setup" and "teardown" are not added to the html
+ if report.when == "call" or (
+ report.when in ["setup", "teardown"] and report.outcome != "passed"
+ ):
+ if not remove_log:
+ processed_logs = _process_logs(report)
+ test_data["log"] = _handle_ansi(processed_logs)
+ self._data["tests"][report.nodeid].append(test_data)
+ return True
+
+ return False
+
+ def update_test_log(self, report):
+ log = []
+ for test in self._data["tests"][report.nodeid]:
+ if test["testId"] == report.nodeid and "log" in test:
+ for section in report.sections:
+ header, content = section
+ if "teardown" in header:
+ log.append(f"{' ' + header + ' ':-^80}")
+ log.append(content)
+ test["log"] += _handle_ansi("\n".join(log))
+
+
+def _process_logs(report):
+ log = []
+ if report.longreprtext:
+ log.append(report.longreprtext.replace("<", "<").replace(">", ">") + "\n")
+ for section in report.sections:
+ header, content = section
+ log.append(f"{' ' + header + ' ':-^80}")
+ log.append(content)
+
+ # weird formatting related to logs
+ if "log" in header:
+ log.append("")
+ if "call" in header:
+ log.append("")
+ if not log:
+ log.append("No log output captured.")
+ return "\n".join(log)
diff --git a/src/pytest_html/selfcontained_report.py b/src/pytest_html/selfcontained_report.py
index 55fb8556..c5fd1e6b 100644
--- a/src/pytest_html/selfcontained_report.py
+++ b/src/pytest_html/selfcontained_report.py
@@ -6,8 +6,8 @@
class SelfContainedReport(BaseReport):
- def __init__(self, report_path, config):
- super().__init__(report_path, config)
+ def __init__(self, report_path, config, report_data):
+ super().__init__(report_path, config, report_data)
@property
def css(self):
diff --git a/src/pytest_html/util.py b/src/pytest_html/util.py
index 3cfce7a2..3d1dea0e 100644
--- a/src/pytest_html/util.py
+++ b/src/pytest_html/util.py
@@ -1,18 +1,20 @@
-import importlib
import json
-from functools import lru_cache
+from functools import partial
from typing import Any
from typing import Dict
-@lru_cache()
-def ansi_support():
- try:
- # from ansi2html import Ansi2HTMLConverter, style # NOQA
- return importlib.import_module("ansi2html")
- except ImportError:
- # ansi2html is not installed
- pass
+try:
+ from ansi2html import Ansi2HTMLConverter, style
+
+ converter = Ansi2HTMLConverter(inline=False, escaped=False)
+ _handle_ansi = partial(converter.convert, full=False)
+ _ansi_styles = style.get_styles()
+except ImportError:
+ from _pytest.logging import _remove_ansi_escape_sequences
+
+ _handle_ansi = _remove_ansi_escape_sequences
+ _ansi_styles = []
def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]: