Notes for anyone (human or AI) changing this codebase. The formats themselves are documented in SPD.md and JPD.md; this file covers the things the code doesn't say out loud.
Symbols reach KiCad along one path — a part description becomes CSV rows, and
kipart turns those rows into a .kicad_sym library:
SPD ─┐
├─> CSV ─> .kicad_sym
JPD ─┘
kipart/kipart.py— the core: CSV rows ↔.kicad_sym, plus symbol layout. Owns thekipartandkilib2csvcommands.kipart/spd.py— owns the SPD format, in both directions: its comment syntax, pin type codes, style modifiers, pin naming rules, and the reading and writing of pin lines.parse_spd_symbolis the one reader of SPD's directives, and it returns a structured part shaped exactly like JPD.kipart/part.py— owns the neutral part structure thatparse_spd_symbolreturns and JPD spells out.symbol_to_partis the one reader of a.kicad_symsymbol into it, andload_partsreads a part out of any of the three formats. Asksymbol_to_partforgeometry=Trueand the part also carries what SPD and JPD can't say — pin positions, pin lengths, and the body shapes — which is whatkilib2spdneeds to work out spacers and whatcompare_partsneeds to compare layouts.kipart/spd2csv.py,kipart/kilib2spd.py,kipart/jpd.py— converters that sit on top ofspd.pyandpart.py: SPD→CSV, library→SPD, and SPD↔JPD (plus library→JPD).kipart/compare_parts.py— compares the parts of two or more libraries in any of the three formats, optionally disregarding geometry, and pairs up parts whose names don't agree. Owns thecmppartscommand.kipart/mcp_server.py— an optional MCP server, built on FastMCP, putting one tool behind each of the eight commands so an AI agent can reach them. It sits on top of everything else and nothing depends on it; FastMCP is thekipart[mcp]extra, so the import is guarded. Its tools take a part description as text or as a path, which is why it wants string-level functions rather than the file-level ones the commands use.tests/unit/compare_symbols.py— a test-only helper (not part of the shipped package) that compares.kicad_symsymbols and libraries at the S-expression level, ignoring the order of properties, units, and pins, and including geometry and property effects thatcompare_partsdeliberately drops. It's how the round-trip tests check the symbol writer is byte-faithful;compare_partsis the one to reach for when the question is only whether two libraries hold the same parts.
The layering rule: converters depend on spd.py and part.py, never on each
other. If you need something from a sibling converter, it belongs in one of those
two. Anything about how SPD is spelled goes in spd.py — the whole point of
that module is that adding a directive or a modifier is a one-place change — and
anything that reads a .kicad_sym symbol into a part goes in part.py, so that
there is one such reader and not several. part.py may depend on kipart.py and
spd.py; neither of those may depend on it.
Run the tests, but don't stop there — the tests can't tell you a symbol came out subtly wrong:
pytest tests # 194 tests
tox # py39-py314
The MCP server's tests skip themselves when FastMCP isn't installed, so a tox
run that comes back green says nothing about them. pip install '.[mcp]'
first, or run them directly, when that's what changed.
Tools must stay run_in_thread=False — that's what the tool() wrapper in
mcp_server.py is for. The tools capture stdout so that a stray print can't
corrupt the JSON-RPC on the same pipe, and redirecting stdout changes the whole
process: from FastMCP's default worker thread, a tool would be able to swallow
a response the event loop was writing at that moment.
The real check is a round trip against tests/examples/grabbag.spd, which is a
deliberate torture case (multi-unit parts, alternates, buses, spacers, every pin
type and modifier). Convert it and compare — tests/unit/compare_symbols.py is
the S-expression-level yardstick:
- SPD → JPD → SPD should produce identical CSV, and the JPD should be idempotent across a second trip.
- library → SPD → library should leave every pin's name, type, style, and
alternates intact —
compare_parts(..., ignore=["names", "properties", "geometry"])is the check (seetest_symbol_lib_to_spd_roundtrip). Six of grabbag's eight symbols also come back byte-identical viasymbols_are_equal; the two that don't (opa2333,rt9818) differ only in where a side's pins sit along its edge, which SPD deliberately doesn't record — kipart re-derives that from--push. Symbols built with--bundledon't come back byte-identical either, because their bundled pins are stacked at one spot.
cmpparts is the quick way to ask the same question of two libraries built any
which way: cmpparts -g old.kicad_sym new.kicad_sym exits 0 when they hold the
same parts, whatever the layout, and names what changed when they don't.
tests/examples/cmp_a.spd and cmp_b.spd are a pair holding one difference of
every category cmpparts reports, for trying it out by hand. Their comments say
what those differences are, and TestExampleLibraries holds them to it — so if
you edit the pair, that's where it will tell you.
tests/examples/*.kicad_symare gitignored build products, not fixtures.test.mkregenerates them from the.csvand.spdfiles. A test that reads one will pass locally and fail on a fresh clone — build fromgrabbag.spdinstead.str_to_typeandstr_to_styleinkipart.pyaccept loose aliases (pwr,inv_clk, …) and have twice mapped a value to the wrong result. New code should validate against KiCad's canonical names —spd.KICAD_TYPE_TO_SPDandspd.KICAD_STYLE_TO_SPDare keyed by them — rather than route through the alias functions.- CSV row order is not symbol order. kipart groups pins by unit and side and sorts within a side, so re-ordering rows across sides changes the CSV without changing the symbol. Compare symbols, not CSV text.
- A pin number repeated within a unit becomes an alternate of the pin that claimed it first. That's true of CSV rows, SPD lines, and JPD alike.
- A pin number is a name, not a quantity. KiCad quotes them, so
simp_sexphands them back as strings — but an unquoted one comes back as anint, and then"1" != 1quietly makes every pin of a part look missing.part.pyreads pin numbers, names, types, and styles throughstr()for that reason. Anything else that compares pin numbers across formats should do the same. extendsis resolved only in the kilib2 read paths*, not universally.kipart.resolve_extendscopies a base part's units and pins into a part that extends it, and it's called insymbol_lib_to_parts(feeding kilib2spd and kilib2jpd) andsymbol_lib_file_to_csv_file(kilib2csv) — not inextract_symbols_from_lib, whichmerge_symbol_libsuses and which must leave theextendsrelationship intact. It lives inkipart.pybecause it's a.kicad_sym-structure operation andpart.pymay import it from there; a copy inpart.pywould break the layering.