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

Add optional tweaking of the .gv output #215

Merged
merged 4 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ additional_bom_items: # custom items to add to BOM
- <bom-item> # BOM item (see below)
...

tweak: # optional tweaking of .gv output
...
```

## Metadata entries
Expand Down Expand Up @@ -327,6 +329,31 @@ Alternatively items can be added to just the BOM by putting them in the section
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 are defined via uppercase, two character strings.
Expand Down Expand Up @@ -403,6 +430,7 @@ The following attributes accept multiline strings:
- `manufacturer`
- `mpn`
- `image.caption`
- `tweak.append`

### Method 1

Expand Down
6 changes: 6 additions & 0 deletions src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 54 additions & 2 deletions src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +25,7 @@
class Harness:
metadata: Metadata
options: Options
tweak: Tweak

def __post_init__(self):
self.connectors = {}
Expand Down Expand Up @@ -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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason that the append section should be a list, and not a string?
Is there a benefit to having multiple list entries for different tweaks, as opposed to all of them concatenated into one string? I am not opposed to it, just genuinely curious.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I used a list of quoted strings to easily specify leading spaces where I wanted them. Leading spaces in multiline strings seems to be stripped by the YAML parser, but maybe you know a way to avoid that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you include the indentation indicator (source), any additional leading spaces are preserved. I quickly tested this and it works. Example:

tweak:
  append: |2  # two leading spaces will be interpreted as indentation; any more will be preserved
              // Tweaking two cable nodes to keep close to each other
              subgraph cluster1 {
                color=yellow // Same as background to hide the surrounding frame
                TPPinkSilver
                TPBlueSilver
              }

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
Expand Down
3 changes: 2 additions & 1 deletion src/wireviz/wireviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down