Add optional tweaking of the .gv output (#215)

Co-authored-by: Daniel Rojas <github@danielrojas.net>
This commit is contained in:
kvid 2021-09-14 19:20:51 +02:00 committed by GitHub
parent 92354e6852
commit db05514469
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 3 deletions

View File

@ -35,6 +35,8 @@ additional_bom_items: # custom items to add to BOM
- <bom-item> # BOM item (see below) - <bom-item> # BOM item (see below)
... ...
tweak: # optional tweaking of .gv output
...
``` ```
## Metadata entries ## Metadata entries
@ -327,6 +329,31 @@ Alternatively items can be added to just the BOM by putting them in the section
manufacturer: <str> # manufacturer name manufacturer: <str> # 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.
<str>: # leading string of .gv entry
<str> : <str/null> # 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: <str/list> # string or list of strings to append to the .gv output
```
## Colors ## Colors
Colors are defined via uppercase, two character strings. Colors are defined via uppercase, two character strings.
@ -403,6 +430,7 @@ The following attributes accept multiline strings:
- `manufacturer` - `manufacturer`
- `mpn` - `mpn`
- `image.caption` - `image.caption`
- `tweak.append`
### Method 1 ### Method 1

View File

@ -59,6 +59,12 @@ class Options:
self.bgcolor_bundle = self.bgcolor_cable 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 @dataclass
class Image: class Image:
gv_dir: InitVar[Path] # Directory of .gv file injected as context during parsing gv_dir: InitVar[Path] # Directory of .gv file injected as context during parsing

View File

@ -3,14 +3,14 @@
from graphviz import Graph from graphviz import Graph
from collections import Counter from collections import Counter
from typing import List, Union from typing import Any, List, Union
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from itertools import zip_longest from itertools import zip_longest
import re import re
from wireviz import wv_colors, __version__, APP_NAME, APP_URL 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_colors import get_color_hex, translate_color
from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \ from wireviz.wv_gv_html import nested_html_table, html_colorbar, html_image, \
html_caption, remove_links, html_line_breaks html_caption, remove_links, html_line_breaks
@ -25,6 +25,7 @@ from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
class Harness: class Harness:
metadata: Metadata metadata: Metadata
options: Options options: Options
tweak: Tweak
def __post_init__(self): def __post_init__(self):
self.connectors = {} self.connectors = {}
@ -344,6 +345,57 @@ class Harness:
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', dot.node(cable.name, label=f'<\n{html}\n>', shape='box',
style=style, fillcolor=translate_color(bgcolor, "HEX")) 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 return dot
@property @property

View File

@ -13,7 +13,7 @@ if __name__ == '__main__':
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from wireviz import __version__ from wireviz import __version__
from wireviz.DataClasses import Metadata, Options from wireviz.DataClasses import Metadata, Options, Tweak
from wireviz.Harness import Harness from wireviz.Harness import Harness
from wireviz.wv_helper import expand, open_file_read 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( harness = Harness(
metadata = Metadata(**yaml_data.get('metadata', {})), metadata = Metadata(**yaml_data.get('metadata', {})),
options = Options(**yaml_data.get('options', {})), options = Options(**yaml_data.get('options', {})),
tweak = Tweak(**yaml_data.get('tweak', {})),
) )
if 'title' not in harness.metadata: if 'title' not in harness.metadata:
harness.metadata['title'] = Path(file_out).stem harness.metadata['title'] = Path(file_out).stem