From 0de14c6b72ecfb99b4b218e7f0b25df620d9a475 Mon Sep 17 00:00:00 2001 From: aidanbrzezinski Date: Sun, 8 Mar 2026 15:45:01 -0400 Subject: [PATCH] updated .py script --- README.md | 2 +- update_component_index.py | 138 +++++++++++++++++++++----------------- 2 files changed, 79 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 92e9879..35524a5 100644 --- a/README.md +++ b/README.md @@ -385,7 +385,7 @@ AidanBrzezinski_KiCad_Library/ | MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN | |---|---|---|---|---|---| -| 74LS192 | Synchronous 4-bit Up/Down (2 clk) counter | - | 0_ic_logic | - | - | +| SN74HC193DR | Synchronous 4-bit Up/Down (2 clk) counter | - | 0_ic_logic | 0_package_SO:SOIC-16_3.9x9.9mm_P1.27mm | - | *1 components — auto-generated 2026-03-08* diff --git a/update_component_index.py b/update_component_index.py index c8e2c32..c14ef0b 100644 --- a/update_component_index.py +++ b/update_component_index.py @@ -9,33 +9,20 @@ Reads the following fields from each symbol: Symbol Library column = which .kicad_sym file the symbol lives in. Used to locate a symbol quickly in KiCad Symbol Editor. +Handles derived symbols (Derive from symbol) by merging parent fields +with child overrides so all fields resolve correctly. + Run locally: python3 .github/scripts/update_component_index.py Run in CI: triggered automatically on push to symbols/ """ import re -import os from pathlib import Path from datetime import date SYMBOLS_DIR = Path("symbols") README_PATH = Path("README.md") -# Maps symbol filename (without extension) to a human-readable category label -LIBRARY_LABELS = { - "0_ic_logic": "0_ic_logic", - "0_ic_mcu": "0_ic_mcu", - "0_ic_driver": "0_ic_driver", - "0_ic_power": "0_ic_power", - "0_ic_analog": "0_ic_analog", - "0_ic_rf": "0_ic_rf", - "0_ic_interface": "0_ic_interface", - "0_passive": "0_passive", - "0_connector": "0_connector", - "0_discrete": "0_discrete", -} - -# Order categories should appear in the table CATEGORY_ORDER = [ "0_ic_logic", "0_ic_driver", @@ -50,63 +37,96 @@ CATEGORY_ORDER = [ ] -def parse_field(symbol_block: str, field_name: str) -> str: - """Extract a named field value from a symbol block.""" - # KiCad sym format: (property "FieldName" "Value" ...) - pattern = rf'\(property\s+"{re.escape(field_name)}"\s+"([^"]*)"' - match = re.search(pattern, symbol_block) - return match.group(1).strip() if match else "" +def extract_all_fields(block: str) -> dict: + """Extract all property fields from a symbol block into a dict.""" + fields = {} + for m in re.finditer(r'\(property\s+"([^"]+)"\s+"([^"]*)"', block): + fields[m.group(1)] = m.group(2).strip() + return fields -def parse_symbols_from_file(filepath: Path) -> list[dict]: +def shorten_footprint(footprint: str) -> str: + """Trim long footprint names for table readability.""" + if footprint and ":" in footprint: + lib_part, fp_name = footprint.split(":", 1) + if len(fp_name) > 30: + fp_name = fp_name[:27] + "..." + return f"{lib_part}:{fp_name}" + return footprint + + +def parse_symbols_from_file(filepath: Path) -> list: """ Parse all symbols from a .kicad_sym file. - Returns a list of dicts with keys: name, mpn, description, footprint, digikey_pn, manufacturer + + Handles both normal symbols and derived symbols (Derive from symbol). + Derived symbols only store fields that differ from their parent — the + parser merges parent fields first then overlays child overrides so all + fields resolve correctly. """ content = filepath.read_text(encoding="utf-8") - symbols = [] - # Find all top-level symbol blocks - # Each starts with: (symbol "NAME" - symbol_pattern = re.compile(r'\(symbol\s+"([^"]+)"(?!\s+\(extends)', re.MULTILINE) + # --- Pass 1: collect ALL raw symbol blocks indexed by name --- + symbol_pattern = re.compile(r'\(symbol\s+"([^"]+)"', re.MULTILINE) + all_matches = list(symbol_pattern.finditer(content)) - matches = list(symbol_pattern.finditer(content)) + raw_blocks = {} # name -> raw text block + extends_map = {} # child name -> parent name - for i, match in enumerate(matches): + for i, match in enumerate(all_matches): name = match.group(1) - - # Skip sub-units (contain _ followed by digits at the end e.g. "SN74HC193_0_1") - # KiCad uses these internally for multi-unit symbols + # Skip KiCad internal sub-unit blocks e.g. SN74HC193DR_0_1 if re.search(r'_\d+_\d+$', name): continue - - # Extract the block for this symbol (up to next top-level symbol or end) start = match.start() - end = matches[i + 1].start() if i + 1 < len(matches) else len(content) + end = all_matches[i + 1].start() if i + 1 < len(all_matches) else len(content) block = content[start:end] + raw_blocks[name] = block - mpn = parse_field(block, "MPN") or parse_field(block, "Value") or name - description = (parse_field(block, "ki_description") - or parse_field(block, "Description") - or parse_field(block, "description") - or "") - footprint = parse_field(block, "Footprint") - digikey_pn = parse_field(block, "Digikey_PN") or parse_field(block, "Digikey PN") or "-" - manufacturer = parse_field(block, "Manufacturer") or "-" + extends_match = re.search(r'\(extends\s+"([^"]+)"\)', block) + if extends_match: + extends_map[name] = extends_match.group(1) - # Shorten footprint for table readability — keep library:name but trim long paths - if footprint and ":" in footprint: - lib_part, fp_name = footprint.split(":", 1) - # Truncate very long footprint names - if len(fp_name) > 30: - fp_name = fp_name[:27] + "..." - footprint = f"{lib_part}:{fp_name}" + # --- Pass 2: resolve fields, merging parent into child --- + def resolve_fields(name, visited=None): + if visited is None: + visited = set() + if name in visited: + return {} + visited.add(name) + block = raw_blocks.get(name, "") + own_fields = extract_all_fields(block) + parent_name = extends_map.get(name) + if parent_name: + parent_fields = resolve_fields(parent_name, visited) + return {**parent_fields, **own_fields} + return own_fields + + # --- Pass 3: build symbol list --- + symbols = [] + + for name in raw_blocks: + is_derived = name in extends_map + fields = resolve_fields(name) + mpn = fields.get("MPN") or fields.get("Value") or name + + # Skip base/template symbols — no MPN field, just drawing sources + if not is_derived and not fields.get("MPN"): + continue + + description = (fields.get("ki_description") + or fields.get("Description") + or fields.get("description") + or "—") + footprint = shorten_footprint(fields.get("Footprint", "")) or "-" + digikey_pn = fields.get("Digikey_PN") or fields.get("Digikey PN") or "-" + manufacturer = fields.get("Manufacturer") or "-" symbols.append({ "name": name, "mpn": mpn, "description": description, - "footprint": footprint or "-", + "footprint": footprint, "digikey_pn": digikey_pn, "manufacturer": manufacturer, }) @@ -114,7 +134,7 @@ def parse_symbols_from_file(filepath: Path) -> list[dict]: return symbols -def build_component_table(all_symbols: dict[str, list[dict]]) -> str: +def build_component_table(all_symbols: dict) -> str: """Build the full markdown component index table.""" lines = [] lines.append("| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |") @@ -128,7 +148,7 @@ def build_component_table(all_symbols: dict[str, list[dict]]) -> str: for s in sorted(symbols, key=lambda x: x["mpn"]): lines.append( f"| {s['mpn']} " - f"| {s['description'] or '—'} " + f"| {s['description']} " f"| {s['manufacturer']} " f"| {category} " f"| {s['footprint']} " @@ -143,13 +163,11 @@ def build_component_table(all_symbols: dict[str, list[dict]]) -> str: def update_readme(new_table: str) -> bool: """ - Replace the content between the Component Index header and the next - top-level ## header in README.md with the new table. + Replace the Component Index section in README.md with the new table. Returns True if the file was changed. """ readme = README_PATH.read_text(encoding="utf-8") - # Match from ## Component Index to the next ## section or end of file pattern = re.compile( r'(## Component Index\n+)(.*?)(\n## |\Z)', re.DOTALL @@ -178,14 +196,14 @@ def update_readme(new_table: str) -> bool: def main(): if not SYMBOLS_DIR.exists(): - print(f"ERROR: symbols/ directory not found. Run from repo root.") + print("ERROR: symbols/ directory not found. Run from repo root.") return if not README_PATH.exists(): - print(f"ERROR: README.md not found. Run from repo root.") + print("ERROR: README.md not found. Run from repo root.") return - all_symbols: dict[str, list[dict]] = {} + all_symbols = {} for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")): lib_name = sym_file.stem