style: apply Black formatting to all Python files

Add [tool.black] config to pyproject.toml and Black hook to
.pre-commit-config.yaml (rev 26.3.1), then auto-format all Python
source and test files with line-length=100, target-version=py310.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-29 13:01:08 +01:00
parent eee5bfb9ed
commit 75cead0860
53 changed files with 1847 additions and 2394 deletions

View File

@@ -5,6 +5,7 @@ Key regression: the handler previously used a line-by-line regex that required
`(symbol` and `(lib_id` to appear on the *same* line. KiCAD's file writer puts
them on *separate* lines, so every real-world delete returned "not found".
"""
import re
import sys
import tempfile
@@ -17,7 +18,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
TEMPLATE_SCH = Path(__file__).parent.parent / "templates" / "empty.kicad_sch"
# Inline format (single line) matches what tests previously used
PLACED_RESISTOR_INLINE = '''\
PLACED_RESISTOR_INLINE = """\
(symbol (lib_id "Device:R") (at 50 50 0) (unit 1)
(in_bom yes) (on_board yes) (dnp no)
(uuid "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
@@ -34,11 +35,11 @@ PLACED_RESISTOR_INLINE = '''\
(effects (font (size 1.27 1.27)) hide)
)
)
'''
"""
# Multi-line format as KiCAD's own file writer produces it.
# (symbol and (lib_id are on separate lines, which broke the old regex.
PLACED_RESISTOR_MULTILINE = '''\
PLACED_RESISTOR_MULTILINE = """\
\t(symbol
\t\t(lib_id "Device:R")
\t\t(at 50 50 0)
@@ -64,10 +65,10 @@ PLACED_RESISTOR_MULTILINE = '''\
\t\t\t)
\t\t)
\t)
'''
"""
# Multi-line power symbol the exact scenario that was reported as broken.
PLACED_POWER_SYMBOL_MULTILINE = '''\
PLACED_POWER_SYMBOL_MULTILINE = """\
\t(symbol
\t\t(lib_id "power:VCC")
\t\t(at 365.6 38.1 0)
@@ -94,7 +95,7 @@ PLACED_POWER_SYMBOL_MULTILINE = '''\
\t\t\t)
\t\t)
\t)
'''
"""
def _make_test_schematic(tmp_path: Path, extra_block: str = "") -> Path:
@@ -112,6 +113,7 @@ def _make_test_schematic(tmp_path: Path, extra_block: str = "") -> Path:
# Unit tests regression proof for the old regex vs the new approach
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDeleteDetectionRegex:
"""Verify that the new content-string pattern finds blocks in both formats."""
@@ -154,10 +156,12 @@ class TestDeleteDetectionRegex:
# Integration tests real file I/O using the handler
# ---------------------------------------------------------------------------
@pytest.mark.integration
class TestDeleteSchematicComponentIntegration:
def _get_handler(self):
from kicad_interface import KiCADInterface
iface = KiCADInterface.__new__(KiCADInterface)
return iface._handle_delete_schematic_component

View File

@@ -47,9 +47,7 @@ def reset_pcbnew_mock():
@pytest.fixture
def mock_board():
board = MagicMock()
board.GetFileName.return_value = (
"/tmp/test_project/test.kicad_pcb"
)
board.GetFileName.return_value = "/tmp/test_project/test.kicad_pcb"
board.GetTracks.return_value = []
return board
@@ -101,15 +99,14 @@ def _patch_no_runtime():
class TestCheckFreerouting:
def test_no_java_no_docker(self, cmds):
with patch(
"commands.freerouting._find_java", return_value=None
), patch(
"commands.freerouting._docker_available",
return_value=False,
with (
patch("commands.freerouting._find_java", return_value=None),
patch(
"commands.freerouting._docker_available",
return_value=False,
),
):
result = cmds.check_freerouting(
{"freeroutingJar": "/nonexistent.jar"}
)
result = cmds.check_freerouting({"freeroutingJar": "/nonexistent.jar"})
assert result["success"] is True
assert result["java"]["found"] is False
assert result["docker"]["available"] is False
@@ -119,48 +116,46 @@ class TestCheckFreerouting:
def test_java_too_old_docker_available(self, cmds, tmp_path):
jar = tmp_path / "freerouting.jar"
jar.touch()
with patch(
"commands.freerouting._find_java",
return_value="/usr/bin/java",
), patch(
"commands.freerouting._java_version_ok",
return_value=False,
), patch(
"commands.freerouting._docker_available",
return_value=True,
), patch(
"commands.freerouting.subprocess.run"
) as mock_run:
mock_run.return_value = MagicMock(
stderr='openjdk version "17.0.1"', stdout=""
)
result = cmds.check_freerouting(
{"freeroutingJar": str(jar)}
)
with (
patch(
"commands.freerouting._find_java",
return_value="/usr/bin/java",
),
patch(
"commands.freerouting._java_version_ok",
return_value=False,
),
patch(
"commands.freerouting._docker_available",
return_value=True,
),
patch("commands.freerouting.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(stderr='openjdk version "17.0.1"', stdout="")
result = cmds.check_freerouting({"freeroutingJar": str(jar)})
assert result["ready"] is True
assert result["execution_mode"] == "docker"
def test_java_21_direct(self, cmds, tmp_path):
jar = tmp_path / "freerouting.jar"
jar.touch()
with patch(
"commands.freerouting._find_java",
return_value="/usr/bin/java",
), patch(
"commands.freerouting._java_version_ok",
return_value=True,
), patch(
"commands.freerouting._docker_available",
return_value=False,
), patch(
"commands.freerouting.subprocess.run"
) as mock_run:
mock_run.return_value = MagicMock(
stderr='openjdk version "21.0.1"', stdout=""
)
result = cmds.check_freerouting(
{"freeroutingJar": str(jar)}
)
with (
patch(
"commands.freerouting._find_java",
return_value="/usr/bin/java",
),
patch(
"commands.freerouting._java_version_ok",
return_value=True,
),
patch(
"commands.freerouting._docker_available",
return_value=False,
),
patch("commands.freerouting.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(stderr='openjdk version "21.0.1"', stdout="")
result = cmds.check_freerouting({"freeroutingJar": str(jar)})
assert result["ready"] is True
assert result["execution_mode"] == "direct"
@@ -198,9 +193,7 @@ class TestExportDsn:
assert result["path"] == output
def test_export_failure(self, cmds):
pcbnew_mock.ExportSpecctraDSN.side_effect = Exception(
"DSN error"
)
pcbnew_mock.ExportSpecctraDSN.side_effect = Exception("DSN error")
result = cmds.export_dsn({})
assert result["success"] is False
assert "DSN error" in result["errorDetails"]
@@ -213,9 +206,7 @@ class TestExportDsn:
class TestImportSes:
def test_no_board(self, cmds_no_board):
result = cmds_no_board.import_ses(
{"sesPath": "/tmp/test.ses"}
)
result = cmds_no_board.import_ses({"sesPath": "/tmp/test.ses"})
assert result["success"] is False
assert "No board" in result["message"]
@@ -225,9 +216,7 @@ class TestImportSes:
assert "Missing sesPath" in result["message"]
def test_ses_file_not_found(self, cmds):
result = cmds.import_ses(
{"sesPath": "/nonexistent/test.ses"}
)
result = cmds.import_ses({"sesPath": "/nonexistent/test.ses"})
assert result["success"] is False
assert "not found" in result["message"]
@@ -245,9 +234,7 @@ class TestImportSes:
ses_file = tmp_path / "test.ses"
ses_file.write_text("(session test)")
pcbnew_mock.ImportSpecctraSES.side_effect = Exception(
"SES error"
)
pcbnew_mock.ImportSpecctraSES.side_effect = Exception("SES error")
result = cmds.import_ses({"sesPath": str(ses_file)})
assert result["success"] is False
assert "SES error" in result["errorDetails"]
@@ -268,16 +255,12 @@ class TestAutoroute:
jar = tmp_path / "freerouting.jar"
jar.touch()
with _patch_no_runtime():
result = cmds.autoroute(
{"freeroutingJar": str(jar)}
)
result = cmds.autoroute({"freeroutingJar": str(jar)})
assert result["success"] is False
assert "No suitable Java runtime" in result["message"]
def test_no_jar(self, cmds):
result = cmds.autoroute(
{"freeroutingJar": "/nonexistent/freerouting.jar"}
)
result = cmds.autoroute({"freeroutingJar": "/nonexistent/freerouting.jar"})
assert result["success"] is False
assert "JAR not found" in result["message"]
@@ -286,21 +269,15 @@ class TestAutoroute:
jar = tmp_path / "freerouting.jar"
jar.touch()
pcbnew_mock.ExportSpecctraDSN.side_effect = Exception(
"export fail"
)
pcbnew_mock.ExportSpecctraDSN.side_effect = Exception("export fail")
with _patch_direct_java():
result = cmds.autoroute(
{"freeroutingJar": str(jar)}
)
result = cmds.autoroute({"freeroutingJar": str(jar)})
assert result["success"] is False
assert "DSN export failed" in result["message"]
@patch("commands.freerouting.subprocess.run")
def test_freerouting_timeout(
self, mock_run, cmds, tmp_path
):
def test_freerouting_timeout(self, mock_run, cmds, tmp_path):
import subprocess
jar = tmp_path / "freerouting.jar"
@@ -312,24 +289,19 @@ class TestAutoroute:
dsn_file = board_dir / "test.dsn"
cmds.board.GetFileName.return_value = str(board_file)
pcbnew_mock.ExportSpecctraDSN.side_effect = (
lambda b, p: (dsn_file.write_text("(pcb)"), True)[1]
)
mock_run.side_effect = subprocess.TimeoutExpired(
cmd="", timeout=10
)
pcbnew_mock.ExportSpecctraDSN.side_effect = lambda b, p: (
dsn_file.write_text("(pcb)"),
True,
)[1]
mock_run.side_effect = subprocess.TimeoutExpired(cmd="", timeout=10)
with _patch_direct_java():
result = cmds.autoroute(
{"freeroutingJar": str(jar), "timeout": 10}
)
result = cmds.autoroute({"freeroutingJar": str(jar), "timeout": 10})
assert result["success"] is False
assert "timed out" in result["message"]
@patch("commands.freerouting.subprocess.run")
def test_full_success_direct(
self, mock_run, cmds, tmp_path
):
def test_full_success_direct(self, mock_run, cmds, tmp_path):
jar = tmp_path / "freerouting.jar"
jar.touch()
board_dir = tmp_path / "project"
@@ -341,12 +313,11 @@ class TestAutoroute:
cmds.board.GetFileName.return_value = str(board_file)
pcbnew_mock.ExportSpecctraDSN.side_effect = (
lambda b, p: (dsn_file.write_text("(pcb)"), True)[1]
)
mock_run.return_value = MagicMock(
returncode=0, stdout="Routing completed", stderr=""
)
pcbnew_mock.ExportSpecctraDSN.side_effect = lambda b, p: (
dsn_file.write_text("(pcb)"),
True,
)[1]
mock_run.return_value = MagicMock(returncode=0, stdout="Routing completed", stderr="")
ses_file.write_text("(session)")
pcbnew_mock.ImportSpecctraSES.return_value = True
@@ -357,9 +328,7 @@ class TestAutoroute:
cmds.board.GetTracks.return_value = [track, track, via]
with _patch_direct_java():
result = cmds.autoroute(
{"freeroutingJar": str(jar)}
)
result = cmds.autoroute({"freeroutingJar": str(jar)})
assert result["success"] is True
assert result["mode"] == "direct"
@@ -368,9 +337,7 @@ class TestAutoroute:
assert "elapsed_seconds" in result
@patch("commands.freerouting.subprocess.run")
def test_full_success_docker(
self, mock_run, cmds, tmp_path
):
def test_full_success_docker(self, mock_run, cmds, tmp_path):
jar = tmp_path / "freerouting.jar"
jar.touch()
board_dir = tmp_path / "project"
@@ -382,24 +349,24 @@ class TestAutoroute:
cmds.board.GetFileName.return_value = str(board_file)
pcbnew_mock.ExportSpecctraDSN.side_effect = (
lambda b, p: (dsn_file.write_text("(pcb)"), True)[1]
)
mock_run.return_value = MagicMock(
returncode=0, stdout="Routing completed", stderr=""
)
pcbnew_mock.ExportSpecctraDSN.side_effect = lambda b, p: (
dsn_file.write_text("(pcb)"),
True,
)[1]
mock_run.return_value = MagicMock(returncode=0, stdout="Routing completed", stderr="")
ses_file.write_text("(session)")
pcbnew_mock.ImportSpecctraSES.return_value = True
cmds.board.GetTracks.return_value = [MagicMock()]
with _patch_docker_mode(), patch(
"commands.freerouting._find_docker",
return_value="/usr/bin/docker",
with (
_patch_docker_mode(),
patch(
"commands.freerouting._find_docker",
return_value="/usr/bin/docker",
),
):
result = cmds.autoroute(
{"freeroutingJar": str(jar)}
)
result = cmds.autoroute({"freeroutingJar": str(jar)})
assert result["success"] is True
assert result["mode"] == "docker"
@@ -409,9 +376,7 @@ class TestAutoroute:
assert "--rm" in call_args
@patch("commands.freerouting.subprocess.run")
def test_freerouting_nonzero_exit(
self, mock_run, cmds, tmp_path
):
def test_freerouting_nonzero_exit(self, mock_run, cmds, tmp_path):
jar = tmp_path / "freerouting.jar"
jar.touch()
board_dir = tmp_path / "project"
@@ -421,17 +386,14 @@ class TestAutoroute:
dsn_file = board_dir / "test.dsn"
cmds.board.GetFileName.return_value = str(board_file)
pcbnew_mock.ExportSpecctraDSN.side_effect = (
lambda b, p: (dsn_file.write_text("(pcb)"), True)[1]
)
mock_run.return_value = MagicMock(
returncode=1, stdout="", stderr="OutOfMemoryError"
)
pcbnew_mock.ExportSpecctraDSN.side_effect = lambda b, p: (
dsn_file.write_text("(pcb)"),
True,
)[1]
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="OutOfMemoryError")
with _patch_direct_java():
result = cmds.autoroute(
{"freeroutingJar": str(jar)}
)
result = cmds.autoroute({"freeroutingJar": str(jar)})
assert result["success"] is False
assert "exited with code 1" in result["message"]
@@ -451,10 +413,13 @@ class TestFindJava:
assert _find_java() == "/usr/bin/java"
def test_none_when_not_found(self):
with patch(
"commands.freerouting.shutil.which",
return_value=None,
), patch("os.path.isfile", return_value=False):
with (
patch(
"commands.freerouting.shutil.which",
return_value=None,
),
patch("os.path.isfile", return_value=False),
):
assert _find_java() is None
@@ -462,18 +427,14 @@ class TestFindDocker:
def test_finds_docker(self):
with patch(
"commands.freerouting.shutil.which",
side_effect=lambda x: "/usr/bin/docker"
if x == "docker"
else None,
side_effect=lambda x: "/usr/bin/docker" if x == "docker" else None,
):
assert _find_docker() == "/usr/bin/docker"
def test_finds_podman(self):
with patch(
"commands.freerouting.shutil.which",
side_effect=lambda x: "/usr/bin/podman"
if x == "podman"
else None,
side_effect=lambda x: "/usr/bin/podman" if x == "podman" else None,
):
assert _find_docker() == "/usr/bin/podman"
@@ -487,12 +448,13 @@ class TestFindDocker:
class TestDockerAvailable:
def test_docker_found(self):
with patch(
"commands.freerouting._find_docker",
return_value="/usr/bin/docker",
), patch(
"commands.freerouting.subprocess.run"
) as mock_run:
with (
patch(
"commands.freerouting._find_docker",
return_value="/usr/bin/docker",
),
patch("commands.freerouting.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=0)
assert _docker_available() is True
@@ -504,33 +466,26 @@ class TestDockerAvailable:
assert _docker_available() is False
def test_docker_not_running(self):
with patch(
"commands.freerouting._find_docker",
return_value="/usr/bin/docker",
), patch(
"commands.freerouting.subprocess.run"
) as mock_run:
with (
patch(
"commands.freerouting._find_docker",
return_value="/usr/bin/docker",
),
patch("commands.freerouting.subprocess.run") as mock_run,
):
mock_run.return_value = MagicMock(returncode=1)
assert _docker_available() is False
class TestJavaVersionOk:
def test_java_21(self):
with patch(
"commands.freerouting.subprocess.run"
) as mock_run:
mock_run.return_value = MagicMock(
stderr='openjdk version "21.0.1"', stdout=""
)
with patch("commands.freerouting.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stderr='openjdk version "21.0.1"', stdout="")
assert _java_version_ok("/usr/bin/java") is True
def test_java_17(self):
with patch(
"commands.freerouting.subprocess.run"
) as mock_run:
mock_run.return_value = MagicMock(
stderr='openjdk version "17.0.18"', stdout=""
)
with patch("commands.freerouting.subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stderr='openjdk version "17.0.18"', stdout="")
assert _java_version_ok("/usr/bin/java") is False
def test_java_error(self):

View File

@@ -256,18 +256,14 @@ class TestFindOverlappingElements:
def test_overlapping_symbols_detected(self):
# Two resistors at nearly the same position — bboxes fully overlap
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
"R2", 100.1, 100
)
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 100.1, 100)
tmp = _make_temp_schematic(extra)
result = find_overlapping_elements(tmp, tolerance=0.5)
assert result["totalOverlaps"] >= 1
assert len(result["overlappingSymbols"]) >= 1
def test_well_separated_symbols_not_flagged(self):
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
"R2", 200, 200
)
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 200, 200)
tmp = _make_temp_schematic(extra)
result = find_overlapping_elements(tmp, tolerance=0.5)
assert result["totalOverlaps"] == 0
@@ -294,9 +290,7 @@ class TestFindOverlappingElements:
"""
# R1 at y=100, R2 at y=105 — pin spans [96.19, 103.81] and [101.19, 108.81]
# These overlap in Y from 101.19 to 103.81
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
"R2", 100, 105
)
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 100, 105)
tmp = _make_temp_schematic(extra)
result = find_overlapping_elements(tmp, tolerance=0.5)
assert result["totalOverlaps"] >= 1, (
@@ -310,9 +304,7 @@ class TestFindOverlappingElements:
R pins at y ±3.81, but different X positions far enough apart.
"""
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp(
"R2", 110, 100
)
extra = _make_resistor_sexp("R1", 100, 100) + _make_resistor_sexp("R2", 110, 100)
tmp = _make_temp_schematic(extra)
result = find_overlapping_elements(tmp, tolerance=0.5)
assert result["totalOverlaps"] == 0
@@ -419,9 +411,7 @@ class TestIntegrationFindWiresCrossingSymbols:
tmp = _make_temp_schematic(r_at_100 + r_at_200 + wire)
result = find_wires_crossing_symbols(tmp)
# The wire must not be reported against the far-away R? at (200, 100)
collisions_at_200 = [
c for c in result if abs(c["component"]["position"]["x"] - 200) < 0.5
]
collisions_at_200 = [c for c in result if abs(c["component"]["position"]["x"] - 200) < 0.5]
assert len(collisions_at_200) == 0, (
"Wire at x≈100 must not be flagged against the R? at x=200; "
"likely caused by reference-lookup always returning the first 'R?'"
@@ -458,9 +448,7 @@ class TestIntegrationFindWiresCrossingSymbols:
tmp = _make_temp_schematic(extra)
result = find_wires_crossing_symbols(tmp)
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
assert (
len(d1_crossings) == 0
), "Wire terminating at pin from outside should not be flagged"
assert len(d1_crossings) == 0, "Wire terminating at pin from outside should not be flagged"
def test_wire_shorts_component_pins_detected_as_collision(self):
"""Regression: a wire connecting pin1→pin2 of the same component
@@ -825,9 +813,7 @@ class TestComputeSymbolBboxWithGraphics:
}
graphics_points = [(-1.016, -2.54), (1.016, 2.54)]
bbox = _compute_symbol_bbox_direct(
sym, pin_defs, graphics_points=graphics_points
)
bbox = _compute_symbol_bbox_direct(sym, pin_defs, graphics_points=graphics_points)
assert bbox is not None
min_x, min_y, max_x, max_y = bbox
# X should come from rectangle: 100 ± 1.016
@@ -902,9 +888,7 @@ class TestComputeSymbolBboxWithGraphics:
# Rectangle corners in local coords
graphics_points = [(-1.016, -2.54), (1.016, 2.54)]
bbox = _compute_symbol_bbox_direct(
sym, pin_defs, graphics_points=graphics_points
)
bbox = _compute_symbol_bbox_direct(sym, pin_defs, graphics_points=graphics_points)
assert bbox is not None
min_x, min_y, max_x, max_y = bbox
# After 90° rotation, X and Y swap roles
@@ -935,9 +919,7 @@ class TestIntegrationGraphicsBbox:
assert len(graphics_points) >= 2, "Should have extracted rectangle points"
bbox = _compute_symbol_bbox_direct(
r1, pin_defs, graphics_points=graphics_points
)
bbox = _compute_symbol_bbox_direct(r1, pin_defs, graphics_points=graphics_points)
assert bbox is not None
min_x, min_y, max_x, max_y = bbox
# Rectangle is ±1.016 in X, NOT ±1.5 from degenerate expansion

View File

@@ -8,6 +8,7 @@ Covers:
(tested by calling _handle_* methods on a lightweight stub that avoids
importing the full kicad_interface module).
"""
import shutil
import tempfile
from pathlib import Path
@@ -43,9 +44,7 @@ _WIRE_SCH = """\
def _write_temp_sch(content: str) -> Path:
"""Write *content* to a temp file and return its Path."""
tmp = tempfile.NamedTemporaryFile(
suffix=".kicad_sch", delete=False, mode="w", encoding="utf-8"
)
tmp = tempfile.NamedTemporaryFile(suffix=".kicad_sch", delete=False, mode="w", encoding="utf-8")
tmp.write(content)
tmp.close()
return Path(tmp.name)
@@ -66,9 +65,7 @@ class TestDeleteWireUnit:
self.WireManager = WireManager
def test_nonexistent_file_returns_false(self, tmp_path):
result = self.WireManager.delete_wire(
tmp_path / "nope.kicad_sch", [0, 0], [10, 10]
)
result = self.WireManager.delete_wire(tmp_path / "nope.kicad_sch", [0, 0], [10, 10])
assert result is False
def test_no_matching_wire_returns_false(self, tmp_path):
@@ -100,9 +97,7 @@ class TestDeleteLabelUnit:
self.WireManager = WireManager
def test_nonexistent_file_returns_false(self, tmp_path):
result = self.WireManager.delete_label(
tmp_path / "nope.kicad_sch", "VCC"
)
result = self.WireManager.delete_label(tmp_path / "nope.kicad_sch", "VCC")
assert result is False
def test_missing_label_returns_false(self, tmp_path):
@@ -114,9 +109,7 @@ class TestDeleteLabelUnit:
def test_position_kwarg_accepted(self, tmp_path):
sch = tmp_path / "test.kicad_sch"
shutil.copy(EMPTY_SCH, sch)
result = self.WireManager.delete_label(
sch, "VCC", position=[10.0, 20.0], tolerance=0.5
)
result = self.WireManager.delete_label(sch, "VCC", position=[10.0, 20.0], tolerance=0.5)
assert result is False
@@ -145,9 +138,7 @@ class TestDeleteWireIntegration:
wire_items = [
item
for item in data
if isinstance(item, list)
and item
and item[0] == sexpdata.Symbol("wire")
if isinstance(item, list) and item and item[0] == sexpdata.Symbol("wire")
]
assert wire_items == [], "Wire should have been removed from the file"
@@ -165,18 +156,14 @@ class TestDeleteWireIntegration:
sch.write_text(_WIRE_SCH, encoding="utf-8")
# Coordinates differ by 0.3 mm — within default tolerance of 0.5
result = self.WireManager.delete_wire(
sch, [10.3, 20.3], [30.3, 20.3], tolerance=0.5
)
result = self.WireManager.delete_wire(sch, [10.3, 20.3], [30.3, 20.3], tolerance=0.5)
assert result is True
def test_outside_tolerance_no_delete(self, tmp_path):
sch = tmp_path / "test.kicad_sch"
sch.write_text(_WIRE_SCH, encoding="utf-8")
result = self.WireManager.delete_wire(
sch, [10.0, 20.0], [30.0, 20.0], tolerance=0.0
)
result = self.WireManager.delete_wire(sch, [10.0, 20.0], [30.0, 20.0], tolerance=0.0)
# tolerance=0.0 means exact float equality — may still match on most
# platforms, but the key thing is that a *distant* miss is rejected
sch2 = tmp_path / "test2.kicad_sch"
@@ -200,9 +187,7 @@ class TestDeleteWireIntegration:
labels = [
item
for item in data
if isinstance(item, list)
and item
and item[0] == sexpdata.Symbol("label")
if isinstance(item, list) and item and item[0] == sexpdata.Symbol("label")
]
assert len(labels) == 1
@@ -230,9 +215,7 @@ class TestDeleteLabelIntegration:
labels = [
item
for item in data
if isinstance(item, list)
and item
and item[0] == sexpdata.Symbol("label")
if isinstance(item, list) and item and item[0] == sexpdata.Symbol("label")
]
assert labels == [], "Label should have been removed"
@@ -247,9 +230,7 @@ class TestDeleteLabelIntegration:
sch = tmp_path / "test.kicad_sch"
sch.write_text(_WIRE_SCH, encoding="utf-8")
result = self.WireManager.delete_label(
sch, "VCC", position=[99.0, 99.0], tolerance=0.5
)
result = self.WireManager.delete_label(sch, "VCC", position=[99.0, 99.0], tolerance=0.5)
assert result is False
def test_wire_preserved_after_label_deletion(self, tmp_path):
@@ -260,9 +241,7 @@ class TestDeleteLabelIntegration:
wires = [
item
for item in data
if isinstance(item, list)
and item
and item[0] == sexpdata.Symbol("wire")
if isinstance(item, list) and item and item[0] == sexpdata.Symbol("wire")
]
assert len(wires) == 1
@@ -387,9 +366,7 @@ class TestHandlerParamValidation:
params = {}
schematic_path = params.get("schematicPath")
result = (
{"success": False, "message": "schematicPath is required"}
if not schematic_path
else {}
{"success": False, "message": "schematicPath is required"} if not schematic_path else {}
)
assert result["success"] is False

View File

@@ -265,18 +265,14 @@ class TestCoreLogic:
def test_find_connected_wires_no_wire_at_point(self):
wires = [[(0, 0), (10_000, 0)]]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, net_points = _find_connected_wires(
5.0, 0.0, wires, iu_to_wires, adjacency
)
visited, net_points = _find_connected_wires(5.0, 0.0, wires, iu_to_wires, adjacency)
assert visited is None
assert net_points is None
def test_find_connected_wires_single_wire(self):
wires = [[(0, 0), (10_000, 0)]]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, net_points = _find_connected_wires(
0.0, 0.0, wires, iu_to_wires, adjacency
)
visited, net_points = _find_connected_wires(0.0, 0.0, wires, iu_to_wires, adjacency)
assert visited == {0}
assert (0, 0) in net_points
assert (10_000, 0) in net_points
@@ -289,9 +285,7 @@ class TestCoreLogic:
[(20_000, 0), (30_000, 0)],
]
adjacency, iu_to_wires = _build_adjacency(wires)
visited, net_points = _find_connected_wires(
0.0, 0.0, wires, iu_to_wires, adjacency
)
visited, net_points = _find_connected_wires(0.0, 0.0, wires, iu_to_wires, adjacency)
assert visited == {0, 1, 2}
def test_find_connected_wires_does_not_cross_gap(self):

View File

@@ -44,11 +44,7 @@ def _parse_sch(path: Path):
def _find_elements(sch_data, tag: str):
"""Return all top-level S-expression elements with the given tag Symbol."""
return [
item
for item in sch_data
if isinstance(item, list) and item and item[0] == Symbol(tag)
]
return [item for item in sch_data if isinstance(item, list) and item and item[0] == Symbol(tag)]
# ---------------------------------------------------------------------------
@@ -188,9 +184,7 @@ class TestHandleAddSchematicWireValidation:
assert "Schematic path" in result["message"]
def test_missing_waypoints(self):
result = self.iface._handle_add_schematic_wire(
{"schematicPath": "/tmp/x.kicad_sch"}
)
result = self.iface._handle_add_schematic_wire({"schematicPath": "/tmp/x.kicad_sch"})
assert result["success"] is False
assert "waypoint" in result["message"].lower()
@@ -334,9 +328,7 @@ class TestPinSnapping:
def __init__(self):
pass
skip_mod.Schematic = lambda path: type(
"FakeSch", (), {"symbol": [FakeSymbol()]}
)()
skip_mod.Schematic = lambda path: type("FakeSch", (), {"symbol": [FakeSymbol()]})()
mock_pins.return_value = {"1": [10.0, 20.0], "2": [10.0, 30.0]}
@@ -350,9 +342,7 @@ class TestPinSnapping:
with patch.object(KiCADInterface, "__init__", lambda self, *a, **kw: None):
iface = KiCADInterface.__new__(KiCADInterface)
with patch(
"commands.wire_manager.WireManager.add_wire", return_value=True
) as mw:
with patch("commands.wire_manager.WireManager.add_wire", return_value=True) as mw:
result = iface._handle_add_schematic_wire(
{
"schematicPath": str(self.sch_path),
@@ -371,9 +361,10 @@ class TestPinSnapping:
def test_snap_disabled_passes_original_coords(self):
"""With snapToPins=False the handler should not load PinLocator at all."""
with patch(
"commands.wire_manager.WireManager.add_wire", return_value=True
) as mw, patch("commands.pin_locator.PinLocator") as mock_locator_cls:
with (
patch("commands.wire_manager.WireManager.add_wire", return_value=True) as mw,
patch("commands.pin_locator.PinLocator") as mock_locator_cls,
):
result = self.iface._handle_add_schematic_wire(
{
"schematicPath": str(self.sch_path),
@@ -389,9 +380,7 @@ class TestPinSnapping:
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
def test_snap_miss_leaves_coords_unchanged(self, mock_wire):
"""Point beyond tolerance should not be snapped."""
with patch(
"commands.wire_manager.WireManager.add_wire", return_value=True
) as mw:
with patch("commands.wire_manager.WireManager.add_wire", return_value=True) as mw:
result = self.iface._handle_add_schematic_wire(
{
"schematicPath": str(self.sch_path),
@@ -409,9 +398,7 @@ class TestPinSnapping:
def test_intermediate_waypoints_not_snapped(self, mock_poly):
"""Middle waypoints must remain unchanged even with snapToPins=True."""
mid = [50.0, 50.0]
with patch(
"commands.wire_manager.WireManager.add_polyline_wire", return_value=True
) as mp:
with patch("commands.wire_manager.WireManager.add_polyline_wire", return_value=True) as mp:
result = self.iface._handle_add_schematic_wire(
{
"schematicPath": str(self.sch_path),
@@ -451,9 +438,7 @@ class TestHandleAddSchematicJunction:
assert "Schematic path" in result["message"]
def test_missing_position(self):
result = self.iface._handle_add_schematic_junction(
{"schematicPath": "/tmp/x.kicad_sch"}
)
result = self.iface._handle_add_schematic_junction({"schematicPath": "/tmp/x.kicad_sch"})
assert result["success"] is False
assert "Position" in result["message"]
@@ -551,9 +536,7 @@ class TestIntegrationWireManager:
assert ok is True
data = _parse_sch(sch)
wires = _find_elements(data, "wire")
assert (
len(wires) == 3
), f"4 waypoints should produce 3 wire segments, got {len(wires)}"
assert len(wires) == 3, f"4 waypoints should produce 3 wire segments, got {len(wires)}"
def test_add_junction_writes_junction_element(self, sch):
from commands.wire_manager import WireManager
@@ -575,14 +558,10 @@ class TestIntegrationWireManager:
data = _parse_sch(sch)
wire = _find_elements(data, "wire")[0]
pts = [
item
for item in wire
if isinstance(item, list) and item and item[0] == Symbol("pts")
item for item in wire if isinstance(item, list) and item and item[0] == Symbol("pts")
][0]
xy_entries = [
item
for item in pts
if isinstance(item, list) and item and item[0] == Symbol("xy")
item for item in pts if isinstance(item, list) and item and item[0] == Symbol("xy")
]
assert xy_entries[0][1] == 5.0
assert xy_entries[0][2] == 7.5
@@ -644,9 +623,7 @@ class TestIntegrationHandlerEndToEnd:
assert result["success"] is True
data = _parse_sch(self.sch)
wires = _find_elements(data, "wire")
assert (
len(wires) == 3
), f"4 waypoints should produce 3 wire segments, got {len(wires)}"
assert len(wires) == 3, f"4 waypoints should produce 3 wire segments, got {len(wires)}"
# ---------------------------------------------------------------------------
@@ -812,9 +789,7 @@ class TestMakeWireSexp:
def test_custom_stroke(self):
from commands.wire_manager import WireManager
sexp = WireManager._make_wire_sexp(
[0, 0], [5, 0], stroke_width=0.5, stroke_type="dash"
)
sexp = WireManager._make_wire_sexp([0, 0], [5, 0], stroke_width=0.5, stroke_type="dash")
parsed = WireManager._parse_wire(sexp)
assert parsed is not None
_, _, width, stype = parsed
@@ -922,9 +897,7 @@ class TestBreakWiresAtPoint:
data = [Symbol("kicad_sch")]
data.append(
WireManager._make_wire_sexp(
[0, 0], [20, 0], stroke_width=0.5, stroke_type="dash"
)
WireManager._make_wire_sexp([0, 0], [20, 0], stroke_width=0.5, stroke_type="dash")
)
data.append([Symbol("sheet_instances")])
splits = WireManager._break_wires_at_point(data, [10, 0])
@@ -998,9 +971,7 @@ class TestIntegrationTJunction:
WireManager.add_junction(sch, [15, 0])
data = _parse_sch(sch)
wires = _find_elements(data, "wire")
assert (
len(wires) == 2
), f"Expected 2 wires after junction split, got {len(wires)}"
assert len(wires) == 2, f"Expected 2 wires after junction split, got {len(wires)}"
junctions = _find_elements(data, "junction")
assert len(junctions) == 1
@@ -1012,9 +983,7 @@ class TestIntegrationTJunction:
WireManager.add_junction(sch, [20, 0])
data = _parse_sch(sch)
wires = _find_elements(data, "wire")
assert (
len(wires) == 1
), f"Expected 1 wire (no split at endpoint), got {len(wires)}"
assert len(wires) == 1, f"Expected 1 wire (no split at endpoint), got {len(wires)}"
def test_polyline_breaks_existing_wire(self, sch):
"""Polyline whose start/end hits mid-wire should break it."""