From db05514469d4fc24aae166c01af0002fc380ba1a Mon Sep 17 00:00:00 2001 From: kvid Date: Tue, 14 Sep 2021 19:20:51 +0200 Subject: [PATCH] Add optional tweaking of the .gv output (#215) Co-authored-by: Daniel Rojas --- docs/syntax.md | 28 +++++++++++++++++++ src/wireviz/DataClasses.py | 6 ++++ src/wireviz/Harness.py | 56 ++++++++++++++++++++++++++++++++++++-- src/wireviz/wireviz.py | 3 +- 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 4a7a52ac..fa433c3d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -35,6 +35,8 @@ additional_bom_items: # custom items to add to BOM - # BOM item (see below) ... +tweak: # optional tweaking of .gv output + ... ``` ## Metadata entries @@ -327,6 +329,31 @@ Alternatively items can be added to just the BOM by putting them in the section manufacturer: # manufacturer name ``` +## GraphViz tweaking (experimental) + +```yaml + # Optional tweaking of the .gv output. + # This feature is experimental and might change + # or be removed in future versions. + + override: # dict of .gv entries to override + # Each entry is identified by its leading string + # in lines beginning with a TAB character. + # The leading string might be in "quotes" in + # the .gv output. This leading string must be + # followed by attributes in [square brackets]. + # Entries with an attribute containing HTML are + # not supported. + : # leading string of .gv entry + : # attribute and its new value + # Any number of attributes can be overridden + # for each entry. Attributes not already existing + # in the entry will be appended to the entry. + # Use null as new value to delete an attribute. + + append: # string or list of strings to append to the .gv output +``` + ## Colors Colors are defined via uppercase, two character strings. @@ -403,6 +430,7 @@ The following attributes accept multiline strings: - `manufacturer` - `mpn` - `image.caption` +- `tweak.append` ### Method 1 diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index e80437b2..95325d18 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -59,6 +59,12 @@ def __post_init__(self): self.bgcolor_bundle = self.bgcolor_cable +@dataclass +class Tweak: + override: Optional[Dict[Designator, Dict[str, Optional[str]]]] = None + append: Union[str, List[str], None] = None + + @dataclass class Image: gv_dir: InitVar[Path] # Directory of .gv file injected as context during parsing diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 39180500..f4937f7f 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -3,14 +3,14 @@ from graphviz import Graph from collections import Counter -from typing import List, Union +from typing import Any, List, Union from dataclasses import dataclass from pathlib import Path from itertools import zip_longest import re from wireviz import wv_colors, __version__, APP_NAME, APP_URL -from wireviz.DataClasses import Metadata, Options, Connector, Cable +from wireviz.DataClasses import Metadata, Options, Tweak, Connector, Cable from wireviz.wv_colors import get_color_hex, translate_color from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \ html_caption, remove_links, html_line_breaks @@ -25,6 +25,7 @@ class Harness: metadata: Metadata options: Options + tweak: Tweak def __post_init__(self): self.connectors = {} @@ -344,6 +345,57 @@ def create_graph(self) -> Graph: dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style=style, fillcolor=translate_color(bgcolor, "HEX")) + def typecheck(name: str, value: Any, expect: type) -> None: + if not isinstance(value, expect): + raise Exception(f'Unexpected value type of {name}: Expected {expect}, got {type(value)}\n{value}') + + # TODO?: Differ between override attributes and HTML? + if self.tweak.override is not None: + typecheck('tweak.override', self.tweak.override, dict) + for k, d in self.tweak.override.items(): + typecheck(f'tweak.override.{k} key', k, str) + typecheck(f'tweak.override.{k} value', d, dict) + for a, v in d.items(): + typecheck(f'tweak.override.{k}.{a} key', a, str) + typecheck(f'tweak.override.{k}.{a} value', v, (str, type(None))) + + # Override generated attributes of selected entries matching tweak.override. + for i, entry in enumerate(dot.body): + if isinstance(entry, str): + # Find a possibly quoted keyword after leading TAB(s) and followed by [ ]. + match = re.match(r'^\t*(")?((?(1)[^"]|[^ "])+)(?(1)") \[.*\]$', entry, re.S) + keyword = match and match[2] + if keyword in self.tweak.override.keys(): + for attr, value in self.tweak.override[keyword].items(): + if value is None: + entry, n_subs = re.subn(f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', '', entry) + if n_subs < 1: + print(f'Harness.create_graph() warning: {attr} not found in {keyword}!') + elif n_subs > 1: + print(f'Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!') + continue + + if len(value) == 0 or ' ' in value: + value = value.replace('"', r'\"') + value = f'"{value}"' + entry, n_subs = re.subn(f'{attr}=("[^"]*"|[^] ]*)', f'{attr}={value}', entry) + if n_subs < 1: + # If attr not found, then append it + entry = re.sub(r'\]$', f' {attr}={value}]', entry) + elif n_subs > 1: + print(f'Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!') + + dot.body[i] = entry + + if self.tweak.append is not None: + if isinstance(self.tweak.append, list): + for i, element in enumerate(self.tweak.append, 1): + typecheck(f'tweak.append[{i}]', element, str) + dot.body.extend(self.tweak.append) + else: + typecheck('tweak.append', self.tweak.append, str) + dot.body.append(self.tweak.append) + return dot @property diff --git a/src/wireviz/wireviz.py b/src/wireviz/wireviz.py index 6ba77eec..95674945 100755 --- a/src/wireviz/wireviz.py +++ b/src/wireviz/wireviz.py @@ -13,7 +13,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from wireviz import __version__ -from wireviz.DataClasses import Metadata, Options +from wireviz.DataClasses import Metadata, Options, Tweak from wireviz.Harness import Harness from wireviz.wv_helper import expand, open_file_read @@ -38,6 +38,7 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st harness = Harness( metadata = Metadata(**yaml_data.get('metadata', {})), options = Options(**yaml_data.get('options', {})), + tweak = Tweak(**yaml_data.get('tweak', {})), ) if 'title' not in harness.metadata: harness.metadata['title'] = Path(file_out).stem