Skip to content

Commit e75ff5c

Browse files
Add per-connector / per-cable tweak with name placeholder (port of upstream PR wireviz#357)
Connectors and cables can now carry their own ``tweak:`` block with the same ``override`` / ``append`` shape as the global one. The harness folds per-node tweaks into the global tweak at instantiation, with an optional placeholder substring rewritten to the node's actual designator — making it practical to author a single tweak template and apply it to many components. Example: tweak: placeholder: "@@" connectors: X1: pinlabels: [A, B] tweak: append: - "@@_extra [color=red, style=dashed];" renders X1's per-node tweak as ``X1_extra [color=red, style=dashed];`` in the .gv source. The same connector definition reused for X2 would produce ``X2_extra ...``. Placeholder semantics: * Per-node ``tweak.placeholder`` overrides the global ``tweak.placeholder``. * An empty string at the per-node level explicitly opts out of substitution for that node. * ``None`` (the default) falls back to the global placeholder. * When neither is set, no substitution happens — bare strings are appended/overridden as-written. Implementation: * DataClasses.py — ``Tweak`` gains ``placeholder: Optional[str] = None``. ``Connector`` and ``Cable`` gain ``tweak: Optional[Tweak] = None``, with ``__post_init__`` coercing a dict literal into a Tweak instance. * Harness.py — new ``Harness._extend_tweak(node)`` method, called from ``add_connector()`` and ``add_cable()``, performs the placeholder substitution and merges into ``self.tweak``. Raises ``ValueError`` if two nodes contribute conflicting overrides for the same key. * docs/syntax.md — documents per-connector / per-cable tweak fields and the new placeholder semantics. Adapted from wireviz#357 (originally by @kvid, targeting upstream ``dev``). Renamed ``extend_tweak`` to ``_extend_tweak`` to mark it private; otherwise faithful to the original logic. ``make_list`` is already in master's wv_bom so no helper backport needed. Verified: * Smoke test with placeholder ``@@`` and per-connector + per-cable ``append:`` blocks produces ``X1_extra``, ``W1_label``, ``cable W1`` in the rendered .gv (the @@'s are substituted). * build_examples.py: deterministic outputs (.gv, .bom.tsv) byte- identical to baseline (no existing example uses the new syntax, so no new substitutions fire). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3d925b2 commit e75ff5c

3 files changed

Lines changed: 59 additions & 0 deletions

File tree

docs/syntax.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ tweak: # optional tweaking of .gv output
8585
# loops
8686
loops: <List> # every list item is itself a list of exactly two pins
8787
# on the connector that are to be shorted
88+
89+
# optional tweaking of .gv output executed for each instance of this connector
90+
tweak: # see tweak section below
8891
```
8992
9093
## Cable attributes
@@ -148,6 +151,8 @@ tweak: # optional tweaking of .gv output
148151
show_wirecount: <bool> # defaults to true
149152
show_wirenumbers: <bool> # defaults to true for cables; false for bundles
150153

154+
# optional tweaking of .gv output executed for each instance of this cable
155+
tweak: # see tweak section below
151156
```
152157
153158
## Connection sets
@@ -457,6 +462,12 @@ Alternatively items can be added to just the BOM by putting them in the section
457462
# This feature is experimental and might change
458463
# or be removed in future versions.
459464
465+
placeholder: <str> # Substring to be replaced with the node name in
466+
# any per-connector / per-cable tweak overrides and append entries.
467+
# An empty string disables placeholder substitution for that node.
468+
# When omitted at the per-node level, the global placeholder
469+
# (in the top-level tweak: section) is used as the fallback.
470+
460471
override: # dict of .gv entries to override
461472
# Each entry is identified by its leading string
462473
# in lines beginning with a TAB character.

src/wireviz/DataClasses.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def __post_init__(self):
7979

8080
@dataclass
8181
class Tweak:
82+
placeholder: Optional[PlainText] = None
8283
override: Optional[Dict[Designator, Dict[str, Optional[str]]]] = None
8384
append: Union[str, List[str], None] = None
8485

@@ -170,10 +171,13 @@ class Connector:
170171
loops: List[List[Pin]] = field(default_factory=list)
171172
ignore_in_bom: bool = False
172173
additional_components: List[AdditionalComponent] = field(default_factory=list)
174+
tweak: Optional[Tweak] = None
173175

174176
def __post_init__(self) -> None:
175177
if isinstance(self.image, dict):
176178
self.image = Image(**self.image)
179+
if isinstance(self.tweak, dict):
180+
self.tweak = Tweak(**self.tweak)
177181

178182
self.ports_left = False
179183
self.ports_right = False
@@ -335,10 +339,13 @@ class Cable:
335339
show_wirenumbers: Optional[bool] = None
336340
ignore_in_bom: bool = False
337341
additional_components: List[AdditionalComponent] = field(default_factory=list)
342+
tweak: Optional[Tweak] = None
338343

339344
def __post_init__(self) -> None:
340345
if isinstance(self.image, dict):
341346
self.image = Image(**self.image)
347+
if isinstance(self.tweak, dict):
348+
self.tweak = Tweak(**self.tweak)
342349

343350
if isinstance(self.gauge, str): # gauge and unit specified
344351
try:

src/wireviz/Harness.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
component_table_entry,
3131
generate_bom,
3232
get_additional_component_table,
33+
make_list,
3334
pn_info_string,
3435
)
3536
from wireviz.wv_colors import get_color_hex, translate_color
@@ -84,9 +85,49 @@ def __post_init__(self):
8485
def add_connector(self, name: str, *args, **kwargs) -> None:
8586
check_old(f"Connector '{name}'", OLD_CONNECTOR_ATTR, kwargs)
8687
self.connectors[name] = Connector(name, *args, **kwargs)
88+
self._extend_tweak(self.connectors[name])
8789

8890
def add_cable(self, name: str, *args, **kwargs) -> None:
8991
self.cables[name] = Cable(name, *args, **kwargs)
92+
self._extend_tweak(self.cables[name])
93+
94+
def _extend_tweak(self, node: Union[Connector, Cable]) -> None:
95+
"""Fold ``node.tweak`` into ``self.tweak`` after substituting the
96+
node's name for the placeholder string.
97+
98+
Per-connector / per-cable ``tweak:`` entries let users author a
99+
single template and have its ``override`` keys / ``append`` lines
100+
rewritten with the actual designator at instantiation time. This
101+
is the only place the placeholder substitution happens — the
102+
global tweak is applied unchanged at graph emission time.
103+
"""
104+
if not node.tweak:
105+
return
106+
ph = node.tweak.placeholder
107+
# An empty string is a legal value to opt out of the global
108+
# placeholder; only None falls back.
109+
if ph is None:
110+
ph = self.tweak.placeholder
111+
rph = (lambda s: s.replace(ph, node.name)) if ph else (lambda s: s)
112+
113+
n_override = node.tweak.override or {}
114+
s_override = self.tweak.override or {}
115+
for ident, n_dict in n_override.items():
116+
ident = rph(ident)
117+
s_dict = s_override.get(ident, {})
118+
for k, v in n_dict.items():
119+
k, v = rph(k), rph(v)
120+
if k in s_dict and v != s_dict[k]:
121+
raise ValueError(
122+
f"{node.name}.tweak.override.{ident}.{k} conflicts with another"
123+
)
124+
s_dict[k] = v
125+
s_override[ident] = s_dict or None
126+
self.tweak.override = s_override or None
127+
self.tweak.append = (
128+
make_list(self.tweak.append)
129+
+ [rph(v) for v in make_list(node.tweak.append)]
130+
) or None
90131

91132
def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None:
92133
self.mates.append(MatePin(from_name, from_pin, to_name, to_pin, arrow_type))

0 commit comments

Comments
 (0)