updated .py script

This commit is contained in:
2026-03-08 15:45:01 -04:00
parent 0df4e2a382
commit 0de14c6b72
2 changed files with 79 additions and 61 deletions

View File

@@ -385,7 +385,7 @@ AidanBrzezinski_KiCad_Library/
| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN | | 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* *1 components — auto-generated 2026-03-08*

View File

@@ -9,33 +9,20 @@ Reads the following fields from each symbol:
Symbol Library column = which .kicad_sym file the symbol lives in. Symbol Library column = which .kicad_sym file the symbol lives in.
Used to locate a symbol quickly in KiCad Symbol Editor. 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 locally: python3 .github/scripts/update_component_index.py
Run in CI: triggered automatically on push to symbols/ Run in CI: triggered automatically on push to symbols/
""" """
import re import re
import os
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
SYMBOLS_DIR = Path("symbols") SYMBOLS_DIR = Path("symbols")
README_PATH = Path("README.md") 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 = [ CATEGORY_ORDER = [
"0_ic_logic", "0_ic_logic",
"0_ic_driver", "0_ic_driver",
@@ -50,63 +37,96 @@ CATEGORY_ORDER = [
] ]
def parse_field(symbol_block: str, field_name: str) -> str: def extract_all_fields(block: str) -> dict:
"""Extract a named field value from a symbol block.""" """Extract all property fields from a symbol block into a dict."""
# KiCad sym format: (property "FieldName" "Value" ...) fields = {}
pattern = rf'\(property\s+"{re.escape(field_name)}"\s+"([^"]*)"' for m in re.finditer(r'\(property\s+"([^"]+)"\s+"([^"]*)"', block):
match = re.search(pattern, symbol_block) fields[m.group(1)] = m.group(2).strip()
return match.group(1).strip() if match else "" return fields
def parse_symbols_from_file(filepath: Path) -> list[dict]: def shorten_footprint(footprint: str) -> str:
""" """Trim long footprint names for table readability."""
Parse all symbols from a .kicad_sym file.
Returns a list of dicts with keys: name, mpn, description, footprint, digikey_pn, manufacturer
"""
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)
matches = list(symbol_pattern.finditer(content))
for i, match in enumerate(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
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)
block = content[start:end]
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 "-"
# Shorten footprint for table readability — keep library:name but trim long paths
if footprint and ":" in footprint: if footprint and ":" in footprint:
lib_part, fp_name = footprint.split(":", 1) lib_part, fp_name = footprint.split(":", 1)
# Truncate very long footprint names
if len(fp_name) > 30: if len(fp_name) > 30:
fp_name = fp_name[:27] + "..." fp_name = fp_name[:27] + "..."
footprint = f"{lib_part}:{fp_name}" return f"{lib_part}:{fp_name}"
return footprint
def parse_symbols_from_file(filepath: Path) -> list:
"""
Parse all symbols from a .kicad_sym file.
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")
# --- 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))
raw_blocks = {} # name -> raw text block
extends_map = {} # child name -> parent name
for i, match in enumerate(all_matches):
name = match.group(1)
# Skip KiCad internal sub-unit blocks e.g. SN74HC193DR_0_1
if re.search(r'_\d+_\d+$', name):
continue
start = match.start()
end = all_matches[i + 1].start() if i + 1 < len(all_matches) else len(content)
block = content[start:end]
raw_blocks[name] = block
extends_match = re.search(r'\(extends\s+"([^"]+)"\)', block)
if extends_match:
extends_map[name] = extends_match.group(1)
# --- 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({ symbols.append({
"name": name, "name": name,
"mpn": mpn, "mpn": mpn,
"description": description, "description": description,
"footprint": footprint or "-", "footprint": footprint,
"digikey_pn": digikey_pn, "digikey_pn": digikey_pn,
"manufacturer": manufacturer, "manufacturer": manufacturer,
}) })
@@ -114,7 +134,7 @@ def parse_symbols_from_file(filepath: Path) -> list[dict]:
return symbols 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.""" """Build the full markdown component index table."""
lines = [] lines = []
lines.append("| MPN | Description | Manufacturer | Symbol Library | Footprint | Digikey PN |") 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"]): for s in sorted(symbols, key=lambda x: x["mpn"]):
lines.append( lines.append(
f"| {s['mpn']} " f"| {s['mpn']} "
f"| {s['description'] or ''} " f"| {s['description']} "
f"| {s['manufacturer']} " f"| {s['manufacturer']} "
f"| {category} " f"| {category} "
f"| {s['footprint']} " 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: def update_readme(new_table: str) -> bool:
""" """
Replace the content between the Component Index header and the next Replace the Component Index section in README.md with the new table.
top-level ## header in README.md with the new table.
Returns True if the file was changed. Returns True if the file was changed.
""" """
readme = README_PATH.read_text(encoding="utf-8") readme = README_PATH.read_text(encoding="utf-8")
# Match from ## Component Index to the next ## section or end of file
pattern = re.compile( pattern = re.compile(
r'(## Component Index\n+)(.*?)(\n## |\Z)', r'(## Component Index\n+)(.*?)(\n## |\Z)',
re.DOTALL re.DOTALL
@@ -178,14 +196,14 @@ def update_readme(new_table: str) -> bool:
def main(): def main():
if not SYMBOLS_DIR.exists(): 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 return
if not README_PATH.exists(): 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 return
all_symbols: dict[str, list[dict]] = {} all_symbols = {}
for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")): for sym_file in sorted(SYMBOLS_DIR.glob("*.kicad_sym")):
lib_name = sym_file.stem lib_name = sym_file.stem