treewide: use pathlib and force path arguments

This commit is contained in:
Laurier Loiselle 2023-01-25 16:39:30 -05:00
parent 76b2c271f2
commit 565391e72f
No known key found for this signature in database
GPG Key ID: 345920CC72089A3F
6 changed files with 116 additions and 223 deletions

View File

@ -2,17 +2,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import argparse import argparse
import click
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
import click
script_path = Path(__file__).absolute() script_path = Path(__file__).absolute()
sys.path.insert(0, str(script_path.parent.parent.parent)) # to find wireviz module sys.path.insert(0, str(script_path.parent.parent.parent)) # to find wireviz module
from wireviz import APP_NAME, __version__ from wireviz import APP_NAME, __version__
from wireviz.wv_cli import cli from wireviz.wv_cli import cli
from wireviz.wv_utils import open_file_append, open_file_read, open_file_write
base_dir = script_path.parent.parent.parent.parent base_dir = script_path.parent.parent.parent.parent
readme = "readme.md" readme = "readme.md"
@ -60,22 +60,24 @@ def build_generated(groupkeys):
if build_readme: if build_readme:
include_readme = "md" in groups[key][readme] include_readme = "md" in groups[key][readme]
include_source = "yml" in groups[key][readme] include_source = "yml" in groups[key][readme]
with open_file_write(path / readme) as out: with (path / readme).open("w") as out:
out.write(f'# {groups[key]["title"]}\n\n') out.write(f'# {groups[key]["title"]}\n\n')
# collect and iterate input YAML files # collect and iterate input YAML files
for yaml_file in collect_filenames("Building", key, input_extensions): for yaml_file in collect_filenames("Building", key, input_extensions):
try: try:
res = cli(['--format', 'ghpst', str(yaml_file)]) res = cli(["--formats", "ghpst", str(yaml_file)])
except BaseException as e: except BaseException as e:
if str(e) != '0' and not isinstance(e, (click.ClickException, SystemExit)): if str(e) != "0" and not isinstance(
e, (click.ClickException, SystemExit)
):
raise raise
if build_readme: if build_readme:
i = "".join(filter(str.isdigit, yaml_file.stem)) i = "".join(filter(str.isdigit, yaml_file.stem))
with open_file_append(path / readme) as out: with (path / readme).open("a") as out:
if include_readme: if include_readme:
with open_file_read(yaml_file.with_suffix(".md")) as info: with yaml_file.with_suffix(".md").open("r") as info:
for line in info: for line in info:
out.write(line.replace("## ", f"## {i} - ")) out.write(line.replace("## ", f"## {i} - "))
out.write("\n\n") out.write("\n\n")
@ -83,7 +85,7 @@ def build_generated(groupkeys):
out.write(f"## Example {i}\n") out.write(f"## Example {i}\n")
if include_source: if include_source:
with open_file_read(yaml_file) as src: with yaml_file.open("r") as src:
out.write("```yaml\n") out.write("```yaml\n")
for line in src: for line in src:
out.write(line) out.write(line)

View File

@ -2,33 +2,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Tuple, Union from typing import Any, Dict, List, Tuple, Union
import yaml import yaml
if __name__ == "__main__":
sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH
from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
from wireviz.wv_harness import Harness from wireviz.wv_harness import Harness
from wireviz.wv_utils import ( from wireviz.wv_utils import (
expand, expand,
get_single_key_and_value, get_single_key_and_value,
is_arrow, is_arrow,
open_file_read,
smart_file_resolve, smart_file_resolve,
) )
def parse( def parse(
inp: Union[Path, str, Dict], inp: List[Path],
return_types: Union[None, str, Tuple[str]] = None, return_types: Union[None, str, Tuple[str]] = None,
output_formats: Union[None, str, Tuple[str]] = None, output_formats: Union[None, str, Tuple[str]] = None,
output_dir: Union[str, Path] = None, output_dir: Path = None,
output_name: Union[None, str] = None, output_name: Union[None, str] = None,
image_paths: Union[Path, str, List] = [],
extra_metadata: Dict = {}, extra_metadata: Dict = {},
shared_bom: Dict = {}, shared_bom: Dict = {},
) -> Any: ) -> Any:
@ -37,9 +31,7 @@ def parse(
and outputs the result as one or more files and/or as a function return value and outputs the result as one or more files and/or as a function return value
Accepted inputs: Accepted inputs:
* A Path object or a path-like string pointing to a YAML source file to parse * A List of Path object pointing 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: Supported return types:
* "png": the diagram as raw PNG data * "png": the diagram as raw PNG data
@ -56,7 +48,7 @@ def parse(
* "tsv": the BOM, as a tab-separated text file * "tsv": the BOM, as a tab-separated text file
Args: Args:
inp (Path | str | Dict): inp:
The input to be parsed (see above for accepted inputs). The input to be parsed (see above for accepted inputs).
return_types (optional): return_types (optional):
One of the supported return types (see above), or a tuple of multiple return types. One of the supported return types (see above), or a tuple of multiple return types.
@ -71,10 +63,6 @@ def parse(
The name to use for the generated output files (without extension). The name to use for the generated output files (without extension).
Defaults to inp's file name (without extension). Defaults to inp's file name (without extension).
Required parameter if inp is not a path. 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.
extra_metadata (Dict, optional): extra_metadata (Dict, optional):
Any metadata to add to the template. Any metadata to add to the template.
Normally, this should contain programmatic metadata Normally, this should contain programmatic metadata
@ -82,28 +70,19 @@ def parse(
Returns: Returns:
Depending on the return_types parameter, may return: Depending on the return_types parameter, may return:
* None * None
* one of the following, or a tuple containing two or more of the following: * A dict of {return_type: data}
* PNG data
* SVG data
* a Harness object
""" """
if not output_formats and not return_types: if not output_formats and not return_types:
raise Exception("No output formats or return types specified") raise Exception("No output formats or return types specified")
yaml_data, yaml_file = _get_yaml_data_and_path(inp) yaml_file = inp[-1]
if output_formats: yaml_data_str = "\n".join(f.open("r").read() for f in inp)
# need to write data to file, determine output directory and filename yaml_data = yaml.safe_load(yaml_data_str)
output_dir = _get_output_dir(yaml_file, output_dir) image_paths = {f.parent for f in inp if f.parent.is_dir()}
output_name = _get_output_name(yaml_file, output_name)
output_file = output_dir / output_name
if yaml_file: output_dir = yaml_file.parent if not output_dir else output_dir
# if reading from file, ensure that input file's parent directory output_name = yaml_file.stem if not output_name else output_name
# 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 ========================================================= # define variables =========================================================
# containers for parsed component data and connection sets # containers for parsed component data and connection sets
@ -124,7 +103,7 @@ def parse(
autogenerated_designators = {} autogenerated_designators = {}
if "title" not in harness.metadata: if "title" not in harness.metadata:
harness.metadata["title"] = Path(yaml_file).stem if yaml_file else "" harness.metadata["title"] = yaml_file.stem
# add items # add items
# parse YAML input file ==================================================== # parse YAML input file ====================================================
@ -140,8 +119,8 @@ def parse(
# an image file with a relative path. # an image file with a relative path.
image = attribs.get("image") image = attribs.get("image")
if isinstance(image, dict): if isinstance(image, dict):
image_path = image["src"] image_path = Path(image["src"])
if image_path and not Path(image_path).is_absolute(): if image_path and not image_path.is_absolute():
# resolve relative image path # resolve relative image path
image["src"] = smart_file_resolve( image["src"] = smart_file_resolve(
image_path, image_paths image_path, image_paths
@ -384,68 +363,25 @@ def parse(
harness.populate_bom() harness.populate_bom()
if output_formats: if output_formats:
harness.output(filename=output_file, fmt=output_formats, view=False) harness.output(
filename=output_dir / output_name, fmt=output_formats, view=False
)
if return_types: if return_types:
returns = []
if isinstance(return_types, str): # only one return type speficied if isinstance(return_types, str): # only one return type speficied
return_types = [return_types] return_types = [return_types]
return_types = [t.lower() for t in return_types] return_types = [t.lower() for t in return_types]
returns = {}
for rt in return_types: for rt in return_types:
if rt == "png": if rt == "png":
returns.append(harness.png) returns["png"] = harness.png
if rt == "svg": if rt == "svg":
returns.append(harness.svg) returns["svg"] = harness.svg
if rt == "harness": if rt == "harness":
returns.append(harness) returns["harness"] = harness
if rt == "shared_bom":
returns["shared_bom"] = harness.shared_bom
return tuple(returns) if len(returns) != 1 else returns[0] return returns
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
if isinstance(inp, Path) or (isinstance(inp, str) and not "\n" in inp):
yaml_path = Path(inp).expanduser().resolve(strict=True)
yaml_str = open_file_read(yaml_path).read()
else:
yaml_path = None
yaml_str = inp
yaml_data = yaml.safe_load(yaml_str)
else:
# 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():
print("When running from the command line, please use wv_cli.py instead.")
if __name__ == "__main__":
main()

View File

@ -11,7 +11,8 @@ if __name__ == "__main__":
import wireviz.wireviz as wv import wireviz.wireviz as wv
from wireviz import APP_NAME, __version__ from wireviz import APP_NAME, __version__
from wireviz.wv_utils import open_file_read from wireviz.wv_bom import bom_list
from wireviz.wv_utils import bom2tsv
format_codes = { format_codes = {
"c": "csv", "c": "csv",
@ -24,19 +25,28 @@ format_codes = {
"b": "shared_bom", "b": "shared_bom",
} }
epilog = ( epilog = (
"The -f or --format option accepts a string containing one or more of the " "The -f or --formats option accepts a string containing one or more of the "
"following characters to specify which file types to output:\n" "following characters to specify which file types to output:\n"
+ f", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()]) + f", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()])
) )
@click.command(epilog=epilog, no_args_is_help=True) @click.command(epilog=epilog, no_args_is_help=True)
@click.argument("file", nargs=-1) @click.argument(
"files",
type=click.Path(
exists=True,
readable=True,
dir_okay=False,
path_type=Path,
),
nargs=-1,
required=True,
)
@click.option( @click.option(
"-f", "-f",
"--format", "--formats",
default="hpst", default="hpst",
type=str, type=str,
show_default=True, show_default=True,
@ -47,14 +57,25 @@ epilog = (
"--prepend", "--prepend",
default=[], default=[],
multiple=True, multiple=True,
type=Path, type=click.Path(
exists=True,
readable=True,
file_okay=True,
path_type=Path,
),
help="YAML file to prepend to the input file (optional).", help="YAML file to prepend to the input file (optional).",
) )
@click.option( @click.option(
"-o", "-o",
"--output-dir", "--output-dir",
default=None, default=None,
type=Path, type=click.Path(
exists=True,
readable=True,
file_okay=False,
dir_okay=True,
path_type=Path,
),
help="Directory to use for output files, if different from input file directory.", help="Directory to use for output files, if different from input file directory.",
) )
@click.option( @click.option(
@ -74,100 +95,55 @@ epilog = (
default=False, default=False,
help=f"Output {APP_NAME} version and exit.", help=f"Output {APP_NAME} version and exit.",
) )
def cli(file, format, prepend, output_dir, output_name, version): def cli(files, formats, prepend, output_dir, output_name, version):
""" """
Parses the provided FILE and generates the specified outputs. Parses the provided FILE and generates the specified outputs.
""" """
print() # blank line before execution
print(f"{APP_NAME} {__version__}")
if version: if version:
print(f"{APP_NAME} {__version__}")
return # print version number only and exit return # print version number only and exit
# get list of files _output_dir = files[0].parent if not output_dir else output_dir
try:
_ = iter(file)
except TypeError:
filepaths = [file]
else:
filepaths = list(file)
# determine output formats # determine output formats
output_formats = [] output_formats = {format_codes[f] for f in formats if f in format_codes}
for code in format:
if code in format_codes:
output_formats.append(format_codes[code])
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
else output_formats[0]
)
# check prepend file
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}")
if not prepend_file.is_file():
raise Exception(f"Path is not a file:\n{prepend_file}")
print("Prepend file:", prepend_file)
prepend_input += open_file_read(prepend_file).read() + "\n"
else:
prepend_input = ""
harness = None harness = None
shared_bom = {} shared_bom = {}
sheet_current = 1 sheet_current = 1
# run WireVIz on each input file # run WireVIz on each input file
for file in filepaths: for _file in files:
file = Path(file) _output_name = _file.stem if not output_name else output_name
if not file.exists():
raise Exception(f"File does not exist:\n{file}") print("Input file: ", _file)
if not file.is_file(): print(
raise Exception(f"Path is not a file:\n{file}") "Output file: ",
f"{_output_dir / _output_name}.[{'|'.join(output_formats)}]",
)
extra_metadata = {} extra_metadata = {}
extra_metadata["sheet_name"] = file.stem extra_metadata["sheet_name"] = _output_name.upper()
extra_metadata["sheet_total"] = len(filepaths) extra_metadata["sheet_total"] = len(files)
extra_metadata["sheet_current"] = sheet_current extra_metadata["sheet_current"] = sheet_current
sheet_current += 1 sheet_current += 1
# file_out = file.with_suffix("") if not output_file else output_file file_dir = _file.parent
_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) ret = wv.parse(
print( prepend + (_file,),
"Output file: ", f"{Path(_output_dir / _output_name)}.{output_formats_str}" return_types=("shared_bom"),
) output_formats=output_formats,
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)
harness = wv.parse(
yaml_input,
return_types=("harness"),
output_formats=[f for f in output_formats if f != "shared_bom"],
output_dir=_output_dir, output_dir=_output_dir,
output_name=_output_name, output_name=_output_name,
image_paths=list(image_paths),
extra_metadata=extra_metadata, extra_metadata=extra_metadata,
shared_bom=shared_bom, shared_bom=shared_bom,
) )
shared_bom = ret["shared_bom"]
if "shared_bom" in output_formats: if "shared_bom" in output_formats:
_output_dir = file.parent if not output_dir else output_dir shared_bomlist = bom_list(shared_bom)
harness.output(str(Path(_output_dir) / "shared_bom"), fmt="shared_bom") shared_bom_tsv = bom2tsv(shared_bomlist)
shared_bom = harness.shared_bom (_output_dir / "shared_bom").with_suffix(".tsv").open("w").write(shared_bom_tsv)
print() # blank line after execution print() # blank line after execution

View File

@ -36,7 +36,7 @@ from wireviz.wv_graphviz import (
set_dot_basics, set_dot_basics,
) )
from wireviz.wv_output import embed_svg_images_file, generate_html_output from wireviz.wv_output import embed_svg_images_file, generate_html_output
from wireviz.wv_utils import bom2tsv, open_file_write from wireviz.wv_utils import bom2tsv
@dataclass @dataclass
@ -407,35 +407,32 @@ class Harness:
) -> None: ) -> None:
# graphical output # graphical output
graph = self.graph graph = self.graph
rendered = set()
for f in fmt: for f in fmt:
if f in ("png", "svg", "html"): if f in ("png", "svg", "html"):
if f == "html": # if HTML format is specified, if f == "html": # if HTML format is specified,
f = "svg" # generate SVG for embedding into HTML f = "svg" # generate SVG for embedding into HTML
# SVG file will be renamed/deleted later # SVG file will be renamed/deleted later
_filename = f"{filename}.tmp" if f == "svg" else filename if f in rendered:
# TODO: prevent rendering SVG twice when both SVG and HTML are specified continue
graph.format = f graph.format = f
graph.render(filename=_filename, view=view, cleanup=cleanup) graph.render(filename=filename, view=view, cleanup=cleanup)
rendered.add(f)
# embed images into SVG output # embed images into SVG output
if "svg" in fmt or "html" in fmt: if "svg" in fmt or "html" in fmt:
embed_svg_images_file(f"{filename}.tmp.svg") embed_svg_images_file(filename.with_suffix(".svg"))
# GraphViz output # GraphViz output
if "gv" in fmt: if "gv" in fmt:
graph.save(filename=f"{filename}.gv") graph.save(filename=filename.with_suffix(".gv"))
# BOM output # BOM output
bomlist = bom_list(self.bom) bomlist = bom_list(self.bom)
# bomlist = [[]]
if "tsv" in fmt: if "tsv" in fmt:
tsv = bom2tsv(bomlist) bom_tsv = bom2tsv(bomlist)
open_file_write(f"{filename}.tsv").write(tsv) filename.with_suffix(".tsv").open("w").write(bom_tsv)
if "csv" in fmt: if "csv" in fmt:
# TODO: implement CSV output (preferrably using CSV library) # TODO: implement CSV output (preferrably using CSV library)
print("CSV output is not yet supported") print("CSV output is not yet supported")
if "shared_bom" in fmt:
shared_bomlist = bom_list(self.shared_bom)
shared_bom_tsv = bom2tsv(shared_bomlist)
open_file_write(f"{filename}.tsv").write(shared_bom_tsv)
# HTML output # HTML output
if "html" in fmt: if "html" in fmt:
generate_html_output(filename, bomlist, self.metadata, self.options) generate_html_output(filename, bomlist, self.metadata, self.options)
@ -446,6 +443,4 @@ class Harness:
# delete SVG if not needed # delete SVG if not needed
if "html" in fmt and not "svg" in fmt: if "html" in fmt and not "svg" in fmt:
# SVG file was just needed to generate HTML # SVG file was just needed to generate HTML
Path(f"{filename}.tmp.svg").unlink() filename.with_suffix(".svg").unlink()
elif "svg" in fmt:
Path(f"{filename}.tmp.svg").replace(f"{filename}.svg")

View File

@ -10,7 +10,6 @@ import jinja2
import wireviz # for doing wireviz.__file__ import wireviz # for doing wireviz.__file__
from wireviz import APP_NAME, APP_URL, __version__ from wireviz import APP_NAME, APP_URL, __version__
from wireviz.wv_dataclasses import Metadata, Options from wireviz.wv_dataclasses import Metadata, Options
from wireviz.wv_utils import open_file_read, open_file_write
mime_subtype_replacements = {"jpg": "jpeg", "tif": "tiff"} mime_subtype_replacements = {"jpg": "jpeg", "tif": "tiff"}
@ -69,21 +68,20 @@ def get_template_html(template_name):
def generate_html_output( def generate_html_output(
filename: Union[str, Path], filename: Path,
bom: List[List[str]], bom: List[List[str]],
metadata: Metadata, metadata: Metadata,
options: Options, options: Options,
): ):
print("Generating html output") print("Generating html output")
template_name = metadata.get("template", {}).get("name", "simple") template_name = metadata.get("template", {}).get("name", "simple")
page_template = get_template_html(template_name)
# embed SVG diagram # embed SVG diagram
with open_file_read(f"{filename}.tmp.svg") as file: with filename.with_suffix(".svg").open("r") as f:
svgdata = re.sub( svgdata = re.sub(
"^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>", "^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
"<!-- XML and DOCTYPE declarations from SVG file removed -->", "<!-- XML and DOCTYPE declarations from SVG file removed -->",
file.read(), f.read(),
1, 1,
) )
@ -175,5 +173,9 @@ def generate_html_output(
titleblock_template = get_template_html("titleblock") titleblock_template = get_template_html("titleblock")
replacements["titleblock"] = titleblock_template.render(replacements) replacements["titleblock"] = titleblock_template.render(replacements)
# generate page template
page_template = get_template_html(template_name)
page_rendered = page_template.render(replacements) page_rendered = page_template.render(replacements)
open_file_write(f"{filename}.html").write(page_rendered)
# save generated file
filename.with_suffix(".html").open("w").write(page_rendered)

View File

@ -117,19 +117,6 @@ def clean_whitespace(inp):
return " ".join(inp.split()).replace(" ,", ",") if isinstance(inp, str) else inp return " ".join(inp.split()).replace(" ,", ",") if isinstance(inp, str) else inp
def open_file_read(filename):
# TODO: Intelligently determine encoding
return open(filename, "r", encoding="UTF-8")
def open_file_write(filename):
return open(filename, "w", encoding="UTF-8")
def open_file_append(filename):
return open(filename, "a", encoding="UTF-8")
def is_arrow(inp): def is_arrow(inp):
""" """
Matches strings of one or multiple `-` or `=` (but not mixed) Matches strings of one or multiple `-` or `=` (but not mixed)
@ -159,25 +146,20 @@ def aspect_ratio(image_src):
return 1 # Assume 1:1 when unable to read actual image size return 1 # Assume 1:1 when unable to read actual image size
def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path: def smart_file_resolve(filename: Path, possible_paths: (Path, List[Path])) -> Path:
if not isinstance(possible_paths, List): if isinstance(possible_paths, Path) or isinstance(possible_paths, str):
possible_paths = [possible_paths] possible_paths = [possible_paths]
filename = Path(filename)
if filename.is_absolute(): if filename.is_absolute():
if filename.exists(): if filename.exists():
return filename return filename
else: else:
raise Exception(f"{filename} does not exist.") raise Exception(f"{filename} does not exist.")
else: # search all possible paths in decreasing order of precedence else: # search all possible paths in decreasing order of precedence
possible_paths = [ for path in possible_paths:
Path(path).resolve() for path in possible_paths if path is not None combined_path = (path / filename).resolve()
] if combined_path.exists():
for possible_path in possible_paths: return combined_path
resolved_path = (possible_path / filename).resolve()
if resolved_path.exists():
return resolved_path
else:
raise Exception( raise Exception(
f"{filename} was not found in any of the following locations: \n" f"{filename} was not found in any of the following locations: \n"
+ "\n".join([str(x) for x in possible_paths]) + "\n".join(str(p) for p in possible_paths)
) )