Skip to content

Add stdin/stdout streaming (port of upstream PR #321)#2

Merged
SomethingNew71 merged 2 commits into
upstream-fixesfrom
port-321-stdin-stdout
May 4, 2026
Merged

Add stdin/stdout streaming (port of upstream PR #321)#2
SomethingNew71 merged 2 commits into
upstream-fixesfrom
port-321-stdin-stdout

Conversation

@SomethingNew71
Copy link
Copy Markdown

Summary

Ports upstream PR #321 (originally targeting upstream dev) to our master-derived upstream-fixes branch, enabling the WireViz CLI to participate in unix pipelines.

This is the highest-value PR for the upcoming wireviz-gui work — the GUI can pipe YAML in and get rendered SVG/PNG out without temp files.

Usage

# YAML in, SVG out
cat harness.yml | wireviz -f s -O - -    > harness.svg

# YAML in, PNG out (binary)
cat harness.yml | wireviz -f p -O - -    > harness.png

# File in, HTML out
wireviz -f h -O - harness.yml             > harness.html
  • Pass - as the input filename to read YAML from stdin.
  • Pass - to either --output-dir or --output-name to write to stdout.
  • Exactly one format may be requested when writing to stdout (multi-format → stdout is rejected with a clear error).

Implementation

Two refactors enable the streaming path while keeping file mode bit-for-bit unchanged:

  1. Harness.output()Harness._render() + dispatch. A new _render(fmt) returns an in-memory {format: bytes|str} dict by piping graphviz directly (graph.pipe(format=...)) and calling embed_svg_images() on the SVG string. The old code rendered to a temporary .tmp.svg file, embedded images via embed_svg_images_file(), then renamed/deleted the temp. That dance is gone — embed_svg_images_file is dropped as unused.

  2. generate_html_output() takes the SVG as a string and returns the HTML string instead of reading a temp file and writing the .html file itself. New output_dir / output_name / png_b64 / source_path parameters preserve <!-- %filename% -->, <!-- %filename_stem% -->, and <!-- %diagram_png_b64% --> placeholders in file mode (and leave them empty/unresolved in stdout mode).

Library-level print() warnings in DataClasses.py, Harness.py, wv_colors.py, wv_helper.py, and wireviz.py are routed to sys.stderr so stdout stays clean of log noise when piped. CLI status banners in wv_cli.py go to stderr for the same reason.

Differences from upstream PR wireviz#321

  • Adapted to current master's file_read_text / file_write_text helpers (the upstream PR was written against the older open_file_read / open_file_write API).
  • Integrated cleanly with #473's source_path template-search additions (already on upstream-fixes) — generate_html_output() now takes a source_path parameter and the search order is source-dir → output-dir → built-in templates.
  • Keeps file-mode <!-- %diagram_png_b64% --> working by reading from the just-rendered PNG; in stdout mode the placeholder is unresolved (rare edge case).

Verification

  • build_examples.py runs cleanly across all 14 examples + 8 tutorials + 2 demos.
  • Deterministic outputs (.gv, .bom.tsv) are byte-identical to baseline → no behavior change in file mode.
  • Tested stdin→stdout for SVG (text, 15KB) and PNG (binary, valid 1086×401 RGBA).
  • Multi-format-to-stdout correctly rejected with a clear error.

Test plan

  • cat examples/demo01.yml | wireviz -f s -O - - > /tmp/out.svg produces a valid SVG
  • cat examples/demo01.yml | wireviz -f p -O - - > /tmp/out.png produces a valid PNG
  • CI workflow passes across Python 3.7–3.12
  • Smoke-test from the GUI prototype side (when available)

🤖 Generated with Claude Code

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 refactors the output generation logic to support stdin/stdout piping and in-memory rendering, while also redirecting warnings and errors to stderr. Key improvements include the introduction of an internal _render method and updated HTML generation that supports inlined base64 images. Review feedback suggests refining the type-hinting and normalization of output formats, correcting a regression in image path resolution to ensure relative paths are resolved against the source file directory, and improving code quality by using specific exception types and standardizing import placements.

Comment thread src/wireviz/Harness.py Outdated
self,
filename: (str, Path),
filename: Optional[Union[str, Path]],
fmt: tuple = ("html", "png", "svg", "tsv"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The fmt parameter is type-hinted as a tuple, but the wireviz.parse function allows it to be a single string. If a string is passed, the subsequent loop for f in fmt: will iterate over its characters instead of the intended format names. It is recommended to allow Union[str, tuple, list] and normalize it to a collection.

Suggested change
fmt: tuple = ("html", "png", "svg", "tsv"),
fmt: Union[str, tuple, list] = ("html", "png", "svg", "tsv"),

Comment thread src/wireviz/Harness.py
Comment on lines +689 to +693
outputs: Dict[str, Union[str, bytes]] = self._render(
fmt,
output_dir=output_dir,
output_name=output_name,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Normalize the fmt parameter to a tuple if a single string is provided to ensure correct iteration in the following steps.

        if isinstance(fmt, str):
            fmt = (fmt,)
        outputs: Dict[str, Union[str, bytes]] = self._render(
            fmt,
            output_dir=output_dir,
            output_name=output_name,
        )

Comment thread src/wireviz/Harness.py
Comment on lines +740 to +744
svg_str: Optional[str] = None
if "svg" in fmt or "html" in fmt:
svg_str = embed_svg_images(
graph.pipe(format="svg").decode("utf-8"), Path.cwd()
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using Path.cwd() as the base path for image resolution is a regression for file-based output. Images should be resolved relative to the source YAML file's directory when available. This ensures that relative image paths in the harness definition work correctly regardless of the current working directory.

        svg_str: Optional[str] = None
        if "svg" in fmt or "html" in fmt:
            base_path = Path(self.source_path).parent if self.source_path and str(self.source_path) != "-" else Path.cwd()
            svg_str = embed_svg_images(
                graph.pipe(format="svg").decode("utf-8"), base_path
            )

Comment thread src/wireviz/Harness.py
# -*- coding: utf-8 -*-

import re
import sys
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The base64 import should be moved to the top of the file to adhere to PEP 8 style guidelines, which recommend placing all imports at the beginning of the module.

Suggested change
import sys
import re
import sys
import base64
from collections import Counter

Comment thread src/wireviz/Harness.py Outdated

def _render(
self,
fmt: tuple,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update the type hint for fmt to include str and list for consistency with the output method and the wireviz.parse API.

Suggested change
fmt: tuple,
fmt: Union[str, tuple, list],

Comment thread src/wireviz/Harness.py Outdated
Comment on lines +735 to +737
import base64

graph = self.graph
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The base64 import should be at the top of the file. This local import can be removed.

Suggested change
import base64
graph = self.graph
graph = self.graph

Comment thread src/wireviz/wireviz.py Outdated
Comment on lines +414 to +416
raise Exception(
"Exactly one output format must be specified when writing to stdout."
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Avoid raising a generic Exception. A more specific exception like ValueError is preferred here as it clearly indicates that the provided arguments are invalid for the requested operation.

Suggested change
raise Exception(
"Exactly one output format must be specified when writing to stdout."
)
if len(output_formats) != 1:
raise ValueError(
"Exactly one output format must be specified when writing to stdout."
)

Comment thread src/wireviz/wv_cli.py Outdated
Comment on lines +112 to +114
raise Exception(
"Exactly one output format (-f) must be specified when writing to stdout."
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In a Click-based CLI, it is better to raise click.UsageError or click.ClickException instead of a generic Exception. This allows Click to handle the error gracefully and display a user-friendly message.

Suggested change
raise Exception(
"Exactly one output format (-f) must be specified when writing to stdout."
)
raise click.UsageError(
"Exactly one output format (-f) must be specified when writing to stdout."
)

SomethingNew71 and others added 2 commits May 4, 2026 19:21
Lets the CLI participate in unix pipelines:

  cat harness.yml | wireviz -f s -O - -    > harness.svg
  cat harness.yml | wireviz -f p -O - -    > harness.png
  wireviz -f h -O - harness.yml             > harness.html

Pass `-` as the input filename to read YAML from stdin, and pass `-` to
either `--output-dir` or `--output-name` to write the rendered output to
stdout. When writing to stdout exactly one format may be requested.

Two refactors enable this:

1. Harness.output() now computes every requested format in memory via
   `Harness._render()` (graph.pipe(format=...) + embed_svg_images on a
   string) instead of the previous `graph.render()` -> `.tmp.svg` ->
   embed_svg_images_file -> rename dance. The caller dispatches the
   resulting `{fmt: bytes|str}` dict to either files or stdout.

2. generate_html_output() now takes the SVG as a string and returns the
   HTML string rather than reading a tmp.svg from disk and writing the
   .html file itself. New optional `output_dir` / `output_name` /
   `png_b64` parameters preserve the `<!-- %filename% -->` and
   `<!-- %diagram_png_b64% -->` template placeholders in file mode and
   leave them empty/unresolved in stdout mode.

Library-level `print()` warnings in DataClasses.py, Harness.py,
wv_colors.py, wv_helper.py, and wireviz.py are routed to `sys.stderr`
so stdout stays clean of log noise when piped. CLI status banners in
wv_cli.py go to stderr for the same reason.

Also drops the now-unused `embed_svg_images_file` helper.

Ported to master from wireviz#321
(originally targeting `dev` by Guillaume Grossetie / @ggrossetie,
resolves wireviz#320). The PR was rebased / adapted to current master's
file_read_text/file_write_text helpers and current Harness.output
signature; the in-memory render dict and stdout dispatch are the
load-bearing additions.

Verification:
- All 14 examples + 8 tutorials + 2 demos rebuild without errors.
- Deterministic outputs (.gv, .bom.tsv) byte-identical to baseline ->
  no behavior change in file mode.
- Verified stdin->stdout for SVG (text) and PNG (binary).
- Multi-format-to-stdout correctly rejected with a clear error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six fixes from gemini-code-assist's review of
#2:

* Harness.py: move ``import base64`` to the module-level imports rather
  than the function-local position inside ``_render()`` (PEP 8).

* Harness.py: ``output()`` and ``_render()`` now accept fmt as
  ``Union[str, Tuple[str, ...], List[str]]`` and normalize a bare
  string to a one-tuple before iterating. Previously a programmatic
  caller passing ``output_formats="svg"`` to ``wireviz.parse()`` would
  cause ``for f in fmt:`` to iterate over the characters ``s``, ``v``,
  ``g`` — a pre-existing latent bug that became reachable through this
  PR's refactor.

* Harness.py: when ``self.source_path`` is set, embed_svg_images now
  resolves relative <image src=...> paths against the YAML source's
  parent directory rather than ``Path.cwd()``. In normal use
  wireviz.parse() rewrites relative image paths to absolute during
  YAML parse, so this only matters for already-rendered Harness
  objects or when a tweak injects a post-parse relative path — but
  it's the conceptually correct base path.

* wireviz.py: ``raise ValueError`` instead of generic ``Exception`` for
  the "exactly one output format when writing to stdout" check —
  signals to programmatic callers that this is an argument-validity
  error, not an internal failure.

* wv_cli.py: ``raise click.UsageError`` instead of generic
  ``Exception`` for the same check on the CLI side. Click renders
  UsageError with a "Try 'wireviz -h' for help." footer instead of a
  Python traceback, which is the right UX.

Verified with build_examples.py: deterministic outputs (.gv, .bom.tsv)
remain byte-identical to the baseline; the ``output_formats="svg"``
programmatic path now produces a valid SVG; multi-format-to-stdout is
still rejected, now with a clean Click-formatted error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SomethingNew71 SomethingNew71 force-pushed the port-321-stdin-stdout branch from eb850c0 to c8d9469 Compare May 4, 2026 23:23
@SomethingNew71 SomethingNew71 merged commit b38ea55 into upstream-fixes May 4, 2026
@SomethingNew71
Copy link
Copy Markdown
Author

@ggrossetie — heads up, your upstream #321 (stdin/stdout streaming) is being incorporated here.

Context: We use WireViz to document Classic Mini Cooper wiring harnesses at Classic Mini DIY and are building a GUI front-end. Upstream wireviz/WireViz looks abandoned (last real master commit was 2021, the dev refactor has been sitting unreleased for over a year), so we're forking and pulling in the open PRs we want.

Your stdin/stdout PR is the single most consequential one for our GUI direction — it lets the front-end pipe YAML in and rendered SVG/PNG out without temp files. The port adapts the patch to current master since your PR was originally against dev and pre-dates the file_read_text / file_write_text rename. The core ideas (in-memory render dict, dropping the .tmp.svg roundtrip, routing log output to stderr) are preserved. Commit attribution links back to your PR.

Two small adaptations worth flagging:

Thanks for the work — really appreciate it.

SomethingNew71 added a commit that referenced this pull request May 5, 2026
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>
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