Compare commits

..

No commits in common. "67a9cf9bacf5101b313e5dc6046aa299f087b1f1" and "cfc25dfabf598df19dcd761bc7352fa49b1b1c38" have entirely different histories.

6 changed files with 18 additions and 689 deletions

View File

@ -1,114 +0,0 @@
# Message 001
| Field | Value |
|-------|-------|
| From | notebook-api-agent |
| To | web-ui-agent |
| Date | 2026-02-13T00:00:00Z |
| Re | Notebook-Ready API additions to WireViz core |
---
Three new capabilities have been added to the WireViz core library on `testing/web-ui-features` to support interactive/notebook-style harness building. All changes are backward-compatible.
## 1. Graph Cache Invalidation (automatic)
The `Harness._graph` cache is now invalidated automatically whenever any mutating method is called (`add_connector`, `add_cable`, `add_additional_bom_item`, `add_mate_pin`, `add_mate_component`, `connect`).
**No action needed on your side.** After any mutation, accessing `harness.svg` or `harness.png` will regenerate the diagram. Previously, calling `.svg` after adding new components would return stale output.
```python
h.add_connector("X1", pins=[1, 2])
svg1 = h.svg # renders X1
h.add_connector("X2", pins=[1, 2])
svg2 = h.svg # automatically re-renders, now includes X2
```
## 2. Structured BOM Export via `bom_list_dicts()`
New function in `wireviz.wv_bom`:
```python
from wireviz.wv_bom import bom_list_dicts
dicts = bom_list_dicts(harness.bom)
# Returns: [{"#": 1, "Qty": 2, "Description": "...", ...}, ...]
```
- Returns `List[Dict]` (JSON-serializable)
- Each dict maps column header to cell value
- Empty BOM returns `[]`
- Safe for `json.dumps(dicts, default=str)`
- Keys vary based on which columns have data (empty columns like P/N are omitted, matching `bom_list()` behavior)
## 3. YAML Fragment Merging via `parse(harness=...)`
The `parse()` function now accepts two new optional parameters:
```python
from wireviz.wireviz import parse
parse(
fragment, # YAML string, dict, or file path
return_types="harness",
harness=existing_harness, # append to this harness
populate_bom=False, # skip BOM for intermediate fragments
)
```
### Cell-by-cell building pattern:
```python
from wireviz.wv_harness import Harness
from wireviz.wv_dataclasses import Metadata, Options, Tweak
from wireviz.wireviz import parse
from wireviz.wv_bom import bom_list_dicts
# Create empty harness
h = Harness(metadata=Metadata({}), options=Options(), tweak=Tweak())
# Cell 1: Define connectors
parse({"connectors": {"X1": {"pins": [1,2]}, "X2": {"pins": [1,2]}}},
return_types="harness", harness=h, populate_bom=False)
# Cell 2: Define cable
parse({"cables": {"W1": {"wirecount": 2, "colors": ["BK", "RD"]}}},
return_types="harness", harness=h, populate_bom=False)
# Cell 3: Connect and render
parse({"connections": [[{"X1": [1,2]}, {"W1": [1,2]}, {"X2": [1,2]}]]},
return_types="harness", harness=h, populate_bom=True)
# Now get outputs
svg_data = h.svg # rendered diagram
bom_data = bom_list_dicts(h.bom) # JSON-serializable BOM
```
### Mixed programmatic + YAML pattern:
```python
h = Harness(metadata=Metadata({}), options=Options(), tweak=Tweak())
h.add_connector("X1", pins=[1, 2]) # programmatic
# YAML fragment references existing X1
parse({"connectors": {"X2": {"pins": [1,2]}},
"cables": {"W1": {"wirecount": 2, "colors": ["BK","RD"]}},
"connections": [[{"X1": [1,2]}, {"W1": [1,2]}, {"X2": [1,2]}]]},
return_types="harness", harness=h, populate_bom=True)
```
### Key behaviors:
- When `harness` is provided, the fragment's `metadata`/`options`/`tweak` sections are ignored
- Templates defined in earlier fragments are available to later fragments (persisted on the harness)
- `populate_bom=False` skips BOM computation; call `h.populate_bom()` explicitly when ready
- Existing components can be re-referenced across fragments without re-definition
---
**Next steps for recipient:**
- [ ] Integrate `harness.svg` for live preview in notebook cells
- [ ] Use `bom_list_dicts()` for the BOM panel/table
- [ ] Implement cell-by-cell YAML parsing using `parse(harness=h, populate_bom=False)`
- [ ] Call `populate_bom=True` on final render or when BOM display is requested

View File

@ -40,8 +40,6 @@ def parse(
output_dir: Union[str, Path] = None,
output_name: Union[None, str] = None,
image_paths: Union[Path, str, List] = [],
harness: "Harness" = None,
populate_bom: bool = True,
) -> Any:
"""
This function takes an input, parses it as a WireViz Harness file,
@ -84,15 +82,6 @@ def parse(
Paths to use when resolving any image paths included in the data.
Note: If inp is a path to a YAML file,
its parent directory will automatically be included in the list.
harness (Harness, optional):
An existing Harness object to append components/connections to.
When provided, metadata/options/tweak from inp are ignored and
the existing harness is reused. Enables cell-by-cell building.
populate_bom (bool, optional):
Whether to call harness.populate_bom() after parsing.
Defaults to True. Set to False for intermediate fragments
when building incrementally, then call harness.populate_bom()
explicitly or pass True on the final fragment.
Returns:
Depending on the return_types parameter, may return:
@ -134,30 +123,25 @@ def parse(
# define variables =========================================================
# containers for parsed component data and connection sets
# when reusing a harness, start from its persistent templates
template_connectors = dict(harness._template_connectors) if harness is not None else {}
template_cables = dict(harness._template_cables) if harness is not None else {}
template_connectors = {}
template_cables = {}
connection_sets = []
# actual harness — create new or reuse existing
if harness is None:
harness = Harness(
metadata=Metadata(**yaml_data.get("metadata", {})),
options=Options(**yaml_data.get("options", {})),
tweak=Tweak(**yaml_data.get("tweak", {})),
)
# When title is not given, either deduce it from filename, or use default text.
if "title" not in harness.metadata:
harness.metadata["title"] = output_name or f"{APP_NAME} diagram and BOM"
# actual harness
harness = Harness(
metadata=Metadata(**yaml_data.get("metadata", {})),
options=Options(**yaml_data.get("options", {})),
tweak=Tweak(**yaml_data.get("tweak", {})),
)
# others
# store mapping of components to their respective template
designators_and_templates = {}
# pre-populate from existing components so re-referencing works
for des in harness.connectors:
designators_and_templates[des] = des
for des in harness.cables:
designators_and_templates[des] = des
# keep track of auto-generated designators to avoid duplicates
autogenerated_designators = {}
# When title is not given, either deduce it from filename, or use default text.
if "title" not in harness.metadata:
harness.metadata["title"] = output_name or f"{APP_NAME} diagram and BOM"
# add items
# parse YAML input file ====================================================
@ -192,10 +176,6 @@ def parse(
connection_sets = yaml_data["connections"]
# persist templates on harness for future fragment merges
harness._template_connectors.update(template_connectors)
harness._template_cables.update(template_cables)
# go through connection sets, generate and connect components ==============
template_separator_char = harness.options.template_separator
@ -425,8 +405,7 @@ def parse(
# harness population completed =============================================
if populate_bom:
harness.populate_bom()
harness.populate_bom()
if output_formats:
harness.output(filename=output_file, fmt=output_formats, view=False)

View File

@ -3,7 +3,7 @@
from collections import namedtuple
from dataclasses import dataclass
from enum import Enum, IntEnum
from typing import Dict, List, Optional, Union
from typing import List, Optional, Union
import tabulate as tabulate_module
@ -140,22 +140,6 @@ def bom_list(bom):
return rows
def bom_list_dicts(bom) -> List[Dict]:
"""Return BOM as a list of dicts (JSON-serializable).
Each dict maps column header to cell value, making it suitable
for JSON APIs and notebook contexts.
"""
rows = bom_list(bom)
if not rows:
return []
headers = rows[0]
return [
{h: cell for h, cell in zip(headers, row)}
for row in rows[1:]
]
def print_bom_table(bom):
print()
print(tabulate_module.tabulate(bom_list(bom), headers="firstrow"))

View File

@ -86,7 +86,6 @@ class Options:
bgcolor_bundle: SingleColor = None
color_output_mode: ColorOutputMode = ColorOutputMode.EN_UPPER
mini_bom_mode: bool = True
show_wire_loops: bool = True
template_separator: str = "."
output_dpi: Optional[float] = 96.0
_pad: int = 0

View File

@ -61,29 +61,19 @@ class Harness:
self.mates = []
self.bom = defaultdict(dict)
self.additional_bom_items = []
# persistent template storage for fragment merging
self._template_connectors = {}
self._template_cables = {}
def _invalidate_graph(self):
"""Clear cached graph so next access regenerates it."""
self._graph = None
def add_connector(self, designator: str, *args, **kwargs) -> None:
check_old(f"Connector '{designator}'", OLD_CONNECTOR_ATTR, kwargs)
conn = Connector(designator=designator, *args, **kwargs)
self.connectors[designator] = conn
self._invalidate_graph()
def add_cable(self, designator: str, *args, **kwargs) -> None:
cbl = Cable(designator=designator, *args, **kwargs)
self.cables[designator] = cbl
self._invalidate_graph()
def add_additional_bom_item(self, item: dict) -> None:
new_item = AdditionalBomItem(**item)
self.additional_bom_items.append(new_item)
self._invalidate_graph()
def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_str) -> None:
from_con = self.connectors[from_name]
@ -97,12 +87,10 @@ class Harness:
from_pin, Side.RIGHT, is_connection=False
)
self.connectors[to_name].activate_pin(to_pin, Side.LEFT, is_connection=False)
self._invalidate_graph()
def add_mate_component(self, from_name, to_name, arrow_str) -> None:
arrow = Arrow(direction=parse_arrow_str(arrow_str), weight=ArrowWeight.SINGLE)
self.mates.append(MateComponent(from_name, to_name, arrow))
self._invalidate_graph()
def populate_bom(self): # called once harness creation is complete
# helper lists
@ -305,7 +293,6 @@ class Harness:
self.connectors[from_name].activate_pin(from_pin, Side.RIGHT)
if to_name in self.connectors:
self.connectors[to_name].activate_pin(to_pin, Side.LEFT)
self._invalidate_graph()
def create_graph(self) -> Graph:
dot = Graph()
@ -387,10 +374,9 @@ class Harness:
if not (r1, r2) == (None, None):
dot.edge(r1, r2)
if self.options.show_wire_loops:
for color, we, ww in gv_edge_wire_inside(cable):
if not (we, ww) == (None, None):
dot.edge(we, ww, color=color, straight="straight", href='')
for color, we, ww in gv_edge_wire_inside(cable):
if not (we, ww) == (None, None):
dot.edge(we, ww, color=color, straight="straight", href='')
for mate in self.mates:
color, dir, code_from, code_to = gv_edge_mate(mate)

View File

@ -1,505 +0,0 @@
# -*- coding: utf-8 -*-
"""Tests for the notebook-ready API additions:
- Graph cache invalidation (Gap 1)
- Structured BOM export via bom_list_dicts (Gap 2)
- YAML fragment merging via parse(harness=...) (Gap 3)
- Full incremental notebook workflow simulation
"""
import json
import pytest
from wireviz.wv_bom import bom_list, bom_list_dicts
from wireviz.wv_dataclasses import Metadata, Options, Tweak
from wireviz.wv_harness import Harness
from wireviz.wireviz import parse
def _make_harness(**kwargs):
"""Create a minimal Harness for testing."""
defaults = dict(
metadata=Metadata({}),
options=Options(),
tweak=Tweak(),
)
defaults.update(kwargs)
return Harness(**defaults)
# ===========================================================================
# Gap 1: Graph Cache Invalidation
# ===========================================================================
class TestGraphCacheInvalidation:
"""After render, mutating the harness should produce updated output."""
def test_add_connector_after_render(self):
h = _make_harness()
h.add_connector("X1", pins=[1, 2])
h.add_cable("W1", wirecount=1, colors=["BK"])
h.connect("X1", 1, "W1", 1, None, None)
svg1 = h.svg
assert "X1" in svg1
# Add a second connector after rendering
h.add_connector("X2", pins=[1, 2])
h.connect(None, None, "W1", 1, "X2", 1)
svg2 = h.svg
assert "X2" in svg2
assert svg1 != svg2
def test_add_cable_after_render(self):
h = _make_harness()
h.add_connector("X1", pins=[1, 2])
h.add_cable("W1", wirecount=1, colors=["BK"])
h.connect("X1", 1, "W1", 1, None, None)
svg1 = h.svg
assert "W1" in svg1
# Add a second cable after rendering
h.add_cable("W2", wirecount=1, colors=["RD"])
h.connect("X1", 2, "W2", 1, None, None)
svg2 = h.svg
assert "W2" in svg2
assert svg1 != svg2
def test_connect_after_render(self):
h = _make_harness()
h.add_connector("X1", pins=[1, 2])
h.add_connector("X2", pins=[1, 2])
h.add_cable("W1", wirecount=2, colors=["BK", "RD"])
h.connect("X1", 1, "W1", 1, "X2", 1)
svg1 = h.svg
# Add another connection after rendering
h.connect("X1", 2, "W1", 2, "X2", 2)
svg2 = h.svg
assert svg1 != svg2
def test_add_mate_component_after_render(self):
h = _make_harness()
h.add_connector("X1", pins=[1, 2])
h.add_connector("X2", pins=[1, 2])
h.add_cable("W1", wirecount=1, colors=["BK"])
h.connect("X1", 1, "W1", 1, "X2", 1)
svg1 = h.svg
# Add mate after rendering
h.add_connector("X3", pins=[1])
h.add_connector("X4", pins=[1])
h.add_mate_component("X3", "X4", "-->")
svg2 = h.svg
assert "X3" in svg2
assert "X4" in svg2
assert svg1 != svg2
def test_graph_property_caches_correctly(self):
"""graph property should return same object on repeated access."""
h = _make_harness()
h.add_connector("X1", pins=[1])
h.add_cable("W1", wirecount=1, colors=["BK"])
h.connect("X1", 1, "W1", 1, None, None)
g1 = h.graph
g2 = h.graph
assert g1 is g2 # same cached object
def test_invalidation_clears_cache(self):
"""After mutation, graph property should return a new object."""
h = _make_harness()
h.add_connector("X1", pins=[1])
h.add_cable("W1", wirecount=1, colors=["BK"])
h.connect("X1", 1, "W1", 1, None, None)
g1 = h.graph
h.add_connector("X2", pins=[1])
g2 = h.graph
assert g1 is not g2
# ===========================================================================
# Gap 2: Structured BOM Export
# ===========================================================================
class TestBomListDicts:
"""bom_list_dicts() should return JSON-serializable list of dicts."""
def _make_populated_harness(self):
h = _make_harness()
h.add_connector("X1", pins=[1, 2, 3], type="Molex KK 3-pin")
h.add_connector("X2", pins=[1, 2, 3], type="Molex KK 3-pin")
h.add_cable(
"W1", wirecount=3,
colors=["BK", "RD", "GN"],
length=0.5,
)
h.connect("X1", 1, "W1", 1, "X2", 1)
h.connect("X1", 2, "W1", 2, "X2", 2)
h.connect("X1", 3, "W1", 3, "X2", 3)
h.populate_bom()
return h
def test_returns_list_of_dicts(self):
h = self._make_populated_harness()
result = bom_list_dicts(h.bom)
assert isinstance(result, list)
assert all(isinstance(row, dict) for row in result)
def test_keys_match_headers(self):
h = self._make_populated_harness()
rows = bom_list(h.bom)
headers = rows[0]
dicts = bom_list_dicts(h.bom)
for d in dicts:
assert list(d.keys()) == headers
def test_values_match_rows(self):
h = self._make_populated_harness()
rows = bom_list(h.bom)
dicts = bom_list_dicts(h.bom)
for i, d in enumerate(dicts):
row = rows[i + 1] # skip header row
assert list(d.values()) == row
def test_json_serializable(self):
h = self._make_populated_harness()
dicts = bom_list_dicts(h.bom)
# Should not raise
serialized = json.dumps(dicts, default=str)
assert isinstance(serialized, str)
# Round-trip
deserialized = json.loads(serialized)
assert len(deserialized) == len(dicts)
def test_empty_bom_returns_empty_list(self):
from collections import defaultdict
empty_bom = defaultdict(dict)
result = bom_list_dicts(empty_bom)
assert result == []
def test_has_id_field(self):
h = self._make_populated_harness()
dicts = bom_list_dicts(h.bom)
assert all("#" in d for d in dicts)
ids = [d["#"] for d in dicts]
assert ids == sorted(ids) # IDs should be in order
def test_has_description(self):
h = self._make_populated_harness()
dicts = bom_list_dicts(h.bom)
assert all("Description" in d for d in dicts)
# ===========================================================================
# Gap 3: YAML Fragment Merging
# ===========================================================================
class TestYamlFragmentMerging:
"""parse() with harness= parameter should append to existing harness."""
def test_parse_with_existing_harness_adds_connector(self):
h = _make_harness()
# First fragment: define connector X1
fragment1 = {
"connectors": {"X1": {"pins": [1, 2, 3]}},
"cables": {"W1": {"wirecount": 1, "colors": ["BK"]}},
"connections": [
[
{"X1": [1]},
{"W1": [1]},
],
],
}
parse(fragment1, return_types="harness", harness=h, populate_bom=False)
assert "X1" in h.connectors
assert "W1" in h.cables
def test_parse_fragment_adds_to_existing_components(self):
h = _make_harness()
# Fragment 1: define X1 and W1
fragment1 = {
"connectors": {"X1": {"pins": [1, 2]}},
"cables": {"W1": {"wirecount": 1, "colors": ["BK"]}},
"connections": [
[{"X1": [1]}, {"W1": [1]}],
],
}
parse(fragment1, return_types="harness", harness=h, populate_bom=False)
# Fragment 2: define X2 and connect to W1
fragment2 = {
"connectors": {"X2": {"pins": [1, 2]}},
"connections": [
[{"W1": [1]}, {"X2": [1]}],
],
}
parse(fragment2, return_types="harness", harness=h, populate_bom=False)
assert "X2" in h.connectors
# W1 should have a connection to X2
assert len(h.cables["W1"]._connections) == 2
def test_populate_bom_false_skips_bom(self):
h = _make_harness()
fragment = {
"connectors": {"X1": {"pins": [1]}},
"cables": {"W1": {"wirecount": 1, "colors": ["BK"]}},
"connections": [
[{"X1": [1]}, {"W1": [1]}],
],
}
parse(fragment, return_types="harness", harness=h, populate_bom=False)
# BOM should not be populated
assert len(h.bom) == 0
def test_populate_bom_true_populates_bom(self):
h = _make_harness()
fragment = {
"connectors": {"X1": {"pins": [1]}},
"cables": {"W1": {"wirecount": 1, "colors": ["BK"]}},
"connections": [
[{"X1": [1]}, {"W1": [1]}],
],
}
parse(fragment, return_types="harness", harness=h, populate_bom=True)
assert len(h.bom) > 0
def test_reuse_existing_connector_across_fragments(self):
"""Connector defined in fragment 1 can be referenced in fragment 2."""
h = _make_harness()
fragment1 = {
"connectors": {"X1": {"pins": [1, 2]}},
"cables": {"W1": {"wirecount": 2, "colors": ["BK", "RD"]}},
"connections": [
[{"X1": [1]}, {"W1": [1]}],
],
}
parse(fragment1, return_types="harness", harness=h, populate_bom=False)
# Fragment 2: reference existing X1 (no re-definition needed)
fragment2 = {
"connections": [
[{"X1": [2]}, {"W1": [2]}],
],
}
parse(fragment2, return_types="harness", harness=h, populate_bom=False)
# Both connections should exist on W1
assert len(h.cables["W1"]._connections) == 2
def test_parse_without_harness_param_backward_compatible(self):
"""parse() without harness= should work identically to before."""
yaml_input = {
"connectors": {"X1": {"pins": [1, 2]}, "X2": {"pins": [1, 2]}},
"cables": {"W1": {"wirecount": 2, "colors": ["BK", "RD"]}},
"connections": [
[
{"X1": [1, 2]},
{"W1": [1, 2]},
{"X2": [1, 2]},
],
],
}
result = parse(yaml_input, return_types="harness")
assert isinstance(result, Harness)
assert "X1" in result.connectors
assert "X2" in result.connectors
assert "W1" in result.cables
assert len(result.bom) > 0 # BOM was populated
def test_metadata_ignored_when_harness_provided(self):
"""When harness is provided, fragment's metadata/options are ignored."""
h = _make_harness(metadata=Metadata({"title": "Original Title"}))
fragment = {
"metadata": {"title": "Should Be Ignored"},
"connectors": {"X1": {"pins": [1]}},
"cables": {"W1": {"wirecount": 1, "colors": ["BK"]}},
"connections": [
[{"X1": [1]}, {"W1": [1]}],
],
}
parse(fragment, return_types="harness", harness=h, populate_bom=False)
# Original metadata should be preserved
assert h.metadata["title"] == "Original Title"
# ===========================================================================
# Full Incremental Workflow Simulation
# ===========================================================================
class TestIncrementalWorkflow:
"""Simulate a notebook building a harness cell-by-cell with live preview."""
def test_cell_by_cell_with_render(self):
"""
Cell 1: Create harness, add connector X1
Cell 2: Render (should show X1)
Cell 3: Add cable W1 and connector X2, connect
Cell 4: Render (should show X1, W1, X2 with connections)
"""
# Cell 1: Create harness and add first connector
h = _make_harness()
h.add_connector("X1", pins=[1, 2, 3])
h.add_cable("W1", wirecount=1, colors=["BK"])
h.connect("X1", 1, "W1", 1, None, None)
# Cell 2: Render
svg1 = h.svg
assert "X1" in svg1
assert "W1" in svg1
# Cell 3: Add more components
h.add_connector("X2", pins=[1, 2, 3])
h.connect(None, None, "W1", 1, "X2", 1)
# Cell 4: Render again — should include X2
svg2 = h.svg
assert "X1" in svg2
assert "X2" in svg2
assert "W1" in svg2
assert svg1 != svg2 # output changed
def test_yaml_fragments_with_intermediate_render(self):
"""Build harness from YAML fragments with SVG preview between each."""
h = _make_harness()
# Fragment 1: connectors
frag1 = {
"connectors": {
"X1": {"pins": [1, 2], "type": "Header 2-pin"},
"X2": {"pins": [1, 2], "type": "Header 2-pin"},
},
}
parse(frag1, return_types="harness", harness=h, populate_bom=False)
# Can't render yet — no cables or connections to draw edges
# Fragment 2: cable
frag2 = {
"cables": {
"W1": {"wirecount": 2, "colors": ["BK", "RD"]},
},
}
parse(frag2, return_types="harness", harness=h, populate_bom=False)
# Fragment 3: connections
frag3 = {
"connections": [
[
{"X1": [1, 2]},
{"W1": [1, 2]},
{"X2": [1, 2]},
],
],
}
parse(frag3, return_types="harness", harness=h, populate_bom=False)
# Now render — should have full harness
svg = h.svg
assert "X1" in svg
assert "X2" in svg
assert "W1" in svg
# Populate BOM explicitly
h.populate_bom()
dicts = bom_list_dicts(h.bom)
assert len(dicts) > 0
serialized = json.dumps(dicts, default=str)
assert isinstance(serialized, str)
def test_mixed_api_and_yaml_fragments(self):
"""Mix programmatic API calls with YAML fragment parsing."""
h = _make_harness()
# Programmatic: add connector
h.add_connector("X1", pins=[1, 2])
# YAML fragment: add cable and second connector
frag = {
"connectors": {"X2": {"pins": [1, 2]}},
"cables": {"W1": {"wirecount": 2, "colors": ["BK", "RD"]}},
"connections": [
[
{"X1": [1, 2]},
{"W1": [1, 2]},
{"X2": [1, 2]},
],
],
}
parse(frag, return_types="harness", harness=h, populate_bom=True)
assert "X1" in h.connectors
assert "X2" in h.connectors
assert "W1" in h.cables
assert len(h.bom) > 0
svg = h.svg
assert "X1" in svg
assert "X2" in svg
def test_bom_consistency_across_fragments(self):
"""BOM should be consistent whether built in one shot or incrementally."""
# One-shot harness
one_shot_yaml = {
"connectors": {
"X1": {"pins": [1, 2], "type": "Header 2-pin"},
"X2": {"pins": [1, 2], "type": "Header 2-pin"},
},
"cables": {
"W1": {"wirecount": 2, "colors": ["BK", "RD"]},
},
"connections": [
[
{"X1": [1, 2]},
{"W1": [1, 2]},
{"X2": [1, 2]},
],
],
}
h_oneshot = parse(one_shot_yaml, return_types="harness")
bom_oneshot = bom_list_dicts(h_oneshot.bom)
# Incremental harness
h_inc = _make_harness()
frag1 = {
"connectors": {
"X1": {"pins": [1, 2], "type": "Header 2-pin"},
"X2": {"pins": [1, 2], "type": "Header 2-pin"},
},
"cables": {
"W1": {"wirecount": 2, "colors": ["BK", "RD"]},
},
"connections": [
[
{"X1": [1, 2]},
{"W1": [1, 2]},
{"X2": [1, 2]},
],
],
}
parse(frag1, return_types="harness", harness=h_inc, populate_bom=True)
bom_inc = bom_list_dicts(h_inc.bom)
# BOM should match
assert len(bom_oneshot) == len(bom_inc)
for d1, d2 in zip(bom_oneshot, bom_inc):
assert d1["Description"] == d2["Description"]
assert d1["Qty"] == d2["Qty"]