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

@@ -23,5 +23,5 @@ Usage:
from kicad_api.factory import create_backend
from kicad_api.base import KiCADBackend
__all__ = ['create_backend', 'KiCADBackend']
__version__ = '2.0.0-alpha.1'
__all__ = ["create_backend", "KiCADBackend"]
__version__ = "2.0.0-alpha.1"

View File

@@ -3,6 +3,7 @@ Abstract base class for KiCAD API backends
Defines the interface that all KiCAD backends must implement.
"""
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, Dict, Any, List
@@ -97,7 +98,7 @@ class KiCADBackend(ABC):
# Board Operations
@abstractmethod
def get_board(self) -> 'BoardAPI':
def get_board(self) -> "BoardAPI":
"""
Get board API for current project
@@ -167,7 +168,7 @@ class BoardAPI(ABC):
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu"
layer: str = "F.Cu",
) -> bool:
"""
Place a component on the board
@@ -194,7 +195,7 @@ class BoardAPI(ABC):
end_y: float,
width: float = 0.25,
layer: str = "F.Cu",
net_name: Optional[str] = None
net_name: Optional[str] = None,
) -> bool:
"""
Add a track (trace) to the board
@@ -220,7 +221,7 @@ class BoardAPI(ABC):
diameter: float = 0.8,
drill: float = 0.4,
net_name: Optional[str] = None,
via_type: str = "through"
via_type: str = "through",
) -> bool:
"""
Add a via to the board
@@ -275,14 +276,17 @@ class BoardAPI(ABC):
class BackendError(Exception):
"""Base exception for backend errors"""
pass
class ConnectionError(BackendError):
"""Raised when connection to KiCAD fails"""
pass
class APINotAvailableError(BackendError):
"""Raised when required API is not available"""
pass

View File

@@ -3,6 +3,7 @@ Backend factory for creating appropriate KiCAD API backend
Auto-detects available backends and provides fallback mechanism.
"""
import os
import logging
from typing import Optional
@@ -34,16 +35,16 @@ def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
"""
# Check environment variable override
if backend_type is None:
backend_type = os.environ.get('KICAD_BACKEND', 'auto').lower()
backend_type = os.environ.get("KICAD_BACKEND", "auto").lower()
logger.info(f"Requested backend: {backend_type}")
# Try specific backend if requested
if backend_type == 'ipc':
if backend_type == "ipc":
return _create_ipc_backend()
elif backend_type == 'swig':
elif backend_type == "swig":
return _create_swig_backend()
elif backend_type == 'auto':
elif backend_type == "auto":
return _auto_detect_backend()
else:
raise ValueError(f"Unknown backend type: {backend_type}")
@@ -61,13 +62,13 @@ def _create_ipc_backend() -> KiCADBackend:
"""
try:
from kicad_api.ipc_backend import IPCBackend
logger.info("Creating IPC backend")
return IPCBackend()
except ImportError as e:
logger.error(f"IPC backend not available: {e}")
raise APINotAvailableError(
"IPC backend requires 'kicad-python' package. "
"Install with: pip install kicad-python"
"IPC backend requires 'kicad-python' package. " "Install with: pip install kicad-python"
) from e
@@ -83,6 +84,7 @@ def _create_swig_backend() -> KiCADBackend:
"""
try:
from kicad_api.swig_backend import SWIGBackend
logger.info("Creating SWIG backend")
logger.warning(
"SWIG backend is DEPRECATED and will be removed in KiCAD 10.0. "
@@ -92,8 +94,7 @@ def _create_swig_backend() -> KiCADBackend:
except ImportError as e:
logger.error(f"SWIG backend not available: {e}")
raise APINotAvailableError(
"SWIG backend requires 'pcbnew' module. "
"Ensure KiCAD Python module is in PYTHONPATH."
"SWIG backend requires 'pcbnew' module. " "Ensure KiCAD Python module is in PYTHONPATH."
) from e
@@ -129,8 +130,7 @@ def _auto_detect_backend() -> KiCADBackend:
try:
backend = _create_swig_backend()
logger.warning(
"Using deprecated SWIG backend. "
"For best results, use IPC API with KiCAD running."
"Using deprecated SWIG backend. " "For best results, use IPC API with KiCAD running."
)
return backend
except (ImportError, APINotAvailableError) as e:
@@ -160,22 +160,18 @@ def get_available_backends() -> dict:
# Check IPC (kicad-python uses 'kipy' module name)
try:
import kipy
results['ipc'] = {
'available': True,
'version': getattr(kipy, '__version__', 'unknown')
}
results["ipc"] = {"available": True, "version": getattr(kipy, "__version__", "unknown")}
except ImportError:
results['ipc'] = {'available': False, 'version': None}
results["ipc"] = {"available": False, "version": None}
# Check SWIG
try:
import pcbnew
results['swig'] = {
'available': True,
'version': pcbnew.GetBuildVersion()
}
results["swig"] = {"available": True, "version": pcbnew.GetBuildVersion()}
except ImportError:
results['swig'] = {'available': False, 'version': None}
results["swig"] = {"available": False, "version": None}
return results
@@ -183,6 +179,7 @@ def get_available_backends() -> dict:
if __name__ == "__main__":
# Quick diagnostic
import json
print("KiCAD Backend Availability:")
print(json.dumps(get_available_backends(), indent=2))

View File

@@ -13,18 +13,14 @@ Key Benefits over SWIG:
- Stable API that won't break between versions
- Multi-language support
"""
import logging
import os
import platform
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable
from kicad_api.base import (
KiCADBackend,
BoardAPI,
ConnectionError,
APINotAvailableError
)
from kicad_api.base import KiCADBackend, BoardAPI, ConnectionError, APINotAvailableError
logger = logging.getLogger(__name__)
@@ -78,10 +74,10 @@ class IPCBackend(KiCADBackend):
# Common socket locations (Unix-like systems only)
# Windows uses named pipes, handled by auto-detect
if platform.system() != "Windows":
socket_paths_to_try.append('ipc:///tmp/kicad/api.sock') # Linux default
socket_paths_to_try.append("ipc:///tmp/kicad/api.sock") # Linux default
# XDG runtime directory (requires getuid, Unix only)
if hasattr(os, 'getuid'):
socket_paths_to_try.append(f'ipc:///run/user/{os.getuid()}/kicad/api.sock')
if hasattr(os, "getuid"):
socket_paths_to_try.append(f"ipc:///run/user/{os.getuid()}/kicad/api.sock")
# Auto-detect for all platforms (Windows uses named pipes, Unix uses sockets)
socket_paths_to_try.append(None)
@@ -117,8 +113,7 @@ class IPCBackend(KiCADBackend):
except ImportError as e:
logger.error("kicad-python library not found")
raise APINotAvailableError(
"IPC backend requires kicad-python. "
"Install with: pip install kicad-python"
"IPC backend requires kicad-python. " "Install with: pip install kicad-python"
) from e
except Exception as e:
logger.error(f"Failed to connect via IPC: {e}")
@@ -190,7 +185,7 @@ class IPCBackend(KiCADBackend):
return {
"success": False,
"message": "Direct project creation not supported via IPC",
"suggestion": "Open KiCAD and create a new project, or use SWIG backend"
"suggestion": "Open KiCAD and create a new project, or use SWIG backend",
}
def open_project(self, path: Path) -> Dict[str, Any]:
@@ -209,22 +204,18 @@ class IPCBackend(KiCADBackend):
return {
"success": True,
"message": f"Project already open: {path}",
"path": str(path)
"path": str(path),
}
return {
"success": False,
"message": "Project not currently open in KiCAD",
"suggestion": "Open the project in KiCAD first, then connect via IPC"
"suggestion": "Open the project in KiCAD first, then connect via IPC",
}
except Exception as e:
logger.error(f"Failed to check project: {e}")
return {
"success": False,
"message": "Failed to check project",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to check project", "errorDetails": str(e)}
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
"""Save current project via IPC."""
@@ -240,17 +231,10 @@ class IPCBackend(KiCADBackend):
self._notify_change("save", {"path": str(path) if path else "current"})
return {
"success": True,
"message": "Project saved successfully"
}
return {"success": True, "message": "Project saved successfully"}
except Exception as e:
logger.error(f"Failed to save project: {e}")
return {
"success": False,
"message": "Failed to save project",
"errorDetails": str(e)
}
return {"success": False, "message": "Failed to save project", "errorDetails": str(e)}
def close_project(self) -> None:
"""Close current project (not supported via IPC)."""
@@ -380,8 +364,8 @@ class IPCBoardAPI(BoardAPI):
# Find bounding box of edge cuts
from kipy.util.units import to_mm
min_x = min_y = float('inf')
max_x = max_y = float('-inf')
min_x = min_y = float("inf")
max_x = max_y = float("-inf")
for shape in shapes:
# Check if on Edge.Cuts layer
@@ -392,14 +376,10 @@ class IPCBoardAPI(BoardAPI):
max_x = max(max_x, bbox.max.x)
max_y = max(max_y, bbox.max.y)
if min_x == float('inf'):
if min_x == float("inf"):
return {"width": 0, "height": 0, "unit": "mm"}
return {
"width": to_mm(max_x - min_x),
"height": to_mm(max_y - min_y),
"unit": "mm"
}
return {"width": to_mm(max_x - min_x), "height": to_mm(max_y - min_y), "unit": "mm"}
except Exception as e:
logger.error(f"Failed to get board size: {e}")
@@ -432,19 +412,23 @@ class IPCBoardAPI(BoardAPI):
for fp in footprints:
try:
pos = fp.position
components.append({
"reference": fp.reference_field.text.value if fp.reference_field else "",
"value": fp.value_field.text.value if fp.value_field else "",
"footprint": str(fp.definition.library_link) if fp.definition else "",
"position": {
"x": to_mm(pos.x) if pos else 0,
"y": to_mm(pos.y) if pos else 0,
"unit": "mm"
},
"rotation": fp.orientation.degrees if fp.orientation else 0,
"layer": str(fp.layer) if hasattr(fp, 'layer') else "F.Cu",
"id": str(fp.id) if hasattr(fp, 'id') else ""
})
components.append(
{
"reference": (
fp.reference_field.text.value if fp.reference_field else ""
),
"value": fp.value_field.text.value if fp.value_field else "",
"footprint": str(fp.definition.library_link) if fp.definition else "",
"position": {
"x": to_mm(pos.x) if pos else 0,
"y": to_mm(pos.y) if pos else 0,
"unit": "mm",
},
"rotation": fp.orientation.degrees if fp.orientation else 0,
"layer": str(fp.layer) if hasattr(fp, "layer") else "F.Cu",
"id": str(fp.id) if hasattr(fp, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing footprint: {e}")
continue
@@ -463,7 +447,7 @@ class IPCBoardAPI(BoardAPI):
y: float,
rotation: float = 0,
layer: str = "F.Cu",
value: str = ""
value: str = "",
) -> bool:
"""
Place a component on the board.
@@ -495,7 +479,9 @@ class IPCBoardAPI(BoardAPI):
)
else:
# Fallback: Create a basic placeholder footprint via IPC
logger.warning(f"Could not load footprint '{footprint}' from library, creating placeholder")
logger.warning(
f"Could not load footprint '{footprint}' from library, creating placeholder"
)
return self._place_placeholder_footprint(
reference, footprint, x, y, rotation, layer, value
)
@@ -518,8 +504,8 @@ class IPCBoardAPI(BoardAPI):
import pcbnew
# Parse library and footprint name
if ':' in footprint_path:
lib_name, fp_name = footprint_path.split(':', 1)
if ":" in footprint_path:
lib_name, fp_name = footprint_path.split(":", 1)
else:
# Try to find the footprint in all libraries
lib_name = None
@@ -560,14 +546,7 @@ class IPCBoardAPI(BoardAPI):
return None
def _place_loaded_footprint(
self,
loaded_fp,
reference: str,
x: float,
y: float,
rotation: float,
layer: str,
value: str
self, loaded_fp, reference: str, x: float, y: float, rotation: float, layer: str, value: str
) -> bool:
"""
Place a loaded pcbnew footprint onto the board.
@@ -589,7 +568,7 @@ class IPCBoardAPI(BoardAPI):
try:
docs = self._kicad.get_open_documents()
for doc in docs:
if hasattr(doc, 'path') and str(doc.path).endswith('.kicad_pcb'):
if hasattr(doc, "path") and str(doc.path).endswith(".kicad_pcb"):
board_path = str(doc.path)
break
except Exception as e:
@@ -637,24 +616,27 @@ class IPCBoardAPI(BoardAPI):
except Exception as e:
logger.debug(f"Could not refresh IPC board: {e}")
self._notify("component_placed", {
"reference": reference,
"footprint": loaded_fp.GetFPIDAsString(),
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": True
})
self._notify(
"component_placed",
{
"reference": reference,
"footprint": loaded_fp.GetFPIDAsString(),
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": True,
},
)
logger.info(f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm")
logger.info(
f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm"
)
return True
except Exception as e:
logger.error(f"Error placing loaded footprint: {e}")
# Fall back to placeholder
return self._place_placeholder_footprint(
reference, "", x, y, rotation, layer, value
)
return self._place_placeholder_footprint(reference, "", x, y, rotation, layer, value)
def _place_placeholder_footprint(
self,
@@ -664,7 +646,7 @@ class IPCBoardAPI(BoardAPI):
y: float,
rotation: float,
layer: str,
value: str
value: str,
) -> bool:
"""
Place a placeholder footprint when library loading fails.
@@ -701,15 +683,18 @@ class IPCBoardAPI(BoardAPI):
board.create_items(fp)
board.push_commit(commit, f"Placed component {reference}")
self._notify("component_placed", {
"reference": reference,
"footprint": footprint,
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": False,
"is_placeholder": True
})
self._notify(
"component_placed",
{
"reference": reference,
"footprint": footprint,
"position": {"x": x, "y": y},
"rotation": rotation,
"layer": layer,
"loaded_from_library": False,
"is_placeholder": True,
},
)
logger.info(f"Placed placeholder component {reference} at ({x}, {y}) mm")
return True
@@ -718,7 +703,9 @@ class IPCBoardAPI(BoardAPI):
logger.error(f"Failed to place placeholder component: {e}")
return False
def move_component(self, reference: str, x: float, y: float, rotation: Optional[float] = None) -> bool:
def move_component(
self, reference: str, x: float, y: float, rotation: Optional[float] = None
) -> bool:
"""Move a component to a new position (updates UI immediately)."""
try:
from kipy.geometry import Vector2, Angle
@@ -749,11 +736,10 @@ class IPCBoardAPI(BoardAPI):
board.update_items([target_fp])
board.push_commit(commit, f"Moved component {reference}")
self._notify("component_moved", {
"reference": reference,
"position": {"x": x, "y": y},
"rotation": rotation
})
self._notify(
"component_moved",
{"reference": reference, "position": {"x": x, "y": y}, "rotation": rotation},
)
return True
@@ -799,7 +785,7 @@ class IPCBoardAPI(BoardAPI):
end_y: float,
width: float = 0.25,
layer: str = "F.Cu",
net_name: Optional[str] = None
net_name: Optional[str] = None,
) -> bool:
"""
Add a track (trace) to the board.
@@ -842,13 +828,16 @@ class IPCBoardAPI(BoardAPI):
board.create_items(track)
board.push_commit(commit, "Added track")
self._notify("track_added", {
"start": {"x": start_x, "y": start_y},
"end": {"x": end_x, "y": end_y},
"width": width,
"layer": layer,
"net": net_name
})
self._notify(
"track_added",
{
"start": {"x": start_x, "y": start_y},
"end": {"x": end_x, "y": end_y},
"width": width,
"layer": layer,
"net": net_name,
},
)
logger.info(f"Added track from ({start_x}, {start_y}) to ({end_x}, {end_y}) mm")
return True
@@ -864,7 +853,7 @@ class IPCBoardAPI(BoardAPI):
diameter: float = 0.8,
drill: float = 0.4,
net_name: Optional[str] = None,
via_type: str = "through"
via_type: str = "through",
) -> bool:
"""
Add a via to the board.
@@ -906,13 +895,16 @@ class IPCBoardAPI(BoardAPI):
board.create_items(via)
board.push_commit(commit, "Added via")
self._notify("via_added", {
"position": {"x": x, "y": y},
"diameter": diameter,
"drill": drill,
"net": net_name,
"type": via_type
})
self._notify(
"via_added",
{
"position": {"x": x, "y": y},
"diameter": diameter,
"drill": drill,
"net": net_name,
"type": via_type,
},
)
logger.info(f"Added via at ({x}, {y}) mm")
return True
@@ -928,7 +920,7 @@ class IPCBoardAPI(BoardAPI):
y: float,
layer: str = "F.SilkS",
size: float = 1.0,
rotation: float = 0
rotation: float = 0,
) -> bool:
"""Add text to the board."""
try:
@@ -959,11 +951,7 @@ class IPCBoardAPI(BoardAPI):
board.create_items(board_text)
board.push_commit(commit, f"Added text: {text}")
self._notify("text_added", {
"text": text,
"position": {"x": x, "y": y},
"layer": layer
})
self._notify("text_added", {"text": text, "position": {"x": x, "y": y}, "layer": layer})
return True
@@ -982,20 +970,16 @@ class IPCBoardAPI(BoardAPI):
result = []
for track in tracks:
try:
result.append({
"start": {
"x": to_mm(track.start.x),
"y": to_mm(track.start.y)
},
"end": {
"x": to_mm(track.end.x),
"y": to_mm(track.end.y)
},
"width": to_mm(track.width),
"layer": str(track.layer),
"net": track.net.name if track.net else "",
"id": str(track.id) if hasattr(track, 'id') else ""
})
result.append(
{
"start": {"x": to_mm(track.start.x), "y": to_mm(track.start.y)},
"end": {"x": to_mm(track.end.x), "y": to_mm(track.end.y)},
"width": to_mm(track.width),
"layer": str(track.layer),
"net": track.net.name if track.net else "",
"id": str(track.id) if hasattr(track, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing track: {e}")
continue
@@ -1017,17 +1001,16 @@ class IPCBoardAPI(BoardAPI):
result = []
for via in vias:
try:
result.append({
"position": {
"x": to_mm(via.position.x),
"y": to_mm(via.position.y)
},
"diameter": to_mm(via.diameter),
"drill": to_mm(via.drill_diameter),
"net": via.net.name if via.net else "",
"type": str(via.type),
"id": str(via.id) if hasattr(via, 'id') else ""
})
result.append(
{
"position": {"x": to_mm(via.position.x), "y": to_mm(via.position.y)},
"diameter": to_mm(via.diameter),
"drill": to_mm(via.drill_diameter),
"net": via.net.name if via.net else "",
"type": str(via.type),
"id": str(via.id) if hasattr(via, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing via: {e}")
continue
@@ -1047,10 +1030,9 @@ class IPCBoardAPI(BoardAPI):
result = []
for net in nets:
try:
result.append({
"name": net.name,
"code": net.code if hasattr(net, 'code') else 0
})
result.append(
{"name": net.name, "code": net.code if hasattr(net, "code") else 0}
)
except Exception as e:
logger.warning(f"Error processing net: {e}")
continue
@@ -1070,7 +1052,7 @@ class IPCBoardAPI(BoardAPI):
min_thickness: float = 0.25,
priority: int = 0,
fill_mode: str = "solid",
name: str = ""
name: str = "",
) -> bool:
"""
Add a copper pour zone to the board.
@@ -1157,12 +1139,10 @@ class IPCBoardAPI(BoardAPI):
board.create_items(zone)
board.push_commit(commit, f"Added copper zone on {layer}")
self._notify("zone_added", {
"layer": layer,
"net": net_name,
"points": len(points),
"priority": priority
})
self._notify(
"zone_added",
{"layer": layer, "net": net_name, "points": len(points), "priority": priority},
)
logger.info(f"Added zone on {layer} with {len(points)} points")
return True
@@ -1182,14 +1162,18 @@ class IPCBoardAPI(BoardAPI):
result = []
for zone in zones:
try:
result.append({
"name": zone.name if hasattr(zone, 'name') else "",
"net": zone.net.name if zone.net else "",
"priority": zone.priority if hasattr(zone, 'priority') else 0,
"layers": [str(l) for l in zone.layers] if hasattr(zone, 'layers') else [],
"filled": zone.filled if hasattr(zone, 'filled') else False,
"id": str(zone.id) if hasattr(zone, 'id') else ""
})
result.append(
{
"name": zone.name if hasattr(zone, "name") else "",
"net": zone.net.name if zone.net else "",
"priority": zone.priority if hasattr(zone, "priority") else 0,
"layers": (
[str(l) for l in zone.layers] if hasattr(zone, "layers") else []
),
"filled": zone.filled if hasattr(zone, "filled") else False,
"id": str(zone.id) if hasattr(zone, "id") else "",
}
)
except Exception as e:
logger.warning(f"Error processing zone: {e}")
continue
@@ -1219,10 +1203,9 @@ class IPCBoardAPI(BoardAPI):
result = []
for item in selection:
result.append({
"type": type(item).__name__,
"id": str(item.id) if hasattr(item, 'id') else ""
})
result.append(
{"type": type(item).__name__, "id": str(item.id) if hasattr(item, "id") else ""}
)
return result
except Exception as e:
@@ -1241,4 +1224,4 @@ class IPCBoardAPI(BoardAPI):
# Export for factory
__all__ = ['IPCBackend', 'IPCBoardAPI']
__all__ = ["IPCBackend", "IPCBoardAPI"]

View File

@@ -8,16 +8,12 @@ WARNING: SWIG bindings are deprecated as of KiCAD 9.0
and will be removed in KiCAD 10.0.
Please migrate to IPC backend.
"""
import logging
from pathlib import Path
from typing import Optional, Dict, Any, List
from kicad_api.base import (
KiCADBackend,
BoardAPI,
ConnectionError,
APINotAvailableError
)
from kicad_api.base import KiCADBackend, BoardAPI, ConnectionError, APINotAvailableError
logger = logging.getLogger(__name__)
@@ -48,6 +44,7 @@ class SWIGBackend(KiCADBackend):
"""
try:
import pcbnew
self._pcbnew = pcbnew
version = pcbnew.GetBuildVersion()
logger.info(f"✓ Connected to pcbnew (SWIG): {version}")
@@ -191,7 +188,7 @@ class SWIGBoardAPI(BoardAPI):
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu"
layer: str = "F.Cu",
) -> bool:
"""Place component using existing implementation"""
from commands.component import ComponentCommands
@@ -202,7 +199,7 @@ class SWIGBoardAPI(BoardAPI):
position={"x": x, "y": y, "unit": "mm"},
reference=reference,
rotation=rotation,
layer=layer
layer=layer,
)
return result.get("success", False)
except Exception as e: