Add stdin/stdout streaming (port of upstream PR #321)#2
Conversation
There was a problem hiding this comment.
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.
| self, | ||
| filename: (str, Path), | ||
| filename: Optional[Union[str, Path]], | ||
| fmt: tuple = ("html", "png", "svg", "tsv"), |
There was a problem hiding this comment.
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.
| fmt: tuple = ("html", "png", "svg", "tsv"), | |
| fmt: Union[str, tuple, list] = ("html", "png", "svg", "tsv"), |
| outputs: Dict[str, Union[str, bytes]] = self._render( | ||
| fmt, | ||
| output_dir=output_dir, | ||
| output_name=output_name, | ||
| ) |
There was a problem hiding this comment.
| 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() | ||
| ) |
There was a problem hiding this comment.
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
)| # -*- coding: utf-8 -*- | ||
|
|
||
| import re | ||
| import sys |
|
|
||
| def _render( | ||
| self, | ||
| fmt: tuple, |
| import base64 | ||
|
|
||
| graph = self.graph |
| raise Exception( | ||
| "Exactly one output format must be specified when writing to stdout." | ||
| ) |
There was a problem hiding this comment.
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.
| 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." | |
| ) |
| raise Exception( | ||
| "Exactly one output format (-f) must be specified when writing to stdout." | ||
| ) |
There was a problem hiding this comment.
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.
| 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." | |
| ) |
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>
eb850c0 to
c8d9469
Compare
|
@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 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 Two small adaptations worth flagging:
Thanks for the work — really appreciate it. |
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>
Summary
Ports upstream PR #321 (originally targeting upstream
dev) to ourmaster-derivedupstream-fixesbranch, enabling the WireViz CLI to participate in unix pipelines.This is the highest-value PR for the upcoming
wireviz-guiwork — the GUI can pipe YAML in and get rendered SVG/PNG out without temp files.Usage
-as the input filename to read YAML from stdin.-to either--output-diror--output-nameto write to stdout.Implementation
Two refactors enable the streaming path while keeping file mode bit-for-bit unchanged:
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 callingembed_svg_images()on the SVG string. The old code rendered to a temporary.tmp.svgfile, embedded images viaembed_svg_images_file(), then renamed/deleted the temp. That dance is gone —embed_svg_images_fileis dropped as unused.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. Newoutput_dir/output_name/png_b64/source_pathparameters preserve<!-- %filename% -->,<!-- %filename_stem% -->, and<!-- %diagram_png_b64% -->placeholders in file mode (and leave them empty/unresolved in stdout mode).Library-level
print()warnings inDataClasses.py,Harness.py,wv_colors.py,wv_helper.py, andwireviz.pyare routed tosys.stderrso stdout stays clean of log noise when piped. CLI status banners inwv_cli.pygo to stderr for the same reason.Differences from upstream PR wireviz#321
file_read_text/file_write_texthelpers (the upstream PR was written against the olderopen_file_read/open_file_writeAPI).source_pathtemplate-search additions (already onupstream-fixes) —generate_html_output()now takes asource_pathparameter and the search order is source-dir → output-dir → built-in templates.<!-- %diagram_png_b64% -->working by reading from the just-rendered PNG; in stdout mode the placeholder is unresolved (rare edge case).Verification
build_examples.pyruns cleanly across all 14 examples + 8 tutorials + 2 demos..gv,.bom.tsv) are byte-identical to baseline → no behavior change in file mode.Test plan
cat examples/demo01.yml | wireviz -f s -O - - > /tmp/out.svgproduces a valid SVGcat examples/demo01.yml | wireviz -f p -O - - > /tmp/out.pngproduces a valid PNG🤖 Generated with Claude Code