Skip to content

Commit 8ffb489

Browse files
authored
Chore: Decouple ReportData (#650)
1 parent 8287c84 commit 8ffb489

File tree

6 files changed

+124
-127
lines changed

6 files changed

+124
-127
lines changed

src/pytest_html/basereport.py

Lines changed: 4 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import os
44
import re
55
import warnings
6-
from collections import defaultdict
7-
from functools import partial
86
from pathlib import Path
97

108
import pytest
@@ -17,99 +15,12 @@
1715
from pytest_html.table import Header
1816
from pytest_html.table import Html
1917
from pytest_html.table import Row
18+
from pytest_html.util import _ansi_styles
2019
from pytest_html.util import cleanup_unserializable
2120

22-
try:
23-
from ansi2html import Ansi2HTMLConverter, style
24-
25-
converter = Ansi2HTMLConverter(inline=False, escaped=False)
26-
_handle_ansi = partial(converter.convert, full=False)
27-
_ansi_styles = style.get_styles()
28-
except ImportError:
29-
from _pytest.logging import _remove_ansi_escape_sequences
30-
31-
_handle_ansi = _remove_ansi_escape_sequences
32-
_ansi_styles = []
33-
3421

3522
class BaseReport:
36-
class ReportData:
37-
def __init__(self, title, config):
38-
self._config = config
39-
self._data = {
40-
"title": title,
41-
"collectedItems": 0,
42-
"runningState": "not_started",
43-
"environment": {},
44-
"tests": defaultdict(list),
45-
"resultsTableHeader": {},
46-
"additionalSummary": defaultdict(list),
47-
}
48-
49-
collapsed = config.getini("render_collapsed")
50-
if collapsed:
51-
if collapsed.lower() == "true":
52-
warnings.warn(
53-
"'render_collapsed = True' is deprecated and support "
54-
"will be removed in the next major release. "
55-
"Please use 'render_collapsed = all' instead.",
56-
DeprecationWarning,
57-
)
58-
self.set_data(
59-
"collapsed", [outcome.lower() for outcome in collapsed.split(",")]
60-
)
61-
62-
@property
63-
def title(self):
64-
return self._data["title"]
65-
66-
@title.setter
67-
def title(self, title):
68-
self._data["title"] = title
69-
70-
@property
71-
def config(self):
72-
return self._config
73-
74-
@property
75-
def data(self):
76-
return self._data
77-
78-
def set_data(self, key, value):
79-
self._data[key] = value
80-
81-
def add_test(self, test_data, report, row, remove_log=False):
82-
for sortable, value in row.sortables.items():
83-
test_data[sortable] = value
84-
85-
# regardless of pass or fail we must add teardown logging to "call"
86-
if report.when == "teardown" and not remove_log:
87-
self.update_test_log(report)
88-
89-
# passed "setup" and "teardown" are not added to the html
90-
if report.when == "call" or (
91-
report.when in ["setup", "teardown"] and report.outcome != "passed"
92-
):
93-
if not remove_log:
94-
processed_logs = _process_logs(report)
95-
test_data["log"] = _handle_ansi(processed_logs)
96-
self._data["tests"][report.nodeid].append(test_data)
97-
return True
98-
99-
return False
100-
101-
def update_test_log(self, report):
102-
log = []
103-
for test in self._data["tests"][report.nodeid]:
104-
if test["testId"] == report.nodeid and "log" in test:
105-
for section in report.sections:
106-
header, content = section
107-
if "teardown" in header:
108-
log.append(f"{' ' + header + ' ':-^80}")
109-
log.append(content)
110-
test["log"] += _handle_ansi("\n".join(log))
111-
112-
def __init__(self, report_path, config, default_css="style.css"):
23+
def __init__(self, report_path, config, report_data, default_css="style.css"):
11324
self._report_path = Path(os.path.expandvars(report_path)).expanduser()
11425
self._report_path.parent.mkdir(parents=True, exist_ok=True)
11526
self._resources_path = Path(__file__).parent.joinpath("resources")
@@ -122,7 +33,8 @@ def __init__(self, report_path, config, default_css="style.css"):
12233
config.getini("max_asset_filename_length")
12334
)
12435

125-
self._report = self.ReportData(self._report_path.name, config)
36+
self._report = report_data
37+
self._report.title = self._report_path.name
12638

12739
@property
12840
def css(self):
@@ -336,25 +248,6 @@ def _is_error(report):
336248
return report.when in ["setup", "teardown"] and report.outcome == "failed"
337249

338250

339-
def _process_logs(report):
340-
log = []
341-
if report.longreprtext:
342-
log.append(report.longreprtext.replace("<", "&lt;").replace(">", "&gt;") + "\n")
343-
for section in report.sections:
344-
header, content = section
345-
log.append(f"{' ' + header + ' ':-^80}")
346-
log.append(content)
347-
348-
# weird formatting related to logs
349-
if "log" in header:
350-
log.append("")
351-
if "call" in header:
352-
log.append("")
353-
if not log:
354-
log.append("No log output captured.")
355-
return "\n".join(log)
356-
357-
358251
def _process_outcome(report):
359252
if _is_error(report):
360253
return "Error"

src/pytest_html/plugin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pytest_html.basereport import BaseReport as HTMLReport # noqa: F401
1010
from pytest_html.report import Report
11+
from pytest_html.report_data import ReportData
1112
from pytest_html.selfcontained_report import SelfContainedReport
1213

1314

@@ -80,10 +81,11 @@ def pytest_configure(config):
8081

8182
if not hasattr(config, "workerinput"):
8283
# prevent opening html_path on worker nodes (xdist)
84+
report_data = ReportData(config)
8385
if config.getoption("self_contained_html"):
84-
html = SelfContainedReport(html_path, config)
86+
html = SelfContainedReport(html_path, config, report_data)
8587
else:
86-
html = Report(html_path, config)
88+
html = Report(html_path, config, report_data)
8789

8890
config.pluginmanager.register(html)
8991

src/pytest_html/report.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77

88
class Report(BaseReport):
9-
def __init__(self, report_path, config):
10-
super().__init__(report_path, config)
9+
def __init__(self, report_path, config, report_data):
10+
super().__init__(report_path, config, report_data)
1111
self._assets_path = Path(self._report_path.parent, "assets")
1212
self._assets_path.mkdir(parents=True, exist_ok=True)
1313
self._css_path = Path(self._assets_path, "style.css")

src/pytest_html/report_data.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import warnings
2+
from collections import defaultdict
3+
4+
from pytest_html.util import _handle_ansi
5+
6+
7+
class ReportData:
8+
def __init__(self, config):
9+
self._config = config
10+
self._data = {
11+
"title": "",
12+
"collectedItems": 0,
13+
"runningState": "not_started",
14+
"environment": {},
15+
"tests": defaultdict(list),
16+
"resultsTableHeader": {},
17+
"additionalSummary": defaultdict(list),
18+
}
19+
20+
collapsed = config.getini("render_collapsed")
21+
if collapsed:
22+
if collapsed.lower() == "true":
23+
warnings.warn(
24+
"'render_collapsed = True' is deprecated and support "
25+
"will be removed in the next major release. "
26+
"Please use 'render_collapsed = all' instead.",
27+
DeprecationWarning,
28+
)
29+
self.set_data(
30+
"collapsed", [outcome.lower() for outcome in collapsed.split(",")]
31+
)
32+
33+
@property
34+
def title(self):
35+
return self._data["title"]
36+
37+
@title.setter
38+
def title(self, title):
39+
self._data["title"] = title
40+
41+
@property
42+
def config(self):
43+
return self._config
44+
45+
@property
46+
def data(self):
47+
return self._data
48+
49+
def set_data(self, key, value):
50+
self._data[key] = value
51+
52+
def add_test(self, test_data, report, row, remove_log=False):
53+
for sortable, value in row.sortables.items():
54+
test_data[sortable] = value
55+
56+
# regardless of pass or fail we must add teardown logging to "call"
57+
if report.when == "teardown" and not remove_log:
58+
self.update_test_log(report)
59+
60+
# passed "setup" and "teardown" are not added to the html
61+
if report.when == "call" or (
62+
report.when in ["setup", "teardown"] and report.outcome != "passed"
63+
):
64+
if not remove_log:
65+
processed_logs = _process_logs(report)
66+
test_data["log"] = _handle_ansi(processed_logs)
67+
self._data["tests"][report.nodeid].append(test_data)
68+
return True
69+
70+
return False
71+
72+
def update_test_log(self, report):
73+
log = []
74+
for test in self._data["tests"][report.nodeid]:
75+
if test["testId"] == report.nodeid and "log" in test:
76+
for section in report.sections:
77+
header, content = section
78+
if "teardown" in header:
79+
log.append(f"{' ' + header + ' ':-^80}")
80+
log.append(content)
81+
test["log"] += _handle_ansi("\n".join(log))
82+
83+
84+
def _process_logs(report):
85+
log = []
86+
if report.longreprtext:
87+
log.append(report.longreprtext.replace("<", "&lt;").replace(">", "&gt;") + "\n")
88+
for section in report.sections:
89+
header, content = section
90+
log.append(f"{' ' + header + ' ':-^80}")
91+
log.append(content)
92+
93+
# weird formatting related to logs
94+
if "log" in header:
95+
log.append("")
96+
if "call" in header:
97+
log.append("")
98+
if not log:
99+
log.append("No log output captured.")
100+
return "\n".join(log)

src/pytest_html/selfcontained_report.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77

88
class SelfContainedReport(BaseReport):
9-
def __init__(self, report_path, config):
10-
super().__init__(report_path, config)
9+
def __init__(self, report_path, config, report_data):
10+
super().__init__(report_path, config, report_data)
1111

1212
@property
1313
def css(self):

src/pytest_html/util.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import importlib
21
import json
3-
from functools import lru_cache
2+
from functools import partial
43
from typing import Any
54
from typing import Dict
65

76

8-
@lru_cache()
9-
def ansi_support():
10-
try:
11-
# from ansi2html import Ansi2HTMLConverter, style # NOQA
12-
return importlib.import_module("ansi2html")
13-
except ImportError:
14-
# ansi2html is not installed
15-
pass
7+
try:
8+
from ansi2html import Ansi2HTMLConverter, style
9+
10+
converter = Ansi2HTMLConverter(inline=False, escaped=False)
11+
_handle_ansi = partial(converter.convert, full=False)
12+
_ansi_styles = style.get_styles()
13+
except ImportError:
14+
from _pytest.logging import _remove_ansi_escape_sequences
15+
16+
_handle_ansi = _remove_ansi_escape_sequences
17+
_ansi_styles = []
1618

1719

1820
def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]:

0 commit comments

Comments
 (0)