Uh oh!
There was an error while loading. Please reload this page.
feat: pluggable HTML themes via Jinja2 template inheritance - #1030
feat: pluggable HTML themes via Jinja2 template inheritance#1030RonnyPfannschmidt wants to merge 7 commits into
Conversation
Add a theme system using entry_points ("pytest_html.themes") and an
html_theme_path ini option for local overrides. Themes provide a
layout.jinja2 that extends base.jinja2 and optionally a style.css.
Ships two built-in themes: classic (current look, default) and modern
(card-based dashboard). External packages can register additional
themes via the same entry point group.
Closespytest-dev#1029
Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Pull request overview
Introduces a pluggable HTML theme system for pytest-html using Jinja2 template inheritance, enabling built-in and third-party themes via a new pytest_html.themes entry point group and an html_theme_path local override.
Changes:
- Adds theme selection/config (
html_theme,html_theme_path) and entry-point-based theme discovery. - Refactors templates into
base.jinja2plus per-themelayout.jinja2, and splits CSS into theme-specific stylesheets (classic/modern). - Adds tests and user documentation covering theme selection, overrides, and CSS behavior.
Reviewed changes
Copilot reviewed 12 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| testing/test_themes.py | Adds functional coverage for built-in themes, local theme path overrides, and CSS composition. |
| src/pytest_html/util.py | Switches default template name from index.jinja2 to layout.jinja2 for theme-based rendering. |
| src/pytest_html/plugin.py | Implements theme resolution (ini + entry points) and wires theme template/CSS selection into report generation. |
| src/pytest_html/resources/base.jinja2 | Introduces overridable Jinja blocks to support template inheritance across themes. |
| src/pytest_html/resources/classic/layout.jinja2 | Defines classic theme layout as a thin wrapper extending the base template. |
| src/pytest_html/resources/classic/style.css | Provides the classic theme CSS (generated output). |
| src/pytest_html/resources/modern/layout.jinja2 | Adds modern theme-specific summary/header layout using base template blocks. |
| src/pytest_html/resources/modern/style.css | Provides the modern theme CSS (generated output). |
| src/layout/css/classic.scss | Adds source SCSS for generating classic CSS. |
| src/layout/css/modern.scss | Adds source SCSS for generating modern CSS. |
| src/.gitattributes | Marks theme CSS outputs as generated for GitHub linguist. |
| pyproject.toml | Registers built-in themes via the new pytest_html.themes entry point group. |
| package.json | Updates build scripts to generate per-theme CSS files. |
| docs/user_guide.rst | Documents theme selection, local theme directories, entry-point authoring, and available template blocks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| eps = importlib.metadata.entry_points(group="pytest_html.themes") | ||
| for ep in eps: | ||
| if ep.name == theme_name: | ||
| theme_module = ep.load() | ||
| theme_traversable = importlib.resources.files(theme_module) | ||
| return Path(str(theme_traversable)) |
| from pathlib import Path | ||
| if ep.name == theme_name: | ||
| theme_module = ep.load() | ||
| theme_traversable = importlib.resources.files(theme_module) | ||
| return Path(str(theme_traversable)) |
- Drop Python 3.9 support (EOL), requires-python bumped to >=3.10 - Validate layout.jinja2 exists in entry-point-resolved theme dirs - Remove unused Path import in test_themes.py Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
- Remove unused imports (flake8) - Replace HTML entity with Unicode literal (djlint) - Fix test references to moved style.css path - Drop Python 3.9/pypy3.9 from CI matrix - Apply pyupgrade --py310-plus (Optional → X | None) - Update pyupgrade config to --py310-plus Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
| def _resolve_theme(config: pytest.Config, resources_path: Path) -> Path: | ||
| theme_name = config.getini("html_theme") | ||
| from __future__ import annotations | ||
| pytest_plugins = ("pytester",) | ||
| def run(pytester, path="report.html", cmd_flags=None): | ||
| cmd_flags = cmd_flags or [] | ||
| path = pytester.path.joinpath(path) | ||
| return pytester.runpytest("--html", path, *cmd_flags) |
| def test_theme_path_without_layout_raises_error(self, pytester): | ||
| theme_dir = pytester.path / "bad_theme" | ||
| theme_dir.mkdir() | ||
| pytester.makeini(f"[pytest]\nhtml_theme_path = {theme_dir}\n") | ||
| pytester.makepyfile("def test_pass(): pass") | ||
| result = run(pytester) | ||
| result.stderr.fnmatch_lines(["*does not contain layout.jinja2*"]) | ||
| def test_unknown_theme_raises_error(self, pytester): | ||
| pytester.makeini("[pytest]\nhtml_theme = nonexistent") | ||
| pytester.makepyfile("def test_pass(): pass") | ||
| result = run(pytester) |
RonnyPfannschmidt
commented
May 25, 2026
its locally broken, so some test assumption is wrong |
RonnyPfannschmidt
commented
May 25, 2026
i need to adapt graphic style a bit more, righ now its blocky but functional - the modern theme needs to be made pretty first as wrll |
Rewrites the modern theme CSS/HTML to match pytest-xhtml aesthetic: - Result cards with colored top borders and status icons in circles - Toggle switches replacing plain checkboxes for filters - Styled buttons (primary/outline) instead of default HTML buttons - Grid layout placing environment card alongside result cards - CSS variables for consistent theming - Explicit ./ prefix on asset paths for file:// compatibility Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
- Cards show icon + number + label inline, truncating with ellipsis - Below 900px: labels hide, cards compact to icon+number in 3-col grid - Below 600px: grid wraps under environment, labels return - Title tooltip always available for full label text Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
RonnyPfannschmidt
commented
May 27, 2026
@BeyondEvil give this a spin and some scrutinity once you get to it |
Summary
pytest_html.themesentry point group and anhtml_theme_pathini option for local overrideslayout.jinja2(extendingbase.jinja2) and optionally astyle.cssMotivation
Addresses user demand for visual customization (#1029, #1028, #730, #841, #913, #904) without requiring forks. The entry point design means theme packages are pip-installable, while
html_theme_pathsupports local project-level themes with zero packaging overhead.Usage
Theme authoring
A theme is a Python package directory containing:
__init__.py(package marker)layout.jinja2(required, extendsbase.jinja2)style.css(optional, replaces default CSS)Register via entry point:
Test plan
html_theme_pathini option overrides entry pointslayout.jinja2raises UsageError--cssflag still appends on top of theme CSSCloses#1029
Made with Cursor