Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve wireviz.parse() #250

Merged
merged 10 commits into from
Aug 5, 2022
1 change: 1 addition & 0 deletions src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Options:
bgcolor_bundle: Optional[Color] = None
color_mode: ColorMode = "SHORT"
mini_bom_mode: bool = True
template_separator: str = "."

def __post_init__(self):
if not self.bgcolor_node:
Expand Down
3 changes: 1 addition & 2 deletions src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,8 +680,7 @@ def output(
# BOM output
bomlist = bom_list(self.bom())
if "tsv" in fmt:
with open_file_write(f"{filename}.bom.tsv") as file:
file.write(tuplelist2tsv(bomlist))
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")
Expand Down
2 changes: 1 addition & 1 deletion src/wireviz/build_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def build_generated(groupkeys):
# collect and iterate input YAML files
for yaml_file in collect_filenames("Building", key, input_extensions):
print(f' "{yaml_file}"')
wireviz.parse_file(yaml_file)
wireviz.parse(yaml_file, output_formats=("gv", "html", "png", "svg", "tsv"))

if build_readme:
i = "".join(filter(str.isdigit, yaml_file.stem))
Expand Down
184 changes: 120 additions & 64 deletions src/wireviz/wireviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import sys
from pathlib import Path
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Tuple, Union

import yaml

Expand All @@ -21,58 +21,82 @@
)


def parse_text(
yaml_str: str,
file_out: (str, Path) = None,
output_formats: (None, str, Tuple[str]) = ("html", "png", "svg", "tsv"),
return_types: (None, str, Tuple[str]) = None,
image_paths: List = [],
def parse(
inp: Union[Path, str, Dict],
return_types: Union[None, str, Tuple[str]] = None,
output_formats: Union[None, str, Tuple[str]] = None,
output_dir: Union[str, Path] = None,
output_name: Union[None, str] = None,
image_paths: Union[Path, str, List] = [],
) -> Any:
"""
Parses a YAML input string and does the high-level harness conversion

:param yaml_input: a string containing the YAML input data
:param file_out:
:param output_formats:
:param return_types: if None, then returns None; if the value is a string, then a
corresponding data format will be returned; if the value is a tuple of strings,
then for every valid format in the `return_types` tuple, another return type
will be generated and returned in the same order; currently supports:
- "png" - will return the PNG data
- "svg" - will return the SVG data
- "harness" - will return the `Harness` instance
This function takes an input, parses it as a WireViz Harness file,
and outputs the result as one or more files and/or as a function return value

Accepted inputs:
* A path to a YAML source file to parse
* A string containing the YAML data to parse
* A Python Dict containing the pre-parsed YAML data

Supported return types:
* "png": the diagram as raw PNG data
* "svg": the diagram as raw SVG data
* "harness": the diagram as a Harness Python object

Supported output formats:
* "csv": the BOM, as a comma-separated text file
* "gv": the diagram, as a GraphViz source file
* "html": the diagram and (depending on the template) the BOM, as a HTML file
* "png": the diagram, as a PNG raster image
* "pdf": the diagram and (depending on the template) the BOM, as a PDF file
* "svg": the diagram, as a SVG vector image
* "tsv": the BOM, as a tab-separated text file

Args:
inp (Path | str | Dict):
The input to be parsed (see above for accepted inputs).
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):
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):
The directory to place the generated output files.
Defaults to inp's parent directory, or cwd if inp is not a path.
output_name (str, optional):
The name to use for the generated output files (without extension).
Defaults to inp's file name (without extension).
Required parameter if inp is not a path.
image_paths (Path | str | List, optional):
Paths to use when resolving any image paths included in the data.
Note: If inp is a path to a YAML file,
its parent directory will automatically be included in the list.

Returns:
Depending on the return_types parameter, may return:
* None
* one of the following, or a tuple containing two or more of the following:
* PNG data
* SVG data
* a Harness object
"""
yaml_data = yaml.safe_load(yaml_str)
return parse(
yaml_data=yaml_data,
file_out=file_out,
output_formats=output_formats,
return_types=return_types,
image_paths=image_paths,
)

if not output_formats and not return_types:
raise Exception("No output formats or return types specified")

def parse(
yaml_data: Dict,
file_out: (str, Path) = None,
output_formats: (None, str, Tuple[str]) = ("html", "png", "svg", "tsv"),
return_types: (None, str, Tuple[str]) = None,
image_paths: List = [],
) -> Any:
"""
Parses a YAML dictionary and does the high-level harness conversion

:param yaml_data: a dictionary containing the YAML data
:param file_out:
:param output_formats:
:param return_types: if None, then returns None; if the value is a string, then a
corresponding data format will be returned; if the value is a tuple of strings,
then for every valid format in the `return_types` tuple, another return type
will be generated and returned in the same order; currently supports:
- "png" - will return the PNG data
- "svg" - will return the SVG data
- "harness" - will return the `Harness` instance
"""
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 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]:
image_paths.append(default_image_path)

# define variables =========================================================
# containers for parsed component data and connection sets
Expand All @@ -92,7 +116,7 @@ def parse(
autogenerated_designators = {}

if "title" not in harness.metadata:
harness.metadata["title"] = Path(file_out).stem
harness.metadata["title"] = Path(yaml_file).stem if yaml_file else ""

# add items
# parse YAML input file ====================================================
Expand Down Expand Up @@ -129,7 +153,7 @@ def parse(

# go through connection sets, generate and connect components ==============

template_separator_char = "." # TODO: make user-configurable (in case user wants to use `.` as part of their template/component names)
template_separator_char = harness.options.template_separator

def resolve_designator(inp, separator):
if separator in inp: # generate a new instance of an item
Expand Down Expand Up @@ -337,10 +361,10 @@ def alternate_type(): # flip between connector and cable/arrow
for line in yaml_data["additional_bom_items"]:
harness.add_bom_item(line)

if file_out is not None:
harness.output(filename=file_out, fmt=output_formats, view=False)
if output_formats:
harness.output(filename=output_file, fmt=output_formats, view=False)

if return_types is not None:
if return_types:
returns = []
if isinstance(return_types, str): # only one return type speficied
return_types = [return_types]
Expand All @@ -358,18 +382,50 @@ def alternate_type(): # flip between connector and cable/arrow
return tuple(returns) if len(returns) != 1 else returns[0]


def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None:
yaml_file = Path(yaml_file)
with open_file_read(yaml_file) as file:
yaml_str = file.read()

if file_out:
file_out = Path(file_out)
def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> (Dict, Path):
# determine whether inp is a file path, a YAML string, or a Dict
if not isinstance(inp, Dict): # received a str or a Path
try:
yaml_path = Path(inp).expanduser().resolve(strict=True)
# 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]
# 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:
raise e
# file does not exist; assume inp is a YAML string
yaml_str = inp
yaml_path = None
yaml_data = yaml.safe_load(yaml_str)
else:
file_out = yaml_file.parent / yaml_file.stem
file_out = file_out.resolve()

parse_text(yaml_str, file_out=file_out, image_paths=[Path(yaml_file).parent])
# received a Dict, use as-is
yaml_data = inp
yaml_path = None
return yaml_data, yaml_path


def _get_output_dir(input_file: Path, default_output_dir: Path) -> Path:
if default_output_dir: # user-specified output directory
output_dir = Path(default_output_dir)
else: # auto-determine appropriate output directory
if input_file: # input comes from a file; place output in same directory
output_dir = input_file.parent
else: # input comes from str or Dict; fall back to cwd
output_dir = Path.cwd()
return output_dir.resolve()


def _get_output_name(input_file: Path, default_output_name: Path) -> str:
if default_output_name: # user-specified output name
output_name = default_output_name
else: # auto-determine appropriate output name
if input_file: # input comes from a file; use same file stem
output_name = input_file.stem
else: # input comes from str or Dict; no fallback available
raise Exception("No output file name provided")
return output_name


def main():
Expand Down
57 changes: 35 additions & 22 deletions src/wireviz/wv_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,24 @@
@click.option(
"-p",
"--prepend",
default=None,
default=[],
multiple=True,
type=Path,
help="YAML file to prepend to the input file (optional).",
)
@click.option(
"-o",
"--output-file",
"--output-dir",
default=None,
type=Path,
help="File name (without extension) to use for output, if different from input file name.",
help="Directory to use for output files, if different from input file directory.",
)
@click.option(
"-O",
"--output-name",
default=None,
type=str,
help="File name (without extension) to use for output files, if different from input file name.",
)
@click.option(
"-V",
Expand All @@ -59,7 +67,7 @@
default=False,
help=f"Output {APP_NAME} version and exit.",
)
def wireviz(file, format, prepend, output_file, version):
def wireviz(file, format, prepend, output_dir, output_name, version):
"""
Parses the provided FILE and generates the specified outputs.
"""
Expand Down Expand Up @@ -90,43 +98,48 @@ def wireviz(file, format, prepend, output_file, version):
else output_formats[0]
)

image_paths = []
# check prepend file
if prepend:
prepend = Path(prepend)
if not prepend.exists():
raise Exception(f"File does not exist:\n{prepend}")
print("Prepend file:", prepend)

with open_file_read(prepend) as file_handle:
prepend_input = file_handle.read() + "\n"
prepend_dir = prepend.parent
if len(prepend) > 0:
prepend_input = ""
for prepend_file in prepend:
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)

prepend_input += open_file_read(prepend_file).read() + "\n"
else:
prepend_input = ""
prepend_dir = None

# 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
# 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"{file_out}.{output_formats_str}")
print(
"Output file: ", f"{Path(_output_dir / _output_name)}.{output_formats_str}"
)

with open_file_read(file) as file_handle:
yaml_input = file_handle.read()
yaml_input = open_file_read(file).read()
file_dir = file.parent

yaml_input = prepend_input + yaml_input
image_paths = {file_dir}
for p in prepend:
image_paths.add(Path(p).parent)

wv.parse_text(
wv.parse(
yaml_input,
file_out=file_out,
output_formats=output_formats,
image_paths=[file_dir, prepend_dir],
output_dir=_output_dir,
output_name=_output_name,
image_paths=list(image_paths),
)

print()
Expand Down
6 changes: 2 additions & 4 deletions src/wireviz/wv_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ def generate_html_output(
# fall back to built-in simple template if no template was provided
templatefile = Path(__file__).parent / "templates/simple.html"

with open_file_read(templatefile) as file:
html = file.read()
html = open_file_read(templatefile).read()

# embed SVG diagram
with open_file_read(f"{filename}.svg") as file:
Expand Down Expand Up @@ -117,5 +116,4 @@ def generate_html_output(
pattern = re.compile("|".join(replacements_escaped))
html = pattern.sub(lambda match: replacements[match.group(0)], html)

with open_file_write(f"{filename}.html") as file:
file.write(html)
open_file_write(f"{filename}.html").write(html)
Loading