diff --git a/src/faebryk/core/graphinterface.py b/src/faebryk/core/graphinterface.py index 45fe244f..9e836af8 100644 --- a/src/faebryk/core/graphinterface.py +++ b/src/faebryk/core/graphinterface.py @@ -10,6 +10,7 @@ from faebryk.core.link import Link, LinkDirect, LinkNamedParent from faebryk.libs.util import ( NotNone, + exceptions_to_log, try_avoid_endless_recursion, ) @@ -123,7 +124,8 @@ def connect(self, other: Self, linkcls=None) -> Self: self.G.add_edge(self, other, link=link) if logger.isEnabledFor(logging.DEBUG): - logger.debug(f"GIF connection: {link}") + with exceptions_to_log(): + logger.debug(f"GIF connection: {link}") return self diff --git a/src/faebryk/libs/kicad/fileformats.py b/src/faebryk/libs/kicad/fileformats.py index 98062d20..130bcdaa 100644 --- a/src/faebryk/libs/kicad/fileformats.py +++ b/src/faebryk/libs/kicad/fileformats.py @@ -1,5 +1,4 @@ import logging -import uuid from dataclasses import dataclass, field from enum import IntEnum, StrEnum, auto from pathlib import Path @@ -7,8 +6,18 @@ from dataclasses_json import CatchAll, Undefined, dataclass_json +from faebryk.libs.kicad.fileformats_common import ( + UUID, + C_effects, + C_pts, + C_stroke, + C_wh, + C_xy, + C_xyr, + C_xyz, + gen_uuid, +) from faebryk.libs.sexp.dataclass_sexp import JSON_File, SEXP_File, SymEnum, sexp_field -from faebryk.libs.util import KeyErrorAmbiguous logger = logging.getLogger(__name__) @@ -474,90 +483,6 @@ class C_text_variables: unknown: CatchAll = None -class UUID(str): - pass - - -def gen_uuid(mark: str = ""): - # format: d864cebe-263c-4d3f-bbd6-bb51c6d2a608 - value = uuid.uuid4().hex - - suffix = mark.encode().hex() - if suffix: - value = value[: -len(suffix)] + suffix - - DASH_IDX = [8, 12, 16, 20] - formatted = value - for i, idx in enumerate(DASH_IDX): - formatted = formatted[: idx + i] + "-" + formatted[idx + i :] - - return UUID(formatted) - - -@dataclass -class C_xy: - x: float = field(**sexp_field(positional=True)) - y: float = field(**sexp_field(positional=True)) - - def __sub__(self, other: "C_xy") -> "C_xy": - return C_xy(x=self.x - other.x, y=self.y - other.y) - - def __add__(self, other: "C_xy") -> "C_xy": - return C_xy(x=self.x + other.x, y=self.y + other.y) - - def rotate(self, center: "C_xy", angle: float) -> "C_xy": - import math - - angle = -angle # rotate kicad style counter-clockwise - - # Translate point to origin - translated_x = self.x - center.x - translated_y = self.y - center.y - - # Convert angle to radians - angle = math.radians(angle) - - # Rotate - rotated_x = translated_x * math.cos(angle) - translated_y * math.sin(angle) - rotated_y = translated_x * math.sin(angle) + translated_y * math.cos(angle) - - # Translate back - new_x = rotated_x + center.x - new_y = rotated_y + center.y - - return C_xy(x=new_x, y=new_y) - - -@dataclass -class C_xyz: - x: float = field(**sexp_field(positional=True)) - y: float = field(**sexp_field(positional=True)) - z: float = field(**sexp_field(positional=True)) - - -@dataclass -class C_xyr: - x: float = field(**sexp_field(positional=True)) - y: float = field(**sexp_field(positional=True)) - r: float = field(**sexp_field(positional=True), default=0) - - -@dataclass -class C_wh: - w: float = field(**sexp_field(positional=True)) - h: Optional[float] = field(**sexp_field(positional=True), default=None) - - -@dataclass -class C_stroke: - class E_type(SymEnum): - solid = auto() - default = auto() - - width: float - type: E_type - - @dataclass class C_text_layer: class E_knockout(SymEnum): @@ -567,56 +492,6 @@ class E_knockout(SymEnum): knockout: Optional[E_knockout] = field(**sexp_field(positional=True), default=None) -@dataclass -class C_effects: - @dataclass - class C_font: - size: C_wh - thickness: Optional[float] = None - - @dataclass - class C_justify: - class E_justify(SymEnum): - center_horizontal = "" - left = auto() - right = auto() - center_vertical = "" - bottom = auto() - top = auto() - normal = "" - mirror = auto() - - justifys: list[E_justify] = field( - **sexp_field(positional=True), default_factory=list - ) - - font: C_font - - # Legal: - # (justify mirror right) - # (justify bottom) - justifys: list[C_justify] = field( - **sexp_field(multidict=True), default_factory=list - ) - - def get_justifys(self) -> list[C_justify.E_justify]: - return [j_ for j in self.justifys for j_ in j.justifys] - - def __post_init__(self): - justifys = set(self.get_justifys()) - - J = C_effects.C_justify.E_justify - - def _only_one_of(lst: list[J]): - dups = [j for j in justifys if j in lst] - if len(dups) > 1: - raise KeyErrorAmbiguous(dups) - - _only_one_of([J.mirror, J.normal]) - _only_one_of([J.left, J.right, J.center_horizontal]) - _only_one_of([J.top, J.bottom, J.center_vertical]) - - class E_fill(SymEnum): none = auto() solid = auto() @@ -687,10 +562,6 @@ class C_rect: @dataclass class C_polygon: - @dataclass - class C_pts: - xys: list[C_xy] = field(**sexp_field(multidict=True), default_factory=list) - pts: C_pts @@ -1042,7 +913,7 @@ class C_filled_polygon: island: Optional[bool] = field( **sexp_field(positional=True), default=None ) - pts: C_polygon.C_pts + pts: C_pts net: int net_name: str diff --git a/src/faebryk/libs/kicad/fileformats_common.py b/src/faebryk/libs/kicad/fileformats_common.py new file mode 100644 index 00000000..4df90f71 --- /dev/null +++ b/src/faebryk/libs/kicad/fileformats_common.py @@ -0,0 +1,151 @@ +import logging +import uuid +from dataclasses import dataclass, field +from enum import auto +from typing import Optional + +from faebryk.libs.sexp.dataclass_sexp import SymEnum, sexp_field +from faebryk.libs.util import KeyErrorAmbiguous + +logger = logging.getLogger(__name__) + +# TODO find complete examples of the fileformats, maybe in the kicad repo + + +class UUID(str): + pass + + +@dataclass +class C_xy: + x: float = field(**sexp_field(positional=True)) + y: float = field(**sexp_field(positional=True)) + + def __sub__(self, other: "C_xy") -> "C_xy": + return C_xy(x=self.x - other.x, y=self.y - other.y) + + def __add__(self, other: "C_xy") -> "C_xy": + return C_xy(x=self.x + other.x, y=self.y + other.y) + + def rotate(self, center: "C_xy", angle: float) -> "C_xy": + import math + + angle = -angle # rotate kicad style counter-clockwise + + # Translate point to origin + translated_x = self.x - center.x + translated_y = self.y - center.y + + # Convert angle to radians + angle = math.radians(angle) + + # Rotate + rotated_x = translated_x * math.cos(angle) - translated_y * math.sin(angle) + rotated_y = translated_x * math.sin(angle) + translated_y * math.cos(angle) + + # Translate back + new_x = rotated_x + center.x + new_y = rotated_y + center.y + + return C_xy(x=new_x, y=new_y) + + +@dataclass +class C_xyz: + x: float = field(**sexp_field(positional=True)) + y: float = field(**sexp_field(positional=True)) + z: float = field(**sexp_field(positional=True)) + + +@dataclass +class C_xyr: + x: float = field(**sexp_field(positional=True)) + y: float = field(**sexp_field(positional=True)) + r: float = field(**sexp_field(positional=True), default=0) + + +@dataclass +class C_wh: + w: float = field(**sexp_field(positional=True)) + h: Optional[float] = field(**sexp_field(positional=True), default=None) + + +@dataclass +class C_stroke: + class E_type(SymEnum): + solid = auto() + default = auto() + + width: float + type: E_type + + +@dataclass +class C_effects: + @dataclass + class C_font: + size: C_wh + thickness: Optional[float] = None + + @dataclass + class C_justify: + class E_justify(SymEnum): + center_horizontal = "" + left = auto() + right = auto() + center_vertical = "" + bottom = auto() + top = auto() + normal = "" + mirror = auto() + + justifys: list[E_justify] = field( + **sexp_field(positional=True), default_factory=list + ) + + font: C_font + + # Legal: + # (justify mirror right) + # (justify bottom) + justifys: list[C_justify] = field( + **sexp_field(multidict=True), default_factory=list + ) + + def get_justifys(self) -> list[C_justify.E_justify]: + return [j_ for j in self.justifys for j_ in j.justifys] + + def __post_init__(self): + justifys = set(self.get_justifys()) + + J = C_effects.C_justify.E_justify + + def _only_one_of(lst: list[J]): + dups = [j for j in justifys if j in lst] + if len(dups) > 1: + raise KeyErrorAmbiguous(dups) + + _only_one_of([J.mirror, J.normal]) + _only_one_of([J.left, J.right, J.center_horizontal]) + _only_one_of([J.top, J.bottom, J.center_vertical]) + + +@dataclass +class C_pts: + xys: list[C_xy] = field(**sexp_field(multidict=True), default_factory=list) + + +def gen_uuid(mark: str = ""): + # format: d864cebe-263c-4d3f-bbd6-bb51c6d2a608 + value = uuid.uuid4().hex + + suffix = mark.encode().hex() + if suffix: + value = value[: -len(suffix)] + suffix + + DASH_IDX = [8, 12, 16, 20] + formatted = value + for i, idx in enumerate(DASH_IDX): + formatted = formatted[: idx + i] + "-" + formatted[idx + i :] + + return UUID(formatted) diff --git a/src/faebryk/libs/kicad/fileformats_sch.py b/src/faebryk/libs/kicad/fileformats_sch.py new file mode 100644 index 00000000..816d3db9 --- /dev/null +++ b/src/faebryk/libs/kicad/fileformats_sch.py @@ -0,0 +1,329 @@ +import logging +from dataclasses import dataclass, field +from enum import StrEnum, auto +from typing import Optional + +from faebryk.libs.kicad.fileformats_common import ( + UUID, + C_effects, + C_pts, + C_xy, + C_xyr, + gen_uuid, +) +from faebryk.libs.sexp.dataclass_sexp import SEXP_File, SymEnum, sexp_field + +logger = logging.getLogger(__name__) + +# TODO find complete examples of the fileformats, maybe in the kicad repo + + +@dataclass +class C_property: + name: str = field(**sexp_field(positional=True)) + value: str = field(**sexp_field(positional=True)) + at: Optional[C_xyr] = None + effects: Optional[C_effects] = None + id: Optional[int] = None + + +@dataclass(kw_only=True) # TODO: when to use kw_only? +class C_fill: + class E_type(SymEnum): + background = "background" + none = "none" + + type: E_type = field(default=E_type.background) + + +@dataclass +class C_stroke: + class E_type(SymEnum): + solid = auto() + default = auto() + + width: float + type: E_type + color: tuple[int, int, int, int] + + +@dataclass(kw_only=True) +class C_circle: + center: C_xy + end: C_xy + stroke: C_stroke + fill: C_fill + uuid: UUID = field(default_factory=gen_uuid) + + +@dataclass(kw_only=True) +class C_arc: + start: C_xy + mid: C_xy + end: C_xy + stroke: C_stroke + fill: C_fill + uuid: UUID = field(default_factory=gen_uuid) + + +@dataclass(kw_only=True) +class C_rect: + start: C_xy + end: C_xy + stroke: C_stroke + fill: C_fill + uuid: UUID = field(default_factory=gen_uuid) + + +@dataclass(kw_only=True) +class C_polyline: + stroke: C_stroke + fill: C_fill + pts: C_pts = field(default_factory=C_pts) + + +@dataclass +class C_kicad_sch_file(SEXP_File): + """ + When in doubt check: kicad/eeschema/sch_io/kicad_sexpr/sch_io_kicad_sexpr_parser.cpp + """ + + @dataclass + class C_kicad_sch: + @dataclass + class C_title_block: + title: Optional[str] = None + date: Optional[str] = None + rev: Optional[str] = None + company: Optional[str] = None + + @dataclass + class C_lib_symbols: + @dataclass + class C_symbol: + @dataclass + class C_pin_names: + offset: float + + @dataclass + class C_symbol: + @dataclass + class C_pin: + class E_type(StrEnum): + input = "input" + output = "output" + passive = "passive" + power_in = "power_in" + power_out = "power_out" + bidirectional = "bidirectional" + + class E_style(StrEnum): + line = "line" + inverted = "inverted" + # Unvalidated + # arrow = "arrow" + # dot = "dot" + # none = "none" + + @dataclass + class C_name: + name: str = field(**sexp_field(positional=True)) + effects: C_effects = field(default_factory=C_effects) + + @dataclass + class C_number: + number: str = field(**sexp_field(positional=True)) + effects: C_effects = field(default_factory=C_effects) + + at: C_xyr + length: float + type: E_type = field(**sexp_field(positional=True)) + style: E_style = field(**sexp_field(positional=True)) + name: C_name = field(default_factory=C_name) + number: C_number = field(default_factory=C_number) + + name: str = field(**sexp_field(positional=True)) + polylines: list[C_polyline] = field( + **sexp_field(multidict=True), default_factory=list + ) + circles: list[C_circle] = field( + **sexp_field(multidict=True), default_factory=list + ) + rectangles: list[C_rect] = field( + **sexp_field(multidict=True), default_factory=list + ) + arcs: list[C_arc] = field( + **sexp_field(multidict=True), default_factory=list + ) + pins: list[C_pin] = field( + **sexp_field(multidict=True), default_factory=list + ) + + class E_show_hide(SymEnum): + hide = "hide" + show = "show" + + @dataclass + class C_power: + pass + + name: str = field(**sexp_field(positional=True)) + power: Optional[C_power] = None + propertys: list[C_property] = field( + **sexp_field(multidict=True), default_factory=list + ) + pin_numbers: E_show_hide = field(default=E_show_hide.show) + pin_names: Optional[C_pin_names] = None + in_bom: Optional[bool] = None + on_board: Optional[bool] = None + symbols: list[C_symbol] = field( + **sexp_field(multidict=True), default_factory=list + ) + convert: Optional[int] = None + + symbol: dict[str, C_symbol] = field( + **sexp_field(multidict=True, key=lambda x: x.name), default_factory=dict + ) + + @dataclass + class C_symbol_instance: + @dataclass + class C_pin: + uuid: UUID + pin: str = field(**sexp_field(positional=True)) + + lib_id: str + uuid: UUID + at: C_xyr + unit: int + in_bom: bool + on_board: bool + fields_autoplaced: bool = True + propertys: list[C_property] = field( + **sexp_field(multidict=True), default_factory=list + ) + pins: list[C_pin] = field( + **sexp_field(multidict=True), default_factory=list + ) + convert: Optional[int] = None + + @dataclass + class C_junction: + at: C_xy + diameter: float + color: tuple[int, int, int, int] + uuid: UUID + + @dataclass + class C_wire: + pts: C_pts + stroke: C_stroke + uuid: UUID + + @dataclass + class C_text: + at: C_xy + effects: C_effects + uuid: UUID + text: str = field(**sexp_field(positional=True)) + + @dataclass + class C_sheet: + @dataclass + class C_fill: + color: Optional[str] = None + + @dataclass + class C_pin: + at: C_xyr + effects: C_effects + uuid: UUID + name: str = field(**sexp_field(positional=True)) + type: str = field(**sexp_field(positional=True)) + + at: C_xy + size: C_xy + stroke: C_stroke + fill: C_fill + uuid: UUID + fields_autoplaced: bool = True + propertys: list[C_property] = field( + **sexp_field(multidict=True), default_factory=list + ) + pins: list[C_pin] = field( + **sexp_field(multidict=True), default_factory=list + ) + + @dataclass + class C_global_label: + shape: str + at: C_xyr + effects: C_effects + uuid: UUID + text: str = field(**sexp_field(positional=True)) + fields_autoplaced: bool = True + propertys: list[C_property] = field( + **sexp_field(multidict=True), default_factory=list + ) + + # TODO: inheritance + # text + # label + # global_label + # hierarchical_label + # netclass_flag + # directive_label + @dataclass + class C_label: + at: C_xyr + effects: C_effects + uuid: UUID + text: str = field(**sexp_field(positional=True)) + + @dataclass + class C_bus: + pts: C_pts + stroke: C_stroke + uuid: UUID + + @dataclass + class C_bus_entry: + at: C_xy + size: C_xy + stroke: C_stroke + uuid: UUID + + version: str + generator: str + uuid: UUID + paper: str + lib_symbols: C_lib_symbols = field(default_factory=C_lib_symbols) + title_block: C_title_block = field(default_factory=C_title_block) + + junctions: list[C_junction] = field( + **sexp_field(multidict=True), default_factory=list + ) + wires: list[C_wire] = field(**sexp_field(multidict=True), default_factory=list) + + texts: list[C_text] = field(**sexp_field(multidict=True), default_factory=list) + symbols: list[C_symbol_instance] = field( + **sexp_field(multidict=True), default_factory=list + ) + sheets: list[C_sheet] = field( + **sexp_field(multidict=True), default_factory=list + ) + global_labels: list[C_global_label] = field( + **sexp_field(multidict=True), default_factory=list + ) + no_connects: list[C_xy] = field( + **sexp_field(multidict=True), default_factory=list + ) + buss: list[C_bus] = field(**sexp_field(multidict=True), default_factory=list) + labels: list[C_label] = field( + **sexp_field(multidict=True), default_factory=list + ) + bus_entrys: list[C_bus_entry] = field( + **sexp_field(multidict=True), default_factory=list + ) + + kicad_sch: C_kicad_sch diff --git a/src/faebryk/libs/sexp/dataclass_sexp.py b/src/faebryk/libs/sexp/dataclass_sexp.py index 18562fea..15b246b2 100644 --- a/src/faebryk/libs/sexp/dataclass_sexp.py +++ b/src/faebryk/libs/sexp/dataclass_sexp.py @@ -1,6 +1,7 @@ import logging from dataclasses import Field, dataclass, fields, is_dataclass from enum import Enum, IntEnum, StrEnum +from os import PathLike from pathlib import Path from types import UnionType from typing import Any, Callable, Iterator, Union, get_args, get_origin @@ -65,6 +66,12 @@ class DecodeError(Exception): """Error during decoding""" +def _prettify_stack(stack: list[tuple[str, type]] | None) -> str: + if stack is None: + return "" + return ".".join(s[0] for s in stack) + + def _convert( val, t, @@ -107,8 +114,23 @@ def _convert( val = val[0] if issubclass(t, bool): - assert val in [Symbol("yes"), Symbol("no")] - return val == Symbol("yes") + # See parseMaybeAbsentBool in kicad + # Default: (hide) hide None + # True: (hide yes) + # False: (hide no) + + # hide, None -> automatically filtered + + # (hide yes) (hide no) + if val in [Symbol("yes"), Symbol("no")]: + return val == Symbol("yes") + + # (hide) + if val == []: + return None + + assert False, f"Invalid value for bool: {val}" + if isinstance(val, Symbol): return t(str(val)) @@ -116,8 +138,9 @@ def _convert( except DecodeError: raise except Exception as e: - pretty_stack = ".".join(s[0] for s in substack) - raise DecodeError(f"Failed to decode {pretty_stack} ({t}) with {val} ") from e + raise DecodeError( + f"Failed to decode {_prettify_stack(substack)} ({t}) with {val} " + ) from e netlist_obj = str | Symbol | int | float | bool | list @@ -177,6 +200,7 @@ def _decode[T]( unprocessed_indices = unprocessed_indices - set(pos_values.keys()) if logger.isEnabledFor(logging.DEBUG): + logger.debug(f"processing: {_prettify_stack(stack)}") logger.debug(f"key_fields: {list(key_fields.keys())}") logger.debug( f"positional_fields: {list(f.name for f in positional_fields.values())}" @@ -188,7 +212,7 @@ def _decode[T]( unprocessed_values = [sexp[i] for i in unprocessed_indices] # This is separate from the above loop to make it easier to debug during dev if logger.isEnabledFor(logging.DEBUG): - logger.debug(f"Unprocessed values: {unprocessed_values}") + logger.debug(f"unprocessed values: {unprocessed_values}") # Parse -------------------------------------------------------------- @@ -236,7 +260,10 @@ def _decode[T]( ) else: assert len(values) == 1, f"Duplicate key: {name}" - value_dict[name] = _convert(values[0][1:], f.type, stack, name) + out = _convert(values[0][1:], f.type, stack, name) + # if val is None, use default + if out is not None: + value_dict[name] = out # Positional for f, v in (it := zip_non_locked(positional_fields.values(), pos_values.values())): @@ -392,7 +419,8 @@ def loads[T](s: str | Path | list, t: type[T]) -> T: return _decode([sexp], t) -def dumps(obj, path: Path | None = None) -> str: +def dumps(obj, path: PathLike | None = None) -> str: + path = Path(path) if path else None sexp = _encode(obj)[0] text = sexpdata.dumps(sexp) text = prettify_sexp_string(text) @@ -416,7 +444,7 @@ class SEXP_File: def loads(cls, path_or_string_or_data: Path | str | list): return loads(path_or_string_or_data, cls) - def dumps(self, path: Path | None = None): + def dumps(self, path: PathLike | None = None): return dumps(self, path) @@ -434,7 +462,8 @@ def loads[T](cls: type[T], path: Path | str) -> T: text = path.read_text() return cls.from_json(text) - def dumps(self, path: Path | None = None): + def dumps(self, path: PathLike | None = None): + path = Path(path) if path else None text = self.to_json(indent=4) if path: path.write_text(text) diff --git a/src/faebryk/libs/util.py b/src/faebryk/libs/util.py index 71304f05..75c0ee65 100644 --- a/src/faebryk/libs/util.py +++ b/src/faebryk/libs/util.py @@ -6,6 +6,7 @@ import logging from abc import abstractmethod from collections import defaultdict +from contextlib import contextmanager from dataclasses import dataclass, fields from enum import StrEnum from functools import cache @@ -858,3 +859,29 @@ def join_if_non_empty(sep: str, *args): def dataclass_as_kwargs(obj: Any) -> dict[str, Any]: return {f.name: getattr(obj, f.name) for f in fields(obj)} + + +@contextmanager +def exceptions_to_log( + logger: logging.Logger = logger, + level: int = logging.WARNING, + mute=True, +): + """ + Send exceptions to the log at level and optionally re-raise. + + The original motivation for this is to avoid raising exceptions + for debugging messages. + """ + try: + yield + except Exception as e: + try: + logger.log(level, str(e), e) + except Exception: + logger.error( + "Exception occurred while logging exception. " + "Not re-stringifying exception to avoid the same" + ) + if not mute: + raise diff --git a/test/libs/kicad/test_fileformats.py b/test/libs/kicad/test_fileformats.py index bcf3c92b..7a70b88d 100644 --- a/test/libs/kicad/test_fileformats.py +++ b/test/libs/kicad/test_fileformats.py @@ -14,8 +14,13 @@ C_kicad_pcb_file, C_kicad_project_file, ) +from faebryk.libs.kicad.fileformats_sch import C_kicad_sch_file from faebryk.libs.logging import setup_basic_logging -from faebryk.libs.sexp.dataclass_sexp import JSON_File, SEXP_File, dataclass_dfs +from faebryk.libs.sexp.dataclass_sexp import ( + JSON_File, + SEXP_File, + dataclass_dfs, +) from faebryk.libs.util import ConfigFlag, NotNone, find logger = logging.getLogger(__name__) @@ -30,7 +35,7 @@ FPFILE = TEST_FILES / "test.kicad_mod" NETFILE = TEST_FILES / "test_e.net" FPLIBFILE = TEST_FILES / "fp-lib-table" - +SCHFILE = TEST_FILES / "test.kicad_sch" DUMP = ConfigFlag("DUMP", descr="dump load->save into /tmp") @@ -41,16 +46,15 @@ def test_parser(self): fp = C_kicad_footprint_file.loads(FPFILE) netlist = C_kicad_netlist_file.loads(NETFILE) pro = C_kicad_project_file.loads(PRJFILE) + sch = C_kicad_sch_file.loads(SCHFILE) + + assert [f.name for f in pcb.kicad_pcb.footprints] == [ + "logos:faebryk_logo", + "lcsc:LED0603-RD-YELLOW", + "lcsc:R0402", + "lcsc:BAT-TH_BS-02-A1AJ010", + ] - self.assertEqual( - [f.name for f in pcb.kicad_pcb.footprints], - [ - "logos:faebryk_logo", - "lcsc:LED0603-RD-YELLOW", - "lcsc:R0402", - "lcsc:BAT-TH_BS-02-A1AJ010", - ], - ) self.assertFalse(pcb.kicad_pcb.setup.pcbplotparams.usegerberextensions) padtype = pcb.C_kicad_pcb.C_pcb_footprint.C_pad.E_type @@ -111,6 +115,16 @@ def test_parser(self): self.assertEqual(pro.pcbnew.last_paths.netlist, "../../faebryk/faebryk.net") + self.assertEqual( + sch.kicad_sch.lib_symbols.symbol["Amplifier_Audio:LM4990ITL"] + .propertys[3] + .value, + "http://www.ti.com/lit/ds/symlink/lm4990.pdf", + ) + + self.assertIsNotNone(sch.kicad_sch.lib_symbols.symbol["power:GND"].power) + self.assertIsNone(sch.kicad_sch.lib_symbols.symbol["Device:R"].power) + def test_write(self): pcb = C_kicad_pcb_file.loads(PCBFILE) @@ -187,6 +201,7 @@ def test_reload(path: Path, parser: type[SEXP_File | JSON_File]): (C_kicad_netlist_file, NETFILE), (C_kicad_project_file, PRJFILE), (C_kicad_fp_lib_table_file, FPLIBFILE), + (C_kicad_sch_file, SCHFILE), ]: test_reload(file, parser)