"""Hand-rolled S-expression emitter for KiCad files. KiCad reads a Lisp-flavored S-expr; values are atoms (symbols / strings / numbers) and lists. We emit lists as nested Python lists, with ``Sym`` marking strings that should serialize as bare symbols (unquoted) — e.g. ``Sym("xy")``, ``Sym("kicad_sch")``, ``Sym("yes")``. Plain ``str`` values get JSON-style double-quoting + backslash escaping. Floats render with up to 6 decimals trimmed of trailing zeros (KiCad reads either form, but trimmed is what kicad-cli emits). """ from __future__ import annotations import io import math from typing import Any class Sym(str): """Marker subclass: render as a bare S-expr symbol (unquoted).""" __slots__ = () def _fmt_number(n: float | int) -> str: if isinstance(n, bool): # bool is int subclass — guard first return "yes" if n else "no" if isinstance(n, int): return str(n) if math.isnan(n) or math.isinf(n): raise ValueError(f"can't serialize non-finite number: {n}") s = f"{n:.6f}".rstrip("0").rstrip(".") return s if s else "0" def _emit(value: Any, out: io.StringIO, indent: int, *, pretty: bool) -> None: if isinstance(value, list): out.write("(") for i, item in enumerate(value): if i > 0: if pretty and isinstance(item, list) and len(item) > 1: out.write("\n" + "\t" * (indent + 1)) else: out.write(" ") _emit(item, out, indent + 1, pretty=pretty) out.write(")") elif isinstance(value, Sym): out.write(str(value)) elif value is True or value is False: out.write("yes" if value else "no") elif isinstance(value, (int, float)): out.write(_fmt_number(value)) elif isinstance(value, str): # KiCad's S-expr reader rejects literal newlines/CR/tabs inside # quoted strings; we MUST escape them. Order matters: backslash first. escaped = ( value.replace("\\", "\\\\") .replace('"', '\\"') .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t") ) out.write(f'"{escaped}"') elif value is None: # rare but sometimes a slot is intentionally empty out.write('""') else: raise TypeError(f"can't S-expr serialize {type(value).__name__}: {value!r}") def to_sexpr(value: Any, *, pretty: bool = True) -> str: """Render a Python data structure as a KiCad-style S-expression. Top-level value is usually a list whose first element is a ``Sym`` (the block tag, e.g. ``Sym("kicad_sch")``). When ``pretty=True``, child lists of length > 1 go onto their own indented line. """ buf = io.StringIO() _emit(value, buf, indent=0, pretty=pretty) if pretty: buf.write("\n") return buf.getvalue()