Bring 6 upstream PR ports onto upstream-fixes#9
Merged
Conversation
Lets users point WireViz at an explicit directory of HTML templates
when resolving a metadata.template.name reference. Useful for shared
branded chrome that lives outside both the YAML source tree and the
output tree.
CLI:
wireviz -t ./brand-templates harness.yml
Programmatic:
wireviz.parse(yaml_str, output_formats=("html",),
template_dir="./brand-templates", ...)
The new explicit path is searched first, before the implicit ones
already in place. Final lookup order:
1. --template-dir / parse template_dir (explicit)
2. YAML source directory (source_path.parent) (PR wireviz#473)
3. output directory (existing)
4. WireViz built-in templates (fallback)
Adapted from wireviz#444 (originally by
@tbornon-sts) — the upstream patch used inconsistent naming
(``templatedir`` on the kwarg, ``template_dir`` on the CLI option) and
included a leftover ``print("Test")`` debug statement; this port uses
``template_dir`` consistently and drops the debug line.
Verified against build_examples.py (deterministic outputs unchanged)
and against a manual case with a custom branded.html living only in
the -t directory: template resolves correctly, and absence of -t
produces a clean "was not found" error from smart_file_resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes the Graphviz "dpi" graph attribute as a top-level WireViz YAML
option. Useful for boosting the resolution of PNG output beyond the
graphviz default of 96 DPI:
options:
output_dpi: 192 # 2x default — 4x pixel area in the PNG
Defaults to 96.0, matching graphviz's own default for non-PostScript
renderers, so existing harnesses render at the same pixel dimensions
they always have.
Files:
* DataClasses.py — Options.output_dpi: Optional[float] = 96.0
* Harness.py — pass dpi=str(self.options.output_dpi) into the graph attr
* docs/syntax.md — documents the new option under "options"
* examples/*.gv, tutorial/*.gv — rebaselined: every .gv now carries
``dpi=96.0``. The .png / .svg / .html outputs are environment-
dependent (graphviz version) and intentionally left untouched, per
CONTRIBUTING.md's "owner will rebuild" policy. ex08.gv is also
intentionally left as baseline because it still contains absolute
image paths from the original maintainer's machine.
Verified:
* Default DPI: demo PNG renders at 428x195 (matches pre-PR baseline).
* output_dpi=192: same harness renders at 857x391 — exactly 2x linear
scale, 4x pixel area, as expected.
* build_examples.py runs cleanly across all examples.
Ported from wireviz#379 (originally
targeting upstream `dev` by Tobias Falk / @tobiasfalk).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses gemini-code-assist feedback on #3 — template_dir was missing from the docstrings of Harness.output(), Harness._render(), and wireviz.parse(). Expanded scope to also document the parameters that earlier PRs in this chain added without docstring coverage: * Harness.output(): Args section now describes filename (incl. None → stdout semantics), fmt (incl. str→tuple normalization), output_dir, output_name, and template_dir; view/cleanup are noted as kept for API compat. * Harness._render(): Args + Returns sections describing fmt, output_dir, output_name, template_dir, and the bytes-vs-str per-format return contract. * wireviz.parse(): source_path (added during PR #1's loopback fix and threaded through PR #2's stdin/stdout port) and template_dir (this PR) added to the Args section, with template-search-priority semantics spelled out. No behavior change. Verified against build_examples.py: deterministic outputs unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses gemini-code-assist feedback on #4 — the prior ``dpi=str(self.options.output_dpi)`` would emit the literal string ``"None"`` if a user set ``output_dpi: null`` in YAML, which Graphviz treats as an invalid value. The reviewer's exact suggestion (``dpi=self.options.output_dpi`` — relying on graphviz auto-coercion of numerics) doesn't quite work either: the graphviz Python lib filters None but does NOT auto-convert ints/floats to strings (``dpi=192`` raises ``TypeError: expected string or bytes-like object, got 'int'``). So compromise: build the graph attr dict, conditionally include the dpi key only when output_dpi is not None, and stringify it ourselves. Verified: * ``output_dpi: 96.0`` (default) — emits ``dpi=96.0`` as before; all example .gv baselines remain byte-identical. * ``output_dpi: 192`` — emits ``dpi=192``; PNG renders at 2x scale (857x391 vs 428x195 default). * ``output_dpi: null`` — no dpi attr emitted; PNG renders at Graphviz's renderer default (96 for PNG → matches default-scale). Also updates the DataClasses.Options.output_dpi comment to document the null-as-defer-to-graphviz semantic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add --template-dir CLI option (port of upstream PR wireviz#444)
Add output_dpi option (port of upstream PR wireviz#379)
…eam PR wireviz#234) Renders now embed the source YAML in PNG output as a zlib-compressed iTXt chunk under the key ``wireviz:yaml``. The CLI auto-detects ``.png`` inputs and pulls the YAML back out, so a single PNG file is enough to re-render or edit a harness — no sidecar .yml needed. The headline workflow: wireviz harness.yml # produces harness.png with yaml inside wireviz harness.png # round-trips: extract YAML, re-render This is the load-bearing capability for the upcoming wireviz-gui: drag a PNG into the editor and recover the source. Without it, every PNG in the wild is an opaque artifact divorced from its model. API surface: * wireviz.parse() gains ``embed_yaml: bool = True``. The default embeds; pass False to render plain PNGs without source-bearing metadata. * Harness.output() / _render() gain ``yaml_source: Optional[str]``. When non-None and PNG is in the requested formats, the rendered PNG bytes are post-processed through PIL to attach the iTXt chunk. * New module-level helpers in Harness.py: - PNG_YAML_CHUNK_KEY = "wireviz:yaml" - _embed_yaml_in_png(png_bytes, yaml_source) -> bytes - read_yaml_from_png(png_path) -> Optional[str] * CLI ``--no-embed-yaml`` flag opts out of embedding when desired (e.g. before sharing a diagram externally without source). Implementation notes: * The chunk uses ``iTXt`` (international text, zip-compressed) rather than ``zTXt`` so unicode YAML round-trips cleanly. Key prefix ``wireviz:`` namespaces the chunk against PNG software-defined keywords. * When parse() is called with a Dict input, we yaml.safe_dump it back for embedding — round-trip-readable, but without the original comments or formatting (those don't survive the dict-conversion step regardless of embedding). * build_examples.py opts out (``embed_yaml=False``) so the regression baseline PNGs stay deterministic. Adapted from wireviz#234 (originally by @jacobian91, targeting upstream ``dev``). The 2021-era PR was heavily bit-rotted — argparse, the old parse_cmdline / parse_file layer, conceal-input enum — only the load-bearing idea (zTXT/iTXt embed in PNG, .png input recovery) was preserved. Reworked against current master's click CLI, the in-memory render dict from PR wireviz#321 stdin/stdout, and threaded source_path / template_dir from earlier PRs in this chain. Verified: * round-trip: harness.yml → harness.png → re-extract → identical YAML * --no-embed-yaml produces a PNG without the chunk (verified via PIL) * ``wireviz harness.png`` on a chunk-less PNG raises a clean click.UsageError * build_examples.py runs cleanly; .gv and .bom.tsv byte-identical to baseline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wireviz#492) Resolves to the key of the most recently added entry in ``metadata.revisions``. Useful in branded HTML chrome to surface a "current revision" badge without expanding to the full ``<!-- %revisions_N_key% -->`` indexed form. Example template fragment: <span class="rev">Rev <!-- %revision% --></span> Adapted from wireviz#492 (originally by @ishaid, targeting upstream ``dev``). The upstream patch was against ``wv_output.py`` (a ``dev``-only renaming of ``wv_html.py``); this port lives in master's ``wv_html.py``. Helper renamed from ``_get_latest_revision`` to ``_latest_revision`` and tightened to return ``""`` for missing/None/empty revisions instead of raising. Documents the new placeholder in templates/README.md. Verified: * Direct unit test: ``_latest_revision({"revisions": {"A": ..., "B": ..., "C": ...}})`` returns ``"C"``. * Empty/missing/None ``revisions`` returns ``""``. * build_examples.py: deterministic outputs (.gv, .bom.tsv) byte- identical to baseline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iew feedback) Addresses gemini-code-assist feedback on #6 — the prior ``str(list(revisions)[-1])`` form returned only the last *character* when ``revisions:`` was a string scalar (e.g. ``revisions: v1.0`` → ``"0"``), and would raise ``TypeError`` on a non-iterable scalar like an integer. Now branches on type: * dict / list → last key/element (preserves prior behavior) * str / int / float / any other non-None scalar → str(value) * None / empty container / missing → "" Verified against all six shapes: {'revisions': {'A': ..., 'B': ..., 'C': ...}} -> 'C' {'revisions': ['A', 'B', 'C']} -> 'C' {'revisions': 'v1.2'} -> 'v1.2' {'revisions': 42} -> '42' {'revisions': None} -> '' {'revisions': {}} -> '' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stream PR wireviz#357) Connectors and cables can now carry their own ``tweak:`` block with the same ``override`` / ``append`` shape as the global one. The harness folds per-node tweaks into the global tweak at instantiation, with an optional placeholder substring rewritten to the node's actual designator — making it practical to author a single tweak template and apply it to many components. Example: tweak: placeholder: "@@" connectors: X1: pinlabels: [A, B] tweak: append: - "@@_extra [color=red, style=dashed];" renders X1's per-node tweak as ``X1_extra [color=red, style=dashed];`` in the .gv source. The same connector definition reused for X2 would produce ``X2_extra ...``. Placeholder semantics: * Per-node ``tweak.placeholder`` overrides the global ``tweak.placeholder``. * An empty string at the per-node level explicitly opts out of substitution for that node. * ``None`` (the default) falls back to the global placeholder. * When neither is set, no substitution happens — bare strings are appended/overridden as-written. Implementation: * DataClasses.py — ``Tweak`` gains ``placeholder: Optional[str] = None``. ``Connector`` and ``Cable`` gain ``tweak: Optional[Tweak] = None``, with ``__post_init__`` coercing a dict literal into a Tweak instance. * Harness.py — new ``Harness._extend_tweak(node)`` method, called from ``add_connector()`` and ``add_cable()``, performs the placeholder substitution and merges into ``self.tweak``. Raises ``ValueError`` if two nodes contribute conflicting overrides for the same key. * docs/syntax.md — documents per-connector / per-cable tweak fields and the new placeholder semantics. Adapted from wireviz#357 (originally by @kvid, targeting upstream ``dev``). Renamed ``extend_tweak`` to ``_extend_tweak`` to mark it private; otherwise faithful to the original logic. ``make_list`` is already in master's wv_bom so no helper backport needed. Verified: * Smoke test with placeholder ``@@`` and per-connector + per-cable ``append:`` blocks produces ``X1_extra``, ``W1_label``, ``cable W1`` in the rendered .gv (the @@'s are substituted). * build_examples.py: deterministic outputs (.gv, .bom.tsv) byte- identical to baseline (no existing example uses the new syntax, so no new substitutions fire). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ack) Addresses gemini-code-assist feedback on #7: * The ``rph`` lambda would raise ``AttributeError`` when called with a None value, which is a legitimate case in YAML when an override deletes a key (``key: null``). Now passes non-strings through unchanged so substitution is a no-op for None / numeric / bool values. * ``s_override[ident] = s_dict or None`` would collapse an empty per-ident override dict to None, which Harness.create_graph() doesn't expect (it iterates ``override.items()`` expecting dict-shaped values). Always store the dict, even when empty — ``self.tweak.override = s_override or None`` already handles the outer "no overrides at all" case. Verified: per-connector override with ``key: null`` now renders without the prior AttributeError. build_examples.py deterministic outputs still byte-identical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the ``pdf`` output format that's been a TODO stub since
v0.4.1. Pipes the graph through Graphviz's PDF renderer
(``graph.pipe(format="pdf")``) and dispatches the bytes the same way
PNG goes — to a file in normal mode, to ``sys.stdout.buffer`` in
stdout mode.
Usage:
wireviz -f P harness.yml # produces harness.pdf
cat harness.yml | wireviz -f P -O - - # stdout, binary
CLI flag changes:
* ``"P": "pdf"`` un-commented in ``format_codes`` (use ``-f P``)
* "PDF output is not yet supported" stderr warning removed from
Harness.output()
Adapted from wireviz#367 (originally
by @tobiasfalk, targeting upstream ``dev``). The upstream patch went
through the old ``graph.render()`` + temp-file path — this port uses
the in-memory ``graph.pipe()`` wired up by the stdin/stdout refactor
(PR wireviz#321), so PDF works in both file mode AND stdout mode without
extra plumbing.
Verified:
* ``wireviz -f P harness.yml`` produces a valid PDF (file 1.7).
* ``cat harness.yml | wireviz -f P -O - -`` writes valid PDF to stdout.
* build_examples.py: deterministic outputs (.gv, .bom.tsv) byte-
identical (no example .yml has been switched to request PDF
rendering — keeping that out of the regression baseline since PDF
bytes from graphviz vary by version).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eedback) Addresses gemini-code-assist feedback on #8 — the prior docstring claimed PDF includes "the diagram and (depending on the template) the BOM", which was carried over from upstream's never-completed PDF stub plan. The actual implementation in this PR pipes the graph through Graphviz's PDF renderer (graph.pipe(format="pdf")), which produces a diagram-only PDF with no embedded BOM table. That matches the PNG/SVG behavior and is the right scope for a fork that already exposes HTML+SVG embed for richer output. Embedding the BOM in PDF would require a full document-composition step (PIL or reportlab) that's well outside the scope of "implement the missing format flag" — and HTML output exists for users who want diagram + BOM in one artifact. No code change; docstring only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Embed YAML source in PNG output (port of upstream PR wireviz#234)
Add <!-- %revision% --> HTML placeholder (port of upstream PR wireviz#492)
Per-connector / per-cable tweak with name placeholder (port of upstream PR wireviz#357)
Wire up PDF output (port of upstream PR wireviz#367)
There was a problem hiding this comment.
Code Review
This pull request introduces several significant features and improvements to WireViz. Key additions include the ability to embed source YAML into rendered PNG files via iTXt chunks for round-trip editing, and the corresponding capability for the CLI to read input directly from these PNGs. It also implements per-connector and per-cable 'tweak' overrides with placeholder substitution, adds a global 'output_dpi' option for Graphviz rendering, and introduces a '%revision%' placeholder for HTML templates. Additionally, the update includes support for custom template directories and enables PDF output in the CLI. I have no feedback to provide as there were no review comments to assess.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the six feature ports merged into `port-321-stdin-stdout` (via PRs #3–#8) onto `upstream-fixes`. PR #2 already merged the stdin/stdout port itself — these are the features that landed on `port-321-stdin-stdout` after PR #2.
What's included
Plus all gemini-code-assist review fixes folded into the relevant commits.
Verification (already run)
Test plan
🤖 Generated with Claude Code