From fb91be402a17ea69e6bf5b9489007031e6d94781 Mon Sep 17 00:00:00 2001 From: Daniel Rojas Date: Thu, 21 Oct 2021 22:41:24 +0200 Subject: [PATCH] Rename modules, adjust imports, move `build_examples.py` --- .github/workflows/main.yml | 4 +- src/wireviz/svgembed.py | 52 ----- src/wireviz/{ => tools}/build_examples.py | 7 +- src/wireviz/wireviz.py | 20 +- src/wireviz/wv_bom.py | 2 +- src/wireviz/wv_cli.py | 5 +- .../{DataClasses.py => wv_dataclasses.py} | 2 +- src/wireviz/{wv_gv_html.py => wv_graphviz.py} | 8 +- src/wireviz/{Harness.py => wv_harness.py} | 9 +- src/wireviz/wv_html.py | 209 +++++++++--------- src/wireviz/wv_output.py | 168 ++++++++++++++ src/wireviz/wv_table_util.py | 125 ----------- src/wireviz/{wv_helper.py => wv_utils.py} | 0 13 files changed, 299 insertions(+), 312 deletions(-) delete mode 100644 src/wireviz/svgembed.py rename src/wireviz/{ => tools}/build_examples.py (96%) rename src/wireviz/{DataClasses.py => wv_dataclasses.py} (99%) rename src/wireviz/{wv_gv_html.py => wv_graphviz.py} (99%) rename src/wireviz/{Harness.py => wv_harness.py} (98%) create mode 100644 src/wireviz/wv_output.py delete mode 100644 src/wireviz/wv_table_util.py rename src/wireviz/{wv_helper.py => wv_utils.py} (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d539aa2..fe576b0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,11 +22,11 @@ jobs: python -m pip install --upgrade pip pip install . - name: Create Examples - run: PYTHONPATH=$(pwd)/src:$PYTHONPATH cd src/wireviz/ && python build_examples.py + run: PYTHONPATH=$(pwd)/src:$PYTHONPATH && python src/wireviz/tools/build_examples.py - name: Upload examples, demos, and tutorials uses: actions/upload-artifact@v2 with: name: examples-and-tutorials path: | examples/ - tutorial/ \ No newline at end of file + tutorial/ diff --git a/src/wireviz/svgembed.py b/src/wireviz/svgembed.py deleted file mode 100644 index ab6b9f1..0000000 --- a/src/wireviz/svgembed.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- - -import base64 -import re -from pathlib import Path -from typing import Union - -mime_subtype_replacements = {"jpg": "jpeg", "tif": "tiff"} - - -def embed_svg_images(svg_in: str, base_path: Union[str, Path] = Path.cwd()) -> str: - images_b64 = {} # cache of base64-encoded images - - def image_tag(pre: str, url: str, post: str) -> str: - return f'' - - def replace(match: re.Match) -> str: - imgurl = match["URL"] - if not imgurl in images_b64: # only encode/cache every unique URL once - imgurl_abs = (Path(base_path) / imgurl).resolve() - image = imgurl_abs.read_bytes() - images_b64[imgurl] = base64.b64encode(image).decode("utf-8") - return image_tag( - match["PRE"] or "", - f"data:image/{get_mime_subtype(imgurl)};base64, {images_b64[imgurl]}", - match["POST"] or "", - ) - - pattern = re.compile( - image_tag(r"(?P
 [^>]*?)?", r'(?P[^"]*?)', r"(?P [^>]*?)?"),
-        re.IGNORECASE,
-    )
-    return pattern.sub(replace, svg_in)
-
-
-def get_mime_subtype(filename: Union[str, Path]) -> str:
-    mime_subtype = Path(filename).suffix.lstrip(".").lower()
-    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)
diff --git a/src/wireviz/build_examples.py b/src/wireviz/tools/build_examples.py
similarity index 96%
rename from src/wireviz/build_examples.py
rename to src/wireviz/tools/build_examples.py
index e54d0f5..5ce9fe4 100755
--- a/src/wireviz/build_examples.py
+++ b/src/wireviz/tools/build_examples.py
@@ -7,13 +7,12 @@ import sys
 from pathlib import Path
 
 script_path = Path(__file__).absolute()
-
-sys.path.insert(0, str(script_path.parent.parent))  # to find wireviz module
-from wv_helper import open_file_append, open_file_read, open_file_write
+sys.path.insert(0, str(script_path.parent.parent.parent))  # to find wireviz module
 
 from wireviz import APP_NAME, __version__, wireviz
+from wireviz.wv_utils import open_file_append, open_file_read, open_file_write
 
-dir = script_path.parent.parent.parent
+dir = script_path.parent.parent.parent.parent
 readme = "readme.md"
 groups = {
     "examples": {
diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py
index 2c6ca77..4dd9b09 100755
--- a/src/wireviz/wireviz.py
+++ b/src/wireviz/wireviz.py
@@ -10,9 +10,9 @@ import yaml
 if __name__ == "__main__":
     sys.path.insert(0, str(Path(__file__).parent.parent))  # add src/wireviz to PATH
 
-from wireviz.DataClasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
-from wireviz.Harness import Harness
-from wireviz.wv_helper import (
+from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
+from wireviz.wv_harness import Harness
+from wireviz.wv_utils import (
     expand,
     get_single_key_and_value,
     is_arrow,
@@ -396,20 +396,12 @@ def parse(
 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:
+        if isinstance(inp, Path) or (isinstance(inp, str) and not "\n" in inp):
             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.ENAMETOOLONG]
-            # when trying to expand and resolve it as a path.
-            # Catch this error, but raise any others
-            from errno import ENAMETOOLONG
-            if type(e) is OSError and e.errno != ENAMETOOLONG:
-                raise e
-            # file does not exist; assume inp is a YAML string
-            yaml_str = inp
+        else:
             yaml_path = None
+            yaml_str = inp
         yaml_data = yaml.safe_load(yaml_str)
     else:
         # received a Dict, use as-is
diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py
index 9cc0fbe..0cb7a3e 100644
--- a/src/wireviz/wv_bom.py
+++ b/src/wireviz/wv_bom.py
@@ -5,7 +5,7 @@ from dataclasses import dataclass
 from enum import Enum
 from typing import List, Optional, Union
 
-from wireviz.wv_helper import html_line_breaks
+from wireviz.wv_utils import html_line_breaks
 
 BOM_HASH_FIELDS = "description unit partnumbers"
 BomHash = namedtuple("BomHash", BOM_HASH_FIELDS)
diff --git a/src/wireviz/wv_cli.py b/src/wireviz/wv_cli.py
index 07aac12..eb0f33c 100644
--- a/src/wireviz/wv_cli.py
+++ b/src/wireviz/wv_cli.py
@@ -11,7 +11,7 @@ if __name__ == "__main__":
 
 import wireviz.wireviz as wv
 from wireviz import APP_NAME, __version__
-from wireviz.wv_helper import open_file_read
+from wireviz.wv_utils import open_file_read
 
 format_codes = {
     "c": "csv",
@@ -23,10 +23,11 @@ format_codes = {
     "t": "tsv",
 }
 
+
 epilog = (
     "The -f or --format option accepts a string containing one or more of the "
     "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()])
 )
 
 
diff --git a/src/wireviz/DataClasses.py b/src/wireviz/wv_dataclasses.py
similarity index 99%
rename from src/wireviz/DataClasses.py
rename to src/wireviz/wv_dataclasses.py
index cce7148..6a79e26 100644
--- a/src/wireviz/DataClasses.py
+++ b/src/wireviz/wv_dataclasses.py
@@ -14,7 +14,7 @@ from wireviz.wv_colors import (
     SingleColor,
     get_color_by_colorcode_index,
 )
-from wireviz.wv_helper import aspect_ratio, awg_equiv, mm2_equiv, remove_links
+from wireviz.wv_utils import aspect_ratio, awg_equiv, mm2_equiv, remove_links
 
 # Each type alias have their legal values described in comments
 # - validation might be implemented in the future
diff --git a/src/wireviz/wv_gv_html.py b/src/wireviz/wv_graphviz.py
similarity index 99%
rename from src/wireviz/wv_gv_html.py
rename to src/wireviz/wv_graphviz.py
index 160e412..93ff237 100644
--- a/src/wireviz/wv_gv_html.py
+++ b/src/wireviz/wv_graphviz.py
@@ -5,7 +5,8 @@ from itertools import zip_longest
 from typing import Any, List, Union
 
 from wireviz import APP_NAME, APP_URL, __version__
-from wireviz.DataClasses import (
+from wireviz.wv_bom import partnumbers_to_list
+from wireviz.wv_dataclasses import (
     ArrowDirection,
     ArrowWeight,
     Cable,
@@ -18,9 +19,8 @@ from wireviz.DataClasses import (
     ShieldClass,
     WireClass,
 )
-from wireviz.wv_bom import partnumbers_to_list
-from wireviz.wv_helper import html_line_breaks, remove_links
-from wireviz.wv_table_util import *  # TODO: explicitly import each needed tag later
+from wireviz.wv_html import Img, Table, Td, Tr
+from wireviz.wv_utils import html_line_breaks, remove_links
 
 
 def gv_node_component(component: Component) -> Table:
diff --git a/src/wireviz/Harness.py b/src/wireviz/wv_harness.py
similarity index 98%
rename from src/wireviz/Harness.py
rename to src/wireviz/wv_harness.py
index 1d722e7..7c4143e 100644
--- a/src/wireviz/Harness.py
+++ b/src/wireviz/wv_harness.py
@@ -8,7 +8,7 @@ from typing import List
 from graphviz import Graph
 
 import wireviz.wv_colors
-from wireviz.DataClasses import (
+from wireviz.wv_dataclasses import (
     AUTOGENERATED_PREFIX,
     AdditionalComponent,
     Arrow,
@@ -23,8 +23,7 @@ from wireviz.DataClasses import (
     Side,
     Tweak,
 )
-from wireviz.svgembed import embed_svg_images_file
-from wireviz.wv_gv_html import (
+from wireviz.wv_graphviz import (
     apply_dot_tweaks,
     calculate_node_bgcolor,
     gv_connector_loops,
@@ -34,8 +33,8 @@ from wireviz.wv_gv_html import (
     parse_arrow_str,
     set_dot_basics,
 )
-from wireviz.wv_helper import open_file_write, tuplelist2tsv
-from wireviz.wv_html import generate_html_output
+from wireviz.wv_output import embed_svg_images_file, generate_html_output
+from wireviz.wv_utils import open_file_write, tuplelist2tsv
 
 
 @dataclass
diff --git a/src/wireviz/wv_html.py b/src/wireviz/wv_html.py
index 16a5d58..1c4b750 100644
--- a/src/wireviz/wv_html.py
+++ b/src/wireviz/wv_html.py
@@ -1,120 +1,125 @@
 # -*- coding: utf-8 -*-
 
-import re
-from pathlib import Path
-from typing import Dict, List, Union
+from collections.abc import Iterable
+from dataclasses import dataclass, field
+from typing import Dict
 
-from wireviz import APP_NAME, APP_URL, __version__, wv_colors
-from wireviz.DataClasses import Metadata, Options
-from wireviz.wv_helper import (
-    flatten2d,
-    html_line_breaks,
-    open_file_read,
-    open_file_write,
-    smart_file_resolve,
-)
+indent_count = 1
 
 
-def generate_html_output(
-    filename: Union[str, Path],
-    bom_list: List[List[str]],
-    metadata: Metadata,
-    options: Options,
-):
+class Attribs(Dict):
+    def __repr__(self):
+        if len(self) == 0:
+            return ""
 
-    # 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"
+        html = []
+        for k, v in self.items():
+            if v is not None:
+                html.append(f' {k}="{v}"')
+            # else:
+            #     html.append(f" {k}")
+        return "".join(html)
 
-    html = open_file_read(templatefile).read()
 
-    # embed SVG diagram
-    with open_file_read(f"{filename}.tmp.svg") as file:
-        svgdata = re.sub(
-            "^<[?]xml [^?>]*[?]>[^<]*]*>",
-            "",
-            file.read(),
-            1,
-        )
+@dataclass
+class Tag:
+    contents = None
+    attribs: Attribs = field(default_factory=Attribs)
+    flat: bool = None
+    delete_if_empty: bool = False
 
-    # generate BOM table
-    bom = flatten2d(bom_list)
+    def __init__(self, contents, flat=None, delete_if_empty=False, **kwargs):
+        self.contents = contents
+        self.flat = flat
+        self.delete_if_empty = delete_if_empty
+        self.attribs = Attribs({**kwargs})
 
-    # generate BOM header (may be at the top or bottom of the table)
-    bom_header_html = "  \n"
-    for item in bom[0]:
-        th_class = f"bom_col_{item.lower()}"
-        bom_header_html = f'{bom_header_html}    {item}\n'
-    bom_header_html = f"{bom_header_html}  \n"
+    def update_attribs(self, **kwargs):
+        for k, v in kwargs.items():
+            self.attribs[k] = v
 
-    # generate BOM contents
-    bom_contents = []
-    for row in bom[1:]:
-        row_html = "  \n"
-        for i, item in enumerate(row):
-            td_class = f"bom_col_{bom[0][i].lower()}"
-            row_html = f'{row_html}    {item}\n'
-        row_html = f"{row_html}  \n"
-        bom_contents.append(row_html)
+    @property
+    def tagname(self):
+        return type(self).__name__.lower()
 
-    bom_html = (
-        '\n' + bom_header_html + "".join(bom_contents) + "
\n" - ) - bom_html_reversed = ( - '\n' - + "".join(list(reversed(bom_contents))) - + bom_header_html - + "
\n" - ) + @property + def auto_flat(self): + if self.flat is not None: # user specified + return self.flat + if not _is_iterable_not_str(self.contents): # catch str, int, float, ... + if not isinstance(self.contents, Tag): # avoid recursion + return not "\n" in str(self.contents) # flatten if single line - # prepare simple replacements - replacements = { - "": f"{APP_NAME} {__version__} - {APP_URL}", - "": options.fontname, - "": options.bgcolor.html, - "": svgdata, - "": bom_html, - "": bom_html_reversed, - "": "1", # TODO: handle multi-page documents - "": "1", # TODO: handle multi-page documents - } + @property + def is_empty(self): + return self.get_contents(force_flat=True) == "" - # prepare metadata replacements - if metadata: - for item, contents in metadata.items(): - if isinstance(contents, (str, int, float)): - replacements[f""] = html_line_breaks(str(contents)) - elif isinstance(contents, Dict): # useful for authors, revisions - for index, (category, entry) in enumerate(contents.items()): - if isinstance(entry, Dict): - replacements[f""] = str(category) - for entry_key, entry_value in entry.items(): - replacements[ - f"" - ] = html_line_breaks(str(entry_value)) + def indent_lines(self, lines, force_flat=False): + if self.auto_flat or force_flat: + return lines + else: + indenter = " " * indent_count + return "\n".join(f"{indenter}{line}" for line in lines.split("\n")) - replacements['"sheetsize_default"'] = '"{}"'.format( - metadata.get("template", {}).get("sheetsize", "") - ) - # include quotes so no replacement happens within