Skip to content

Bring 6 upstream PR ports onto upstream-fixes#9

Merged
SomethingNew71 merged 17 commits into
upstream-fixesfrom
port-321-stdin-stdout
May 5, 2026
Merged

Bring 6 upstream PR ports onto upstream-fixes#9
SomethingNew71 merged 17 commits into
upstream-fixesfrom
port-321-stdin-stdout

Conversation

@SomethingNew71
Copy link
Copy Markdown

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

Upstream Feature Local PR
#444 `--template-dir` CLI option #3 (merged)
#379 `output_dpi` YAML option #4 (merged)
#234 YAML embedded in PNG (round-trip) #5 (merged)
#492 `` HTML placeholder #6 (merged)
#357 Per-connector / per-cable tweak with placeholder #7 (merged)
#367 PDF output format #8 (merged)

Plus all gemini-code-assist review fixes folded into the relevant commits.

Verification (already run)

  • `build_examples.py` clean across all 14 examples + 8 tutorials + 2 demos
  • Deterministic outputs (`.gv`, `.bom.tsv`) byte-identical to baseline
  • All 8 features tested end-to-end on the merged tree (stdin/stdout, PNG round-trip, PDF, revision placeholder, per-node tweaks, template_dir, output_dpi)

Test plan

  • CI passes
  • Spot-check that `wireviz harness.png` round-trips correctly
  • Spot-check that `wireviz -t /some/dir harness.yml` resolves a custom template

🤖 Generated with Claude Code

SomethingNew71 and others added 17 commits May 4, 2026 19:59
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)
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@SomethingNew71 SomethingNew71 merged commit db56205 into upstream-fixes May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant