resolves #320 read from stdin and write to stdout

This commit is contained in:
Guillaume Grossetie 2023-07-14 14:34:01 +02:00
parent 92af90518c
commit 76fa38857f
9 changed files with 158 additions and 138 deletions

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import sys
from dataclasses import InitVar, dataclass, field
from enum import Enum, auto
from pathlib import Path
@ -280,7 +280,7 @@ class Cable:
self.gauge = g
if self.gauge_unit is not None:
print(
sys.stderr.write(
f"Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}"
)
if u.upper() == "AWG":
@ -304,7 +304,7 @@ class Cable:
)
self.length = L
if self.length_unit is not None:
print(
sys.stderr.write(
f"Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}"
)
self.length_unit = u

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
import re
import sys
from collections import Counter
from dataclasses import dataclass
from itertools import zip_longest
from pathlib import Path
from typing import Any, List, Union
from typing import Any, List, Union, Optional
from graphviz import Graph
@ -20,13 +21,12 @@ from wireviz.DataClasses import (
Tweak,
Side,
)
from wireviz.svgembed import embed_svg_images_file
from wireviz.svgembed import embed_svg_images
from wireviz.wv_bom import (
HEADER_MPN,
HEADER_PN,
HEADER_SPN,
bom_list,
component_table_entry,
generate_bom,
get_additional_component_table,
pn_info_string,
@ -543,11 +543,11 @@ class Harness:
f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', "", entry
)
if n_subs < 1:
print(
sys.stderr.write(
f"Harness.create_graph() warning: {attr} not found in {keyword}!"
)
elif n_subs > 1:
print(
sys.stderr.write(
f"Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!"
)
continue
@ -562,7 +562,7 @@ class Harness:
# If attr not found, then append it
entry = re.sub(r"\]$", f" {attr}={value}]", entry)
elif n_subs > 1:
print(
sys.stderr.write(
f"Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!"
)
@ -650,55 +650,61 @@ class Harness:
graph = self.graph
return embed_svg_images(graph.pipe(format="svg").decode("utf-8"), Path.cwd())
def output(
self,
filename: (str, Path),
view: bool = False,
cleanup: bool = True,
fmt: tuple = ("html", "png", "svg", "tsv"),
self,
output_dir: Optional[Union[str, Path]],
output_name: Optional[Union[str, Path]],
formats: List[str] | tuple[str]
) -> None:
# graphical output
graph = self.graph
svg_already_exists = Path(
f"{filename}.svg"
).exists() # if SVG already exists, do not delete later
# graphical output
for f in fmt:
if f in ("png", "svg", "html"):
if f == "html": # if HTML format is specified,
f = "svg" # generate SVG for embedding into HTML
# SVG file will be renamed/deleted later
_filename = f"{filename}.tmp" if f == "svg" else filename
# TODO: prevent rendering SVG twice when both SVG and HTML are specified
graph.format = f
graph.render(filename=_filename, view=view, cleanup=cleanup)
# embed images into SVG output
if "svg" in fmt or "html" in fmt:
embed_svg_images_file(f"{filename}.tmp.svg")
# GraphViz output
if "gv" in fmt:
graph.save(filename=f"{filename}.gv")
# BOM output
bomlist = bom_list(self.bom())
if "tsv" in fmt:
open_file_write(f"{filename}.bom.tsv").write(tuplelist2tsv(bomlist))
if "csv" in fmt:
# TODO: implement CSV output (preferrably using CSV library)
print("CSV output is not yet supported")
# HTML output
if "html" in fmt:
generate_html_output(filename, bomlist, self.metadata, self.options)
# PDF output
if "pdf" in fmt:
if "csv" in formats:
# TODO: implement CSV output (preferably using CSV library)
sys.stderr.write("CSV output is not yet supported")
if "pdf" in formats:
# TODO: implement PDF output
print("PDF output is not yet supported")
# delete SVG if not needed
if "html" in fmt and not "svg" in fmt:
# SVG file was just needed to generate HTML
Path(f"{filename}.tmp.svg").unlink()
elif "svg" in fmt:
Path(f"{filename}.tmp.svg").replace(f"{filename}.svg")
sys.stderr.write("PDF output is not yet supported")
outputs = {}
if "svg" in formats or "html" in formats:
# embed images into SVG output
outputs["svg"] = embed_svg_images(graph.pipe(format="svg", encoding="utf8"))
if "png" in formats:
outputs["png"] = graph.pipe(format="png")
# GraphViz output
if "gv" in formats:
outputs["gv"] = graph.pipe(format="gv")
if "tsv" in formats or "html" in formats:
bomlist = bom_list(self.bom())
# BOM output
if "tsv" in formats:
outputs["tsv"] = tuplelist2tsv(bomlist)
# HTML output
if "html" in formats and "svg" in outputs:
outputs["html"] = generate_html_output(outputs["svg"], output_dir, bomlist, self.metadata, self.options)
# print to stdout or write files in order
for f in formats:
if f in outputs:
output = outputs[f]
if output_dir is None or output_name is None:
if isinstance(output, (bytes, bytearray)):
sys.stdout.buffer.write(output)
else:
sys.stdout.write(output)
else:
file = f"{output_dir}/{output_name}.{f}"
if isinstance(output, (bytes, bytearray)):
with open(file, "wb") as binary_file:
binary_file.write(output)
else:
with open(file, "w") as binary_file:
binary_file.write(output)
def bom(self):
if not self._bom:

View File

@ -4,5 +4,5 @@
__version__ = "0.4-dev"
CMD_NAME = "wireviz" # Lower case command and module name
APP_NAME = "WireViz" # Application name in texts meant to be human readable
APP_URL = "https://github.com/formatc1702/WireViz"
APP_NAME = "WireViz" # Application name in texts meant to be human-readable
APP_URL = "https://github.com/wireviz/WireViz"

View File

@ -38,15 +38,3 @@ def get_mime_subtype(filename: Union[str, Path]) -> str:
if mime_subtype in mime_subtype_replacements:
mime_subtype = mime_subtype_replacements[mime_subtype]
return mime_subtype
def embed_svg_images_file(
filename_in: Union[str, Path], overwrite: bool = True
) -> None:
filename_in = Path(filename_in).resolve()
filename_out = filename_in.with_suffix(".b64.svg")
filename_out.write_text(
embed_svg_images(filename_in.read_text(), filename_in.parent)
)
if overwrite:
filename_out.replace(filename_in)

View File

@ -58,7 +58,7 @@ def parse(
return_types (optional):
One of the supported return types (see above), or a tuple of multiple return types.
If set to None, no output is returned by the function.
output_formats (optional):
output_formats (Tuple[str], optional):
One of the supported output types (see above), or a tuple of multiple output formats.
If set to None, no files are generated.
output_dir (Path | str, optional):
@ -87,15 +87,18 @@ def parse(
yaml_data, yaml_file = _get_yaml_data_and_path(inp)
if output_formats:
# need to write data to file, determine output directory and filename
output_dir = _get_output_dir(yaml_file, output_dir)
output_name = _get_output_name(yaml_file, output_name)
output_file = output_dir / output_name
if str(output_dir) == "-":
# write to stdout
output_dir = None
else:
# write to directory
output_dir = _get_output_dir(yaml_file, output_dir)
output_name = _get_output_name(yaml_file, output_name)
if yaml_file:
# if reading from file, ensure that input file's parent directory is included in image_paths
default_image_path = yaml_file.parent.resolve()
if not default_image_path in [Path(x).resolve() for x in image_paths]:
if default_image_path not in [Path(x).resolve() for x in image_paths]:
image_paths.append(default_image_path)
# define variables =========================================================
@ -362,11 +365,11 @@ def parse(
harness.add_bom_item(line)
if output_formats:
harness.output(filename=output_file, fmt=output_formats, view=False)
harness.output(formats=output_formats, output_dir=output_dir, output_name=output_name)
if return_types:
returns = []
if isinstance(return_types, str): # only one return type speficied
if isinstance(return_types, str): # only one return type specified
return_types = [return_types]
return_types = [t.lower() for t in return_types]
@ -390,10 +393,11 @@ def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> (Dict, Path):
# if no FileNotFoundError exception happens, get file contents
yaml_str = open_file_read(yaml_path).read()
except (FileNotFoundError, OSError) as e:
# if inp is a long YAML string, Pathlib will raise OSError: [Errno 63]
# if inp is a long YAML string, Pathlib will raise OSError: [Errno 63].
# when trying to expand and resolve it as a path.
# Catch this error, but raise any others
if type(e) is OSError and e.errno != 63:
# it can also raise OSError: [Errno 36] File name too long.
# Catch these errors, but raise any others.
if type(e) is OSError and e.errno != 63 and e.errno != 36:
raise e
# file does not exist; assume inp is a YAML string
yaml_str = inp
@ -417,7 +421,7 @@ def _get_output_dir(input_file: Path, default_output_dir: Path) -> Path:
return output_dir.resolve()
def _get_output_name(input_file: Path, default_output_name: Path) -> str:
def _get_output_name(input_file: Path, default_output_name: Union[None, str]) -> str:
if default_output_name: # user-specified output name
output_name = default_output_name
else: # auto-determine appropriate output name
@ -429,7 +433,7 @@ def _get_output_name(input_file: Path, default_output_name: Path) -> str:
def main():
print("When running from the command line, please use wv_cli.py instead.")
sys.stderr.write("When running from the command line, please use wv_cli.py instead.")
if __name__ == "__main__":

View File

@ -3,6 +3,7 @@
import os
import sys
from pathlib import Path
from typing import Union
import click
@ -67,12 +68,11 @@ epilog += ", ".join([f"{key} ({value.upper()})" for key, value in format_codes.i
default=False,
help=f"Output {APP_NAME} version and exit.",
)
def wireviz(file, format, prepend, output_dir, output_name, version):
def wireviz(file, format, prepend, output_dir: Union[Path, None], output_name, version):
"""
Parses the provided FILE and generates the specified outputs.
"""
print()
print(f"{APP_NAME} {__version__}")
sys.stderr.write(f"{APP_NAME} {__version__}\n")
if version:
return # print version number only and exit
@ -88,10 +88,13 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
output_formats = []
for code in format:
if code in format_codes:
output_formats.append(format_codes[code])
output_format: str = format_codes[code]
# unique
if output_format not in output_formats:
output_formats.append(output_format)
else:
raise Exception(f"Unknown output format: {code}")
output_formats = tuple(sorted(set(output_formats)))
output_formats_str = (
f'[{"|".join(output_formats)}]'
if len(output_formats) > 1
@ -105,44 +108,63 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
prepend_file = Path(prepend_file)
if not prepend_file.exists():
raise Exception(f"File does not exist:\n{prepend_file}")
print("Prepend file:", prepend_file)
sys.stderr.write(f"Prepend file: {prepend_file}")
prepend_input += open_file_read(prepend_file).read() + "\n"
else:
prepend_input = ""
# run WireVIz on each input file
for file in filepaths:
file = Path(file)
if not file.exists():
raise Exception(f"File does not exist:\n{file}")
# file_out = file.with_suffix("") if not output_file else output_file
_output_dir = file.parent if not output_dir else output_dir
_output_name = file.stem if not output_name else output_name
print("Input file: ", file)
print(
"Output file: ", f"{Path(_output_dir / _output_name)}.{output_formats_str}"
output_stdout = str(output_dir) == "-" or output_name == "-"
if output_stdout and len(filepaths) == 0:
wv.parse(
sys.stdin.read(),
output_dir="-",
output_formats=tuple(output_formats),
)
yaml_input = open_file_read(file).read()
file_dir = file.parent
# run WireViz on each input file
for file in filepaths:
if file == "-":
# read from stdin
yaml_input = prepend_input + sys.stdin.read()
image_paths = []
else:
file = Path(file)
if not file.exists():
raise Exception(f"File does not exist:\n{file}")
sys.stderr.write(f"Input file: {file}\n")
yaml_input = open_file_read(file).read()
file_dir = file.parent
yaml_input = prepend_input + yaml_input
image_paths = {file_dir}
yaml_input = prepend_input + yaml_input
image_paths = {file_dir}
for p in prepend:
image_paths.add(Path(p).parent)
# file_out = file.with_suffix("") if not output_file else output_file
if output_stdout or file == "-":
sys.stderr.write(f"Output: <stdout>.{output_formats_str}\n")
_output_dir = "-"
_output_name = None
else:
_output_dir = file.parent if not output_dir else output_dir
_output_name = file.stem if not output_name else output_name
sys.stderr.write(
f"Output file: {Path(_output_dir / _output_name)}.{output_formats_str}"
)
wv.parse(
yaml_input,
output_formats=output_formats,
output_formats=tuple(output_formats),
output_dir=_output_dir,
output_name=_output_name,
image_paths=list(image_paths),
)
print()
sys.stderr.write('')
if __name__ == "__main__":

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import sys
from typing import Dict, List
COLOR_CODES = {
@ -138,7 +138,7 @@ def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
if c[0] != "#" or not all(d in _hex_digits for d in c[1:]):
if c != input:
c += f" in input: {input}"
print(f"Invalid hex color: {c}")
sys.stderr.write(f"Invalid hex color: {c}")
output[i] = color_default
else: # Color name(s)
@ -148,7 +148,7 @@ def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
except KeyError:
if c != input:
c += f" in input: {input}"
print(f"Unknown color name: {c}")
sys.stderr.write(f"Unknown color name: {c}")
return color_default
output = [lookup(input[i : i + 2]) for i in range(0, len(input), 2)]

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import re
import sys
from pathlib import Path
from typing import Dict, List
@ -147,10 +148,10 @@ def aspect_ratio(image_src):
image = Image.open(image_src)
if image.width > 0 and image.height > 0:
return image.width / image.height
print(f"aspect_ratio(): Invalid image size {image.width} x {image.height}")
sys.stderr.write(f"aspect_ratio(): Invalid image size {image.width} x {image.height}")
# ModuleNotFoundError and FileNotFoundError are the most expected, but all are handled equally.
except Exception as error:
print(f"aspect_ratio(): {type(error).__name__}: {error}")
sys.stderr.write(f"aspect_ratio(): {type(error).__name__}: {error}")
return 1 # Assume 1:1 when unable to read actual image size

View File

@ -2,7 +2,7 @@
import re
from pathlib import Path
from typing import Dict, List, Union
from typing import Dict, List, Optional
from wireviz import APP_NAME, APP_URL, __version__, wv_colors
from wireviz.DataClasses import Metadata, Options
@ -10,45 +10,46 @@ from wireviz.wv_gv_html import html_line_breaks
from wireviz.wv_helper import (
flatten2d,
open_file_read,
open_file_write,
smart_file_resolve,
)
def generate_html_output(
filename: Union[str, Path],
svg_input: str,
output_dir: Optional[Path],
bom_list: List[List[str]],
metadata: Metadata,
options: Options,
):
) -> str:
# load HTML template
templatename = metadata.get("template", {}).get("name")
if templatename:
# if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory
templatefile = smart_file_resolve(
f"{templatename}.html",
[Path(filename).parent, Path(__file__).parent / "templates"],
)
else:
# fall back to built-in simple template if no template was provided
templatefile = Path(__file__).parent / "templates/simple.html"
template_name = metadata.get("template", {}).get("name")
builtin_template_directory = Path(__file__).parent / "templates" # built-in template directory
if template_name:
possible_paths = []
# if relative path to template was provided, check directory of YAML file first
if output_dir is not None:
possible_paths.append(output_dir)
html = open_file_read(templatefile).read()
possible_paths.append(builtin_template_directory) # fallback
template_file = smart_file_resolve(f"{template_name}.html", possible_paths)
else:
# fallback to built-in simple template if no template was provided
template_file = builtin_template_directory / "simple.html"
html = open_file_read(template_file).read()
# embed SVG diagram
with open_file_read(f"{filename}.tmp.svg") as file:
svgdata = re.sub(
"^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
"<!-- XML and DOCTYPE declarations from SVG file removed -->",
file.read(),
1,
)
svg_data = re.sub(
"^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
"<!-- XML and DOCTYPE declarations from SVG file removed -->",
svg_input,
1,
)
# generate BOM table
bom = flatten2d(bom_list)
# generate BOM header (may be at the top or bottom of the table)
# generate BOM header (might be at the top or bottom of the table)
bom_header_html = " <tr>\n"
for item in bom[0]:
th_class = f"bom_col_{item.lower()}"
@ -80,7 +81,7 @@ def generate_html_output(
"<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
"<!-- %fontname% -->": options.fontname,
"<!-- %bgcolor% -->": wv_colors.translate_color(options.bgcolor, "hex"),
"<!-- %diagram% -->": svgdata,
"<!-- %diagram% -->": svg_data,
"<!-- %bom% -->": bom_html,
"<!-- %bom_reversed% -->": bom_html_reversed,
"<!-- %sheet_current% -->": "1", # TODO: handle multi-page documents
@ -114,6 +115,4 @@ def generate_html_output(
replacements_sorted = sorted(replacements, key=len, reverse=True)
replacements_escaped = map(re.escape, replacements_sorted)
pattern = re.compile("|".join(replacements_escaped))
html = pattern.sub(lambda match: replacements[match.group(0)], html)
open_file_write(f"{filename}.html").write(html)
return pattern.sub(lambda match: replacements[match.group(0)], html)