diff --git a/src/pytest_html/basereport.py b/src/pytest_html/basereport.py
index 2f169110..52180428 100644
--- a/src/pytest_html/basereport.py
+++ b/src/pytest_html/basereport.py
@@ -9,28 +9,21 @@
from pathlib import Path
import pytest
-from jinja2 import Environment
-from jinja2 import FileSystemLoader
-from jinja2 import select_autoescape
from pytest_html import __version__
from pytest_html import extras
from pytest_html.table import Header
from pytest_html.table import Row
-from pytest_html.util import _ansi_styles
from pytest_html.util import cleanup_unserializable
class BaseReport:
- def __init__(self, report_path, config, report_data, default_css="style.css"):
+ def __init__(self, report_path, config, report_data, template, 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")
self._config = config
- self._template = _read_template([self._resources_path])
- self._css = _process_css(
- Path(self._resources_path, default_css), self._config.getoption("css")
- )
+ self._template = template
+ self._css = css
self._max_asset_filename_length = int(
config.getini("max_asset_filename_length")
)
@@ -224,32 +217,6 @@ def pytest_runtest_logreport(self, report):
self._generate_report()
-def _process_css(default_css, extra_css):
- with open(default_css, encoding="utf-8") as f:
- css = f.read()
-
- # Add user-provided CSS
- for path in extra_css:
- css += "\n/******************************"
- css += "\n * CUSTOM CSS"
- css += f"\n * {path}"
- css += "\n ******************************/\n\n"
- with open(path, encoding="utf-8") as f:
- css += f.read()
-
- # ANSI support
- if _ansi_styles:
- ansi_css = [
- "\n/******************************",
- " * ANSI2HTML STYLES",
- " ******************************/\n",
- ]
- ansi_css.extend([str(r) for r in _ansi_styles])
- css += "\n".join(ansi_css)
-
- return css
-
-
def _is_error(report):
return report.when in ["setup", "teardown"] and report.outcome == "failed"
@@ -282,13 +249,3 @@ def _process_outcome(report):
return "XFailed"
return report.outcome.capitalize()
-
-
-def _read_template(search_paths, template_name="index.jinja2"):
- env = Environment(
- loader=FileSystemLoader(search_paths),
- autoescape=select_autoescape(
- enabled_extensions=("jinja2",),
- ),
- )
- return env.get_template(template_name)
diff --git a/src/pytest_html/plugin.py b/src/pytest_html/plugin.py
index 6683c54a..3bca2fd6 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/.
+import os
import warnings
from pathlib import Path
@@ -11,6 +12,8 @@
from pytest_html.report import Report
from pytest_html.report_data import ReportData
from pytest_html.selfcontained_report import SelfContainedReport
+from pytest_html.util import _process_css
+from pytest_html.util import _read_template
def pytest_addhooks(pluginmanager):
@@ -68,10 +71,14 @@ def pytest_addoption(parser):
def pytest_configure(config):
html_path = config.getoption("htmlpath")
if html_path:
+ extra_css = [
+ Path(os.path.expandvars(css)).expanduser()
+ for css in config.getoption("css")
+ ]
missing_css_files = []
- for css_path in config.getoption("css"):
- if not Path(css_path).exists():
- missing_css_files.append(css_path)
+ for css_path in extra_css:
+ if not css_path.exists():
+ missing_css_files.append(str(css_path))
if missing_css_files:
os_error = (
@@ -82,11 +89,17 @@ def pytest_configure(config):
if not hasattr(config, "workerinput"):
# prevent opening html_path on worker nodes (xdist)
+ resources_path = Path(__file__).parent.joinpath("resources")
+ default_css = Path(resources_path, "style.css")
+ template = _read_template([resources_path])
+ processed_css = _process_css(default_css, extra_css)
report_data = ReportData(config)
if config.getoption("self_contained_html"):
- html = SelfContainedReport(html_path, config, report_data)
+ html = SelfContainedReport(
+ html_path, config, report_data, template, processed_css
+ )
else:
- html = Report(html_path, config, report_data)
+ html = Report(html_path, config, report_data, template, processed_css)
config.pluginmanager.register(html)
diff --git a/src/pytest_html/report.py b/src/pytest_html/report.py
index 0ef36f6c..22ef7e14 100644
--- a/src/pytest_html/report.py
+++ b/src/pytest_html/report.py
@@ -10,8 +10,8 @@
class Report(BaseReport):
- def __init__(self, report_path, config, report_data):
- super().__init__(report_path, config, report_data)
+ def __init__(self, report_path, config, report_data, template, css):
+ super().__init__(report_path, config, report_data, template, css)
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/selfcontained_report.py b/src/pytest_html/selfcontained_report.py
index dc48d59f..ecc44600 100644
--- a/src/pytest_html/selfcontained_report.py
+++ b/src/pytest_html/selfcontained_report.py
@@ -9,8 +9,8 @@
class SelfContainedReport(BaseReport):
- def __init__(self, report_path, config, report_data):
- super().__init__(report_path, config, report_data)
+ def __init__(self, report_path, config, report_data, template, css):
+ super().__init__(report_path, config, report_data, template, css)
@property
def css(self):
diff --git a/src/pytest_html/util.py b/src/pytest_html/util.py
index a40bdd30..a9b11d53 100644
--- a/src/pytest_html/util.py
+++ b/src/pytest_html/util.py
@@ -6,6 +6,9 @@
from typing import Any
from typing import Dict
+from jinja2 import Environment
+from jinja2 import FileSystemLoader
+from jinja2 import select_autoescape
try:
from ansi2html import Ansi2HTMLConverter, style
@@ -30,3 +33,39 @@ def cleanup_unserializable(d: Dict[str, Any]) -> Dict[str, Any]:
v = str(v)
result[k] = v
return result
+
+
+def _read_template(search_paths, template_name="index.jinja2"):
+ env = Environment(
+ loader=FileSystemLoader(search_paths),
+ autoescape=select_autoescape(
+ enabled_extensions=("jinja2",),
+ ),
+ )
+ return env.get_template(template_name)
+
+
+def _process_css(default_css, extra_css):
+ with open(default_css, encoding="utf-8") as f:
+ css = f.read()
+
+ # Add user-provided CSS
+ for path in extra_css:
+ css += "\n/******************************"
+ css += "\n * CUSTOM CSS"
+ css += f"\n * {path}"
+ css += "\n ******************************/\n\n"
+ with open(path, encoding="utf-8") as f:
+ css += f.read()
+
+ # ANSI support
+ if _ansi_styles:
+ ansi_css = [
+ "\n/******************************",
+ " * ANSI2HTML STYLES",
+ " ******************************/\n",
+ ]
+ ansi_css.extend([str(r) for r in _ansi_styles])
+ css += "\n".join(ansi_css)
+
+ return css
diff --git a/testing/test_unit.py b/testing/test_unit.py
index f60132e4..be8ef387 100644
--- a/testing/test_unit.py
+++ b/testing/test_unit.py
@@ -1,3 +1,11 @@
+import importlib.resources
+import os
+import sys
+
+import pkg_resources
+import pytest
+from assertpy import assert_that
+
pytest_plugins = ("pytester",)
@@ -7,6 +15,22 @@ def run(pytester, path="report.html", cmd_flags=None):
return pytester.runpytest("--html", path, *cmd_flags)
+def file_content():
+ try:
+ return (
+ importlib.resources.files("pytest_html")
+ .joinpath("assets", "style.css")
+ .read_bytes()
+ .decode("utf-8")
+ .strip()
+ )
+ except AttributeError:
+ # Needed for python < 3.9
+ return pkg_resources.resource_string(
+ "pytest_html", os.path.join("assets", "style.css")
+ ).decode("utf-8")
+
+
def test_duration_format_deprecation_warning(pytester):
pytester.makeconftest(
"""
@@ -44,3 +68,67 @@ def pytest_html_results_summary(prefix, summary, postfix, session):
pytester.makepyfile("def test_pass(): pass")
result = run(pytester)
result.assert_outcomes(passed=1)
+
+
+@pytest.fixture
+def css_file_path(pytester):
+ css_one = """
+ h1 {
+ color: red;
+ }
+ """
+ css_two = """
+ h2 {
+ color: blue;
+ }
+ """
+ css_dir = pytester.path / "extra_css"
+ css_dir.mkdir()
+ file_path = css_dir / "one.css"
+ with open(file_path, "w") as f:
+ f.write(css_one)
+
+ pytester.makefile(".css", two=css_two)
+ pytester.makepyfile("def test_pass(): pass")
+
+ return file_path
+
+
+@pytest.fixture(params=[True, False])
+def expandvar(request, css_file_path, monkeypatch):
+ if request.param:
+ monkeypatch.setenv("EXTRA_CSS", str(css_file_path))
+ return "%EXTRA_CSS%" if sys.platform == "win32" else "${EXTRA_CSS}"
+ return css_file_path
+
+
+def test_custom_css(pytester, css_file_path, expandvar):
+ result = run(
+ pytester, "report.html", cmd_flags=["--css", expandvar, "--css", "two.css"]
+ )
+ result.assert_outcomes(passed=1)
+
+ path = pytester.path.joinpath("assets", "style.css")
+
+ with open(str(path)) as f:
+ css = f.read()
+ assert_that(css).contains("* " + str(css_file_path)).contains("* two.css")
+
+
+def test_custom_css_selfcontained(pytester, css_file_path, expandvar):
+ result = run(
+ pytester,
+ "report.html",
+ cmd_flags=[
+ "--css",
+ expandvar,
+ "--css",
+ "two.css",
+ "--self-contained-html",
+ ],
+ )
+ result.assert_outcomes(passed=1)
+
+ with open(pytester.path / "report.html") as f:
+ html = f.read()
+ assert_that(html).contains("* " + str(css_file_path)).contains("* two.css")