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 1 commit
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
27 changes: 27 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,30 @@ Alternatively items can be added to just the BOM by putting them in the section
manufacturer: <str> # manufacturer name
```

## Tweak entries
kvid marked this conversation as resolved.
Show resolved Hide resolved

```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 containing HTML in an attribute are
# not supported.
<str>: # leading string of .gv entry
<str> : <str> # attribute and its new value
# Any number of attributes can be overridden
# for each entry

append: # single or list of .gv entries to append
<str> # strings to append might have multiple lines
kvid marked this conversation as resolved.
Show resolved Hide resolved
```

## Colors

Colors are defined via uppercase, two character strings.
Expand Down Expand Up @@ -403,6 +429,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, 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
41 changes: 40 additions & 1 deletion src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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,44 @@ 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, var, type) -> None:
if not isinstance(var, type):
raise Exception(f'Unexpected value type of {name}: {var}')
kvid marked this conversation as resolved.
Show resolved Hide resolved

# 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)

# 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 len(value) == 0 or ' ' in value:
value = value.replace('"', r'\"')
value = f'"{value}"'
# TODO?: If value is None: delete attr, and if attr not found: append it?
Copy link
Collaborator

@formatc1702 formatc1702 Sep 8, 2021

Choose a reason for hiding this comment

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

both ideas make sense. I guess this would be a separate PR in the future?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that was the plan. The current implementation should be enough to solve the original issue and a lot of similar experimental tweaking, but I don't expect a huge number of users will need such tweaking in their harnesses. Maybe it'll mainly be a tool for developers to test variations of .gv output before suggesting a proper implementation of a new feature.

Copy link
Collaborator Author

@kvid kvid Sep 12, 2021

Choose a reason for hiding this comment

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

I just implemented these features and committed them to this PR as well.

entry = re.sub(f'{attr}=("[^"]*"|[^] ]*)', f'{attr}={value}', entry)
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(f'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