Merge pull request #109 from mixelpixx/feat/schematic-label-filters-and-duplicates
feat: add netName and labelType filters to list_schematic_labels
This commit is contained in:
@@ -623,8 +623,12 @@ class KiCADInterface:
|
||||
}
|
||||
|
||||
sch_path = path if path and path != "." else None
|
||||
schematic = SchematicManager.create_schematic(project_name, path=sch_path, metadata=metadata)
|
||||
base_name = project_name if project_name.endswith(".kicad_sch") else f"{project_name}.kicad_sch"
|
||||
schematic = SchematicManager.create_schematic(
|
||||
project_name, path=sch_path, metadata=metadata
|
||||
)
|
||||
base_name = (
|
||||
project_name if project_name.endswith(".kicad_sch") else f"{project_name}.kicad_sch"
|
||||
)
|
||||
normalized_path = path or "."
|
||||
file_path = os.path.join(normalized_path, base_name)
|
||||
success = SchematicManager.save_schematic(schematic, file_path)
|
||||
@@ -2075,6 +2079,13 @@ class KiCADInterface:
|
||||
if not schematic_path:
|
||||
return {"success": False, "message": "schematicPath is required"}
|
||||
|
||||
net_name = params.get("netName")
|
||||
label_type = params.get("labelType")
|
||||
|
||||
_valid_label_types = {"net", "global", "power"}
|
||||
if label_type is not None and label_type not in _valid_label_types:
|
||||
return {"success": False, "message": "labelType must be one of: net, global, power"}
|
||||
|
||||
schematic = SchematicManager.load_schematic(schematic_path)
|
||||
if not schematic:
|
||||
return {"success": False, "message": "Failed to load schematic"}
|
||||
@@ -2137,6 +2148,12 @@ class KiCADInterface:
|
||||
}
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if net_name is not None:
|
||||
labels = [lbl for lbl in labels if lbl["name"] == net_name]
|
||||
if label_type is not None:
|
||||
labels = [lbl for lbl in labels if lbl["type"] == label_type]
|
||||
|
||||
return {"success": True, "labels": labels, "count": len(labels)}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -748,11 +748,24 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
// List all labels in schematic
|
||||
server.tool(
|
||||
"list_schematic_labels",
|
||||
"List all net labels, global labels, and power flags in the schematic.",
|
||||
"List all net labels, global labels, and power flags in the schematic. " +
|
||||
"Optionally filter by label name (netName) and/or label type (labelType).",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||
netName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Filter to labels whose name exactly matches this string (case-sensitive). Omit to return all labels.",
|
||||
),
|
||||
labelType: z
|
||||
.enum(["net", "global", "power"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Filter by label type. 'net' = local label, 'global' = global label, 'power' = power symbol. Omit to return all types.",
|
||||
),
|
||||
},
|
||||
async (args: { schematicPath: string }) => {
|
||||
async (args: { schematicPath: string; netName?: string; labelType?: string }) => {
|
||||
const result = await callKicadScript("list_schematic_labels", args);
|
||||
if (result.success) {
|
||||
const labels = result.labels || [];
|
||||
@@ -1534,22 +1547,15 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
"sheet pin name.",
|
||||
{
|
||||
schematicPath: z.string().describe("Path to the sub-sheet .kicad_sch file"),
|
||||
text: z
|
||||
.string()
|
||||
.describe("Label text (e.g. 'SD_CLK') — must match the sheet pin name"),
|
||||
position: z
|
||||
.array(z.number())
|
||||
.length(2)
|
||||
.describe("Position [x, y] in mm"),
|
||||
text: z.string().describe("Label text (e.g. 'SD_CLK') — must match the sheet pin name"),
|
||||
position: z.array(z.number()).length(2).describe("Position [x, y] in mm"),
|
||||
shape: z
|
||||
.enum(["input", "output", "bidirectional"])
|
||||
.describe("Signal direction from the sub-sheet's perspective"),
|
||||
orientation: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Rotation in degrees: 0=label points right, 180=label points left (default: 0)",
|
||||
),
|
||||
.describe("Rotation in degrees: 0=label points right, 180=label points left (default: 0)"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
@@ -1590,29 +1596,19 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
schematicPath: z.string().describe("Path to the PARENT .kicad_sch file"),
|
||||
sheetName: z
|
||||
.string()
|
||||
.describe(
|
||||
"Sheet name as it appears in the Sheetname property (e.g. 'Storage')",
|
||||
),
|
||||
pinName: z
|
||||
.string()
|
||||
.describe("Pin name — must match a hierarchical_label in the sub-sheet"),
|
||||
.describe("Sheet name as it appears in the Sheetname property (e.g. 'Storage')"),
|
||||
pinName: z.string().describe("Pin name — must match a hierarchical_label in the sub-sheet"),
|
||||
pinType: z
|
||||
.enum(["input", "output", "bidirectional"])
|
||||
.describe(
|
||||
"Signal direction (should match the sub-sheet hierarchical label shape)",
|
||||
),
|
||||
.describe("Signal direction (should match the sub-sheet hierarchical label shape)"),
|
||||
position: z
|
||||
.array(z.number())
|
||||
.length(2)
|
||||
.describe(
|
||||
"Pin position [x, y] in mm — must be on the sheet block boundary",
|
||||
),
|
||||
.describe("Pin position [x, y] in mm — must be on the sheet block boundary"),
|
||||
orientation: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Pin orientation: 0=right edge of sheet box, 180=left edge (default: 0)",
|
||||
),
|
||||
.describe("Pin orientation: 0=right edge of sheet box, 180=left edge (default: 0)"),
|
||||
},
|
||||
async (args: {
|
||||
schematicPath: string;
|
||||
@@ -1629,8 +1625,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
||||
{
|
||||
type: "text" as const,
|
||||
text:
|
||||
result.message ||
|
||||
`Added sheet pin '${args.pinName}' to sheet '${args.sheetName}'`,
|
||||
result.message || `Added sheet pin '${args.pinName}' to sheet '${args.sheetName}'`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
176
tests/test_schematic_labels.py
Normal file
176
tests/test_schematic_labels.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Tests for schematic label filters on list_schematic_labels.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "python"))
|
||||
|
||||
TEMPLATE_PATH = Path(__file__).resolve().parent.parent / "python" / "templates" / "empty.kicad_sch"
|
||||
|
||||
|
||||
def _make_temp_schematic(extra_sexp: str = "") -> Path:
|
||||
"""Copy empty.kicad_sch to a temp file and optionally append S-expression content."""
|
||||
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
||||
shutil.copy(TEMPLATE_PATH, tmp)
|
||||
if extra_sexp:
|
||||
content = tmp.read_text(encoding="utf-8")
|
||||
idx = content.rfind(")")
|
||||
content = content[:idx] + "\n" + extra_sexp + "\n)"
|
||||
tmp.write_text(content, encoding="utf-8")
|
||||
return tmp
|
||||
|
||||
|
||||
def _label_sexp(name: str, x: float, y: float) -> str:
|
||||
return f'(label "{name}" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid "l-{name}-{x}-{y}"))'
|
||||
|
||||
|
||||
def _global_label_sexp(name: str, x: float, y: float) -> str:
|
||||
return f'(global_label "{name}" (at {x} {y} 0) (shape input) (effects (font (size 1.27 1.27))) (uuid "g-{name}-{x}-{y}"))'
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# TestListSchematicLabelsSchema (unit)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestListSchematicLabelsSchema:
|
||||
"""Validate parameter acceptance and rejection for list_schematic_labels."""
|
||||
|
||||
def test_list_schematic_labels_accepts_net_name_param(self) -> None:
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
ki = KiCADInterface()
|
||||
tmp = _make_temp_schematic()
|
||||
result = ki._handle_list_schematic_labels({"schematicPath": str(tmp), "netName": "VCC"})
|
||||
assert result["success"] is True
|
||||
|
||||
def test_list_schematic_labels_accepts_label_type_param(self) -> None:
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
ki = KiCADInterface()
|
||||
tmp = _make_temp_schematic()
|
||||
result = ki._handle_list_schematic_labels({"schematicPath": str(tmp), "labelType": "net"})
|
||||
assert result["success"] is True
|
||||
|
||||
def test_invalid_label_type_rejected(self) -> None:
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
ki = KiCADInterface()
|
||||
tmp = _make_temp_schematic()
|
||||
result = ki._handle_list_schematic_labels({"schematicPath": str(tmp), "labelType": "label"})
|
||||
assert result["success"] is False
|
||||
msg = result["message"]
|
||||
assert "net" in msg
|
||||
assert "global" in msg
|
||||
assert "power" in msg
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# TestListSchematicLabelsFilters (unit)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestListSchematicLabelsFilters:
|
||||
"""Verify filter behaviour of _handle_list_schematic_labels."""
|
||||
|
||||
def _ki(self):
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
return KiCADInterface()
|
||||
|
||||
def test_no_filters_returns_all_labels(self) -> None:
|
||||
extra = _label_sexp("VCC", 10, 10) + "\n" + _global_label_sexp("GND", 20, 20)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels({"schematicPath": str(tmp)})
|
||||
assert result["success"] is True
|
||||
names = {lbl["name"] for lbl in result["labels"]}
|
||||
assert "VCC" in names
|
||||
assert "GND" in names
|
||||
assert result["count"] == len(result["labels"])
|
||||
|
||||
def test_net_name_filter_returns_only_matching(self) -> None:
|
||||
extra = _label_sexp("VCC", 10, 10) + "\n" + _label_sexp("GND", 20, 20)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "netName": "VCC"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert all(lbl["name"] == "VCC" for lbl in result["labels"])
|
||||
assert result["count"] == len(result["labels"])
|
||||
|
||||
def test_net_name_filter_case_sensitive(self) -> None:
|
||||
extra = _label_sexp("VCC", 10, 10)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "netName": "vcc"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 0
|
||||
|
||||
def test_net_name_filter_no_match_returns_empty(self) -> None:
|
||||
extra = _label_sexp("VCC", 10, 10)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "netName": "NONEXISTENT"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert result["labels"] == []
|
||||
assert result["count"] == 0
|
||||
|
||||
def test_label_type_filter_net_only(self) -> None:
|
||||
extra = _label_sexp("SIG", 10, 10) + "\n" + _global_label_sexp("SIG", 20, 20)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "labelType": "net"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert all(lbl["type"] == "net" for lbl in result["labels"])
|
||||
|
||||
def test_label_type_filter_global_only(self) -> None:
|
||||
extra = _label_sexp("SIG", 10, 10) + "\n" + _global_label_sexp("SIG", 20, 20)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "labelType": "global"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert all(lbl["type"] == "global" for lbl in result["labels"])
|
||||
|
||||
def test_label_type_filter_power_only(self) -> None:
|
||||
extra = _label_sexp("VCC", 10, 10)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "labelType": "power"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert all(lbl["type"] == "power" for lbl in result["labels"])
|
||||
|
||||
def test_combined_filters_and_semantics(self) -> None:
|
||||
extra = (
|
||||
_label_sexp("VCC", 10, 10)
|
||||
+ "\n"
|
||||
+ _label_sexp("GND", 20, 20)
|
||||
+ "\n"
|
||||
+ _global_label_sexp("VCC", 30, 30)
|
||||
)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels(
|
||||
{"schematicPath": str(tmp), "netName": "VCC", "labelType": "net"}
|
||||
)
|
||||
assert result["success"] is True
|
||||
assert all(lbl["name"] == "VCC" and lbl["type"] == "net" for lbl in result["labels"])
|
||||
|
||||
def test_absent_filters_backward_compatible(self) -> None:
|
||||
extra = _label_sexp("NET1", 5, 5)
|
||||
tmp = _make_temp_schematic(extra)
|
||||
result = self._ki()._handle_list_schematic_labels({"schematicPath": str(tmp)})
|
||||
assert result["success"] is True
|
||||
assert "labels" in result
|
||||
assert "count" in result
|
||||
Reference in New Issue
Block a user