Fix: handle missing library_link attribute on Footprint objects in KiCad 9 (#157)
KiCad 9's IPC API returns Footprint objects where fp.definition lacks the library_link attribute, causing all component queries to fail with "'Footprint' object has no attribute 'library_link'" warnings and returning 0 components. This adds a hasattr check with fallback to fp.definition.id. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
tests/test_ipc_footprint_library_link_guard.py
Normal file
81
tests/test_ipc_footprint_library_link_guard.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Regression test for IPCBoardAPI.list_components — footprint field guard.
|
||||
|
||||
KiCad 9 IPC sometimes returns Footprint objects whose ``definition`` lacks the
|
||||
``library_link`` attribute, which used to raise ``AttributeError`` and drop
|
||||
every component from ``list_components``. The fix guards the access and falls
|
||||
back to ``definition.id`` when present, or an empty string.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||
|
||||
|
||||
# Stub kipy.util.units.to_mm so the module-level import inside list_components
|
||||
# succeeds without a real kipy installation.
|
||||
_kipy = sys.modules.setdefault("kipy", MagicMock(name="kipy"))
|
||||
_kipy_util = sys.modules.setdefault("kipy.util", MagicMock(name="kipy.util"))
|
||||
_kipy_units = sys.modules.setdefault("kipy.util.units", MagicMock(name="kipy.util.units"))
|
||||
_kipy_units.to_mm = lambda v: v / 1_000_000
|
||||
|
||||
|
||||
def _make_fp(*, library_link=None, fp_id=None):
|
||||
"""Build a minimal mock Footprint with the requested definition shape."""
|
||||
if library_link is not None and fp_id is not None:
|
||||
definition = SimpleNamespace(library_link=library_link, id=fp_id)
|
||||
elif library_link is not None:
|
||||
definition = SimpleNamespace(library_link=library_link)
|
||||
elif fp_id is not None:
|
||||
definition = SimpleNamespace(id=fp_id)
|
||||
else:
|
||||
definition = SimpleNamespace() # neither attribute
|
||||
|
||||
fp = SimpleNamespace(
|
||||
position=SimpleNamespace(x=1_000_000, y=2_000_000),
|
||||
reference_field=SimpleNamespace(text=SimpleNamespace(value="R1")),
|
||||
value_field=SimpleNamespace(text=SimpleNamespace(value="220")),
|
||||
definition=definition,
|
||||
orientation=SimpleNamespace(degrees=0),
|
||||
layer="F.Cu",
|
||||
id="fp-uuid-1",
|
||||
)
|
||||
return fp
|
||||
|
||||
|
||||
def _list_components_with(fps):
|
||||
"""Run IPCBoardAPI.list_components against a list of mock footprints."""
|
||||
from kicad_api.ipc_backend import IPCBoardAPI
|
||||
|
||||
api = IPCBoardAPI.__new__(IPCBoardAPI)
|
||||
fake_board = MagicMock()
|
||||
fake_board.get_footprints.return_value = fps
|
||||
with patch.object(api, "_get_board", return_value=fake_board, create=True):
|
||||
return api.list_components()
|
||||
|
||||
|
||||
def test_definition_with_library_link_returned_as_footprint():
|
||||
fp = _make_fp(library_link="Library:Footprint")
|
||||
[comp] = _list_components_with([fp])
|
||||
assert comp["footprint"] == "Library:Footprint"
|
||||
|
||||
|
||||
def test_definition_without_library_link_falls_back_to_id():
|
||||
"""Regression: the bug we are guarding against."""
|
||||
fp = _make_fp(fp_id="some-fp-id")
|
||||
[comp] = _list_components_with([fp])
|
||||
# Old code raised AttributeError and dropped the component entirely.
|
||||
assert comp["footprint"] == "some-fp-id"
|
||||
assert comp["reference"] == "R1"
|
||||
|
||||
|
||||
def test_definition_without_either_attribute_returns_empty_string():
|
||||
fp = _make_fp()
|
||||
[comp] = _list_components_with([fp])
|
||||
assert comp["footprint"] == ""
|
||||
# And the component is still emitted (not silently dropped)
|
||||
assert comp["reference"] == "R1"
|
||||
Reference in New Issue
Block a user