Write a Python renderer¶
Goal. Build a renderer in Python that turns one or more
changesets into a new output format (HTML, a CI-check summary, a
custom JSON shape, …), ending with a working plugin you can drive
from binoc diff -o ....
Prerequisites.
- pip install binoc.
- Familiarity with the
changeset JSON schema.
The minimal shape¶
A renderer is a small class with name, optional file_extension,
and render(changesets, config):
from html import escape
class HtmlRenderer:
name = "binoc.html"
file_extension = "html"
def render(self, changesets, config):
title = (
config.get("title", "Changelog")
if isinstance(config, dict)
else "Changelog"
)
parts = [
"<!DOCTYPE html>",
f"<html><head><title>{escape(title)}</title></head><body>",
f"<h1>{escape(title)}</h1>",
]
for changeset in changesets:
parts.append(
f"<h2>{escape(str(changeset.from_snapshot))} → "
f"{escape(str(changeset.to_snapshot))}</h2>"
)
root = changeset.root
if root is None:
parts.append("<p>No changes detected.</p>")
else:
_render_node(root, parts)
parts.append("</body></html>")
return "\n".join(parts)
def _render_node(node, parts):
parts.append(f"<div><code>{escape(node.path)}</code> "
f"<strong>{escape(node.action)}</strong>")
for tag in node.tags:
parts.append(f" <span>{escape(tag)}</span>")
if node.summary:
parts.append(f"<div>{escape(node.summary)}</div>")
for child in node.children:
_render_node(child, parts)
parts.append("</div>")
def register(registry):
registry.register_renderer("binoc.html", HtmlRenderer())
Key points:
nameis the renderer name referenced from the CLI (binoc diff A B --format binoc.html) and from dataset config (output.binoc-html.*for any renderer-specific settings).file_extension(optional) letsbinoc diff -o changelog.htmlinfer the renderer from the extension. Without it, users must passbinoc.html:path.render(changesets, config)receives a list ofChangesetobjects and the renderer's own config section (a plain dict). Return a string — the rendered output. The CLI writes it to stdout or the designated file.
The binoc-html model plugin is the full runnable example; see
model-plugins/binoc-html/.
Use it without packaging¶
import binoc
config = binoc.Config.default()
config.add_renderer(HtmlRenderer())
changeset = binoc.diff("snapshot-a", "snapshot-b", config=config)
print(binoc.render(changesets=[changeset], renderer="binoc.html", config={}))
For distribution, see Publish a plugin.
Renderer config¶
Each renderer gets its own section of the dataset config (per
Renderer config ADR). Access it via the
config argument:
def render(self, changesets, config):
title = config.get("title", "Changelog") if isinstance(config, dict) else "Changelog"
...
Renderers deserialize their own config — no coordination with core required for new knobs.
Apply significance classification¶
If your renderer wants to group changes by clerical vs. substantive, read the significance map from config and look up each node's tags against it. The Markdown renderer is the reference; see its source and Significance classification for the pattern.
Testing¶
Call render() directly with synthetic changesets, or round-trip a
real diff:
import binoc
changeset = binoc.diff("tests/snap-a", "tests/snap-b")
html = HtmlRenderer().render([changeset], {"title": "Test"})
assert "<h1>Test</h1>" in html
Where to go next¶
- Publish a plugin — package the renderer so
users get
binoc diff A B -o CHANGES.htmlfor free. - Save and render changesets — how users will drive your renderer from the CLI.
- Output routing and CLI UX ADR — the decision record for how renderers are selected.