chore: normalize all tracked files to LF line endings

Mechanical application of the `.gitattributes` rules from the prior commit.
All 50 files differ only in line endings — verified by
`git diff --cached --ignore-all-space` being empty.

Before: main had 42 CRLF + 27 LF Python files plus mixed-ending in YAML,
templates, and shell scripts. After: every text file is LF (except the
Windows-native *.ps1, *.bat scripts which remain CRLF per gitattributes).

This eliminates the noisy-diff failure mode seen in PR #102, where a
small logic change produced a 918-line diff due to whole-file CRLF→LF
conversion.
This commit is contained in:
Eugene Mikhantyev
2026-04-18 15:23:00 +01:00
parent 2c35ff40a7
commit bfc25639c2
50 changed files with 18167 additions and 18167 deletions

View File

@@ -1,27 +1,27 @@
"""
KiCAD API Abstraction Layer
This module provides a unified interface to KiCAD's Python APIs,
supporting both the legacy SWIG bindings and the new IPC API.
Usage:
from kicad_api import create_backend
# Auto-detect best available backend
backend = create_backend()
# Or specify explicitly
backend = create_backend('ipc') # Use IPC API
backend = create_backend('swig') # Use legacy SWIG
# Connect and use
if backend.connect():
board = backend.get_board()
board.set_size(100, 80)
"""
from kicad_api.base import KiCADBackend
from kicad_api.factory import create_backend
__all__ = ["create_backend", "KiCADBackend"]
__version__ = "2.0.0-alpha.1"
"""
KiCAD API Abstraction Layer
This module provides a unified interface to KiCAD's Python APIs,
supporting both the legacy SWIG bindings and the new IPC API.
Usage:
from kicad_api import create_backend
# Auto-detect best available backend
backend = create_backend()
# Or specify explicitly
backend = create_backend('ipc') # Use IPC API
backend = create_backend('swig') # Use legacy SWIG
# Connect and use
if backend.connect():
board = backend.get_board()
board.set_size(100, 80)
"""
from kicad_api.base import KiCADBackend
from kicad_api.factory import create_backend
__all__ = ["create_backend", "KiCADBackend"]
__version__ = "2.0.0-alpha.1"

View File

@@ -1,293 +1,293 @@
"""
Abstract base class for KiCAD API backends
Defines the interface that all KiCAD backends must implement.
"""
import logging
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
class KiCADBackend(ABC):
"""Abstract base class for KiCAD API backends"""
@abstractmethod
def connect(self) -> bool:
"""
Connect to KiCAD
Returns:
True if connection successful, False otherwise
"""
pass
@abstractmethod
def disconnect(self) -> None:
"""Disconnect from KiCAD and clean up resources"""
pass
@abstractmethod
def is_connected(self) -> bool:
"""
Check if currently connected to KiCAD
Returns:
True if connected, False otherwise
"""
pass
@abstractmethod
def get_version(self) -> str:
"""
Get KiCAD version
Returns:
Version string (e.g., "9.0.0")
"""
pass
# Project Operations
@abstractmethod
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
"""
Create a new KiCAD project
Args:
path: Directory path for the project
name: Project name
Returns:
Dictionary with project info
"""
pass
@abstractmethod
def open_project(self, path: Path) -> Dict[str, Any]:
"""
Open an existing KiCAD project
Args:
path: Path to .kicad_pro file
Returns:
Dictionary with project info
"""
pass
@abstractmethod
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
"""
Save the current project
Args:
path: Optional new path to save to
Returns:
Dictionary with save status
"""
pass
@abstractmethod
def close_project(self) -> None:
"""Close the current project"""
pass
# Board Operations
@abstractmethod
def get_board(self) -> "BoardAPI":
"""
Get board API for current project
Returns:
BoardAPI instance
"""
pass
class BoardAPI(ABC):
"""Abstract interface for board operations"""
@abstractmethod
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
"""
Set board size
Args:
width: Board width
height: Board height
unit: Unit of measurement ("mm" or "in")
Returns:
True if successful
"""
pass
@abstractmethod
def get_size(self) -> Dict[str, Any]:
"""
Get current board size
Returns:
Dictionary with width, height, unit
"""
pass
@abstractmethod
def add_layer(self, layer_name: str, layer_type: str) -> bool:
"""
Add a layer to the board
Args:
layer_name: Name of the layer
layer_type: Type ("copper", "technical", "user")
Returns:
True if successful
"""
pass
@abstractmethod
def list_components(self) -> List[Dict[str, Any]]:
"""
List all components on the board
Returns:
List of component dictionaries
"""
pass
@abstractmethod
def place_component(
self,
reference: str,
footprint: str,
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu",
value: str = "",
) -> bool:
"""
Place a component on the board
Args:
reference: Component reference (e.g., "R1")
footprint: Footprint library path
x: X position (mm)
y: Y position (mm)
rotation: Rotation angle (degrees)
layer: Layer name
Returns:
True if successful
"""
pass
# Routing Operations
def add_track(
self,
start_x: float,
start_y: float,
end_x: float,
end_y: float,
width: float = 0.25,
layer: str = "F.Cu",
net_name: Optional[str] = None,
) -> bool:
"""
Add a track (trace) to the board
Args:
start_x: Start X position (mm)
start_y: Start Y position (mm)
end_x: End X position (mm)
end_y: End Y position (mm)
width: Track width (mm)
layer: Layer name
net_name: Optional net name
Returns:
True if successful
"""
raise NotImplementedError()
def add_via(
self,
x: float,
y: float,
diameter: float = 0.8,
drill: float = 0.4,
net_name: Optional[str] = None,
via_type: str = "through",
) -> bool:
"""
Add a via to the board
Args:
x: X position (mm)
y: Y position (mm)
diameter: Via diameter (mm)
drill: Drill diameter (mm)
net_name: Optional net name
via_type: Via type ("through", "blind", "micro")
Returns:
True if successful
"""
raise NotImplementedError()
# Transaction support for undo/redo
def begin_transaction(self, description: str = "MCP Operation") -> None:
"""Begin a transaction for grouping operations."""
pass # Optional - not all backends support this
def commit_transaction(self, description: str = "MCP Operation") -> None:
"""Commit the current transaction."""
pass # Optional
def rollback_transaction(self) -> None:
"""Roll back the current transaction."""
pass # Optional
def save(self) -> bool:
"""Save the board."""
raise NotImplementedError()
# Query operations
def get_tracks(self) -> List[Dict[str, Any]]:
"""Get all tracks on the board."""
raise NotImplementedError()
def get_vias(self) -> List[Dict[str, Any]]:
"""Get all vias on the board."""
raise NotImplementedError()
def get_nets(self) -> List[Dict[str, Any]]:
"""Get all nets on the board."""
raise NotImplementedError()
def get_selection(self) -> List[Dict[str, Any]]:
"""Get currently selected items."""
raise NotImplementedError()
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
"""
Abstract base class for KiCAD API backends
Defines the interface that all KiCAD backends must implement.
"""
import logging
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
class KiCADBackend(ABC):
"""Abstract base class for KiCAD API backends"""
@abstractmethod
def connect(self) -> bool:
"""
Connect to KiCAD
Returns:
True if connection successful, False otherwise
"""
pass
@abstractmethod
def disconnect(self) -> None:
"""Disconnect from KiCAD and clean up resources"""
pass
@abstractmethod
def is_connected(self) -> bool:
"""
Check if currently connected to KiCAD
Returns:
True if connected, False otherwise
"""
pass
@abstractmethod
def get_version(self) -> str:
"""
Get KiCAD version
Returns:
Version string (e.g., "9.0.0")
"""
pass
# Project Operations
@abstractmethod
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
"""
Create a new KiCAD project
Args:
path: Directory path for the project
name: Project name
Returns:
Dictionary with project info
"""
pass
@abstractmethod
def open_project(self, path: Path) -> Dict[str, Any]:
"""
Open an existing KiCAD project
Args:
path: Path to .kicad_pro file
Returns:
Dictionary with project info
"""
pass
@abstractmethod
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
"""
Save the current project
Args:
path: Optional new path to save to
Returns:
Dictionary with save status
"""
pass
@abstractmethod
def close_project(self) -> None:
"""Close the current project"""
pass
# Board Operations
@abstractmethod
def get_board(self) -> "BoardAPI":
"""
Get board API for current project
Returns:
BoardAPI instance
"""
pass
class BoardAPI(ABC):
"""Abstract interface for board operations"""
@abstractmethod
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
"""
Set board size
Args:
width: Board width
height: Board height
unit: Unit of measurement ("mm" or "in")
Returns:
True if successful
"""
pass
@abstractmethod
def get_size(self) -> Dict[str, Any]:
"""
Get current board size
Returns:
Dictionary with width, height, unit
"""
pass
@abstractmethod
def add_layer(self, layer_name: str, layer_type: str) -> bool:
"""
Add a layer to the board
Args:
layer_name: Name of the layer
layer_type: Type ("copper", "technical", "user")
Returns:
True if successful
"""
pass
@abstractmethod
def list_components(self) -> List[Dict[str, Any]]:
"""
List all components on the board
Returns:
List of component dictionaries
"""
pass
@abstractmethod
def place_component(
self,
reference: str,
footprint: str,
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu",
value: str = "",
) -> bool:
"""
Place a component on the board
Args:
reference: Component reference (e.g., "R1")
footprint: Footprint library path
x: X position (mm)
y: Y position (mm)
rotation: Rotation angle (degrees)
layer: Layer name
Returns:
True if successful
"""
pass
# Routing Operations
def add_track(
self,
start_x: float,
start_y: float,
end_x: float,
end_y: float,
width: float = 0.25,
layer: str = "F.Cu",
net_name: Optional[str] = None,
) -> bool:
"""
Add a track (trace) to the board
Args:
start_x: Start X position (mm)
start_y: Start Y position (mm)
end_x: End X position (mm)
end_y: End Y position (mm)
width: Track width (mm)
layer: Layer name
net_name: Optional net name
Returns:
True if successful
"""
raise NotImplementedError()
def add_via(
self,
x: float,
y: float,
diameter: float = 0.8,
drill: float = 0.4,
net_name: Optional[str] = None,
via_type: str = "through",
) -> bool:
"""
Add a via to the board
Args:
x: X position (mm)
y: Y position (mm)
diameter: Via diameter (mm)
drill: Drill diameter (mm)
net_name: Optional net name
via_type: Via type ("through", "blind", "micro")
Returns:
True if successful
"""
raise NotImplementedError()
# Transaction support for undo/redo
def begin_transaction(self, description: str = "MCP Operation") -> None:
"""Begin a transaction for grouping operations."""
pass # Optional - not all backends support this
def commit_transaction(self, description: str = "MCP Operation") -> None:
"""Commit the current transaction."""
pass # Optional
def rollback_transaction(self) -> None:
"""Roll back the current transaction."""
pass # Optional
def save(self) -> bool:
"""Save the board."""
raise NotImplementedError()
# Query operations
def get_tracks(self) -> List[Dict[str, Any]]:
"""Get all tracks on the board."""
raise NotImplementedError()
def get_vias(self) -> List[Dict[str, Any]]:
"""Get all vias on the board."""
raise NotImplementedError()
def get_nets(self) -> List[Dict[str, Any]]:
"""Get all nets on the board."""
raise NotImplementedError()
def get_selection(self) -> List[Dict[str, Any]]:
"""Get currently selected items."""
raise NotImplementedError()
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

@@ -1,195 +1,195 @@
"""
Backend factory for creating appropriate KiCAD API backend
Auto-detects available backends and provides fallback mechanism.
"""
import logging
import os
from pathlib import Path
from typing import Optional
from kicad_api.base import APINotAvailableError, KiCADBackend
logger = logging.getLogger(__name__)
def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
"""
Create appropriate KiCAD backend
Args:
backend_type: Backend to use:
- 'ipc': Use IPC API (recommended)
- 'swig': Use legacy SWIG bindings
- None or 'auto': Auto-detect (try IPC first, fall back to SWIG)
Returns:
KiCADBackend instance
Raises:
APINotAvailableError: If no backend is available
Environment Variables:
KICAD_BACKEND: Override backend selection ('ipc', 'swig', or 'auto')
"""
# Check environment variable override
if backend_type is None:
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":
return _create_ipc_backend()
elif backend_type == "swig":
return _create_swig_backend()
elif backend_type == "auto":
return _auto_detect_backend()
else:
raise ValueError(f"Unknown backend type: {backend_type}")
def _create_ipc_backend() -> KiCADBackend:
"""
Create IPC backend
Returns:
IPCBackend instance
Raises:
APINotAvailableError: If kicad-python not available
"""
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"
) from e
def _create_swig_backend() -> KiCADBackend:
"""
Create SWIG backend
Returns:
SWIGBackend instance
Raises:
APINotAvailableError: If pcbnew not available
"""
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. "
"Please migrate to IPC backend."
)
return SWIGBackend()
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."
) from e
def _auto_detect_backend() -> KiCADBackend:
"""
Auto-detect best available backend
Priority:
1. IPC API (if kicad-python available and KiCAD running)
2. SWIG API (if pcbnew available)
Returns:
Best available KiCADBackend
Raises:
APINotAvailableError: If no backend available
"""
logger.info("Auto-detecting available KiCAD backend...")
# Try IPC first (preferred)
try:
backend = _create_ipc_backend()
# Test connection
if backend.connect():
logger.info("✓ IPC backend available and connected")
return backend
else:
logger.warning("IPC backend available but connection failed")
except (ImportError, APINotAvailableError) as e:
logger.debug(f"IPC backend not available: {e}")
# Fall back to SWIG
try:
backend = _create_swig_backend()
logger.warning(
"Using deprecated SWIG backend. " "For best results, use IPC API with KiCAD running."
)
return backend
except (ImportError, APINotAvailableError) as e:
logger.error(f"SWIG backend not available: {e}")
# No backend available
raise APINotAvailableError(
"No KiCAD backend available. Please install either:\n"
" - kicad-python (recommended): pip install kicad-python\n"
" - Ensure KiCAD Python module (pcbnew) is in PYTHONPATH"
)
def get_available_backends() -> dict:
"""
Check which backends are available
Returns:
Dictionary with backend availability:
{
'ipc': {'available': bool, 'version': str or None},
'swig': {'available': bool, 'version': str or None}
}
"""
results = {}
# Check IPC (kicad-python uses 'kipy' module name)
try:
import kipy
results["ipc"] = {"available": True, "version": getattr(kipy, "__version__", "unknown")}
except ImportError:
results["ipc"] = {"available": False, "version": None}
# Check SWIG
try:
import pcbnew
results["swig"] = {"available": True, "version": pcbnew.GetBuildVersion()}
except ImportError:
results["swig"] = {"available": False, "version": None}
return results
if __name__ == "__main__":
# Quick diagnostic
import json
print("KiCAD Backend Availability:")
print(json.dumps(get_available_backends(), indent=2))
print("\nAttempting to create backend...")
try:
backend = create_backend()
print(f"✓ Created backend: {type(backend).__name__}")
if backend.connect():
print(f"✓ Connected to KiCAD: {backend.get_version()}")
else:
print("✗ Failed to connect to KiCAD")
except Exception as e:
print(f"✗ Error: {e}")
"""
Backend factory for creating appropriate KiCAD API backend
Auto-detects available backends and provides fallback mechanism.
"""
import logging
import os
from pathlib import Path
from typing import Optional
from kicad_api.base import APINotAvailableError, KiCADBackend
logger = logging.getLogger(__name__)
def create_backend(backend_type: Optional[str] = None) -> KiCADBackend:
"""
Create appropriate KiCAD backend
Args:
backend_type: Backend to use:
- 'ipc': Use IPC API (recommended)
- 'swig': Use legacy SWIG bindings
- None or 'auto': Auto-detect (try IPC first, fall back to SWIG)
Returns:
KiCADBackend instance
Raises:
APINotAvailableError: If no backend is available
Environment Variables:
KICAD_BACKEND: Override backend selection ('ipc', 'swig', or 'auto')
"""
# Check environment variable override
if backend_type is None:
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":
return _create_ipc_backend()
elif backend_type == "swig":
return _create_swig_backend()
elif backend_type == "auto":
return _auto_detect_backend()
else:
raise ValueError(f"Unknown backend type: {backend_type}")
def _create_ipc_backend() -> KiCADBackend:
"""
Create IPC backend
Returns:
IPCBackend instance
Raises:
APINotAvailableError: If kicad-python not available
"""
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"
) from e
def _create_swig_backend() -> KiCADBackend:
"""
Create SWIG backend
Returns:
SWIGBackend instance
Raises:
APINotAvailableError: If pcbnew not available
"""
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. "
"Please migrate to IPC backend."
)
return SWIGBackend()
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."
) from e
def _auto_detect_backend() -> KiCADBackend:
"""
Auto-detect best available backend
Priority:
1. IPC API (if kicad-python available and KiCAD running)
2. SWIG API (if pcbnew available)
Returns:
Best available KiCADBackend
Raises:
APINotAvailableError: If no backend available
"""
logger.info("Auto-detecting available KiCAD backend...")
# Try IPC first (preferred)
try:
backend = _create_ipc_backend()
# Test connection
if backend.connect():
logger.info("✓ IPC backend available and connected")
return backend
else:
logger.warning("IPC backend available but connection failed")
except (ImportError, APINotAvailableError) as e:
logger.debug(f"IPC backend not available: {e}")
# Fall back to SWIG
try:
backend = _create_swig_backend()
logger.warning(
"Using deprecated SWIG backend. " "For best results, use IPC API with KiCAD running."
)
return backend
except (ImportError, APINotAvailableError) as e:
logger.error(f"SWIG backend not available: {e}")
# No backend available
raise APINotAvailableError(
"No KiCAD backend available. Please install either:\n"
" - kicad-python (recommended): pip install kicad-python\n"
" - Ensure KiCAD Python module (pcbnew) is in PYTHONPATH"
)
def get_available_backends() -> dict:
"""
Check which backends are available
Returns:
Dictionary with backend availability:
{
'ipc': {'available': bool, 'version': str or None},
'swig': {'available': bool, 'version': str or None}
}
"""
results = {}
# Check IPC (kicad-python uses 'kipy' module name)
try:
import kipy
results["ipc"] = {"available": True, "version": getattr(kipy, "__version__", "unknown")}
except ImportError:
results["ipc"] = {"available": False, "version": None}
# Check SWIG
try:
import pcbnew
results["swig"] = {"available": True, "version": pcbnew.GetBuildVersion()}
except ImportError:
results["swig"] = {"available": False, "version": None}
return results
if __name__ == "__main__":
# Quick diagnostic
import json
print("KiCAD Backend Availability:")
print(json.dumps(get_available_backends(), indent=2))
print("\nAttempting to create backend...")
try:
backend = create_backend()
print(f"✓ Created backend: {type(backend).__name__}")
if backend.connect():
print(f"✓ Connected to KiCAD: {backend.get_version()}")
else:
print("✗ Failed to connect to KiCAD")
except Exception as e:
print(f"✗ Error: {e}")

File diff suppressed because it is too large Load Diff

View File

@@ -1,218 +1,218 @@
"""
SWIG Backend (Legacy - DEPRECATED)
Uses the legacy SWIG-based pcbnew Python bindings.
This backend wraps the existing implementation for backward compatibility.
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 Any, Dict, List, Optional
from kicad_api.base import APINotAvailableError, BoardAPI, ConnectionError, KiCADBackend
logger = logging.getLogger(__name__)
class SWIGBackend(KiCADBackend):
"""
Legacy SWIG-based backend
Wraps existing commands/project.py, commands/component.py, etc.
for compatibility during migration period.
"""
def __init__(self) -> None:
self._connected = False
self._pcbnew = None
logger.warning(
"⚠️ Using DEPRECATED SWIG backend. "
"This will be removed in KiCAD 10.0. "
"Please migrate to IPC API."
)
def connect(self) -> bool:
"""
'Connect' to SWIG API (just validates pcbnew import)
Returns:
True if pcbnew module available
"""
try:
import pcbnew
self._pcbnew = pcbnew
version = pcbnew.GetBuildVersion()
logger.info(f"✓ Connected to pcbnew (SWIG): {version}")
self._connected = True
return True
except ImportError as e:
logger.error("pcbnew module not found")
raise APINotAvailableError(
"SWIG backend requires pcbnew module. "
"Ensure KiCAD Python module is in PYTHONPATH."
) from e
def disconnect(self) -> None:
"""Disconnect from SWIG API (no-op)"""
self._connected = False
self._pcbnew = None
logger.info("Disconnected from SWIG backend")
def is_connected(self) -> bool:
"""Check if connected"""
return self._connected
def get_version(self) -> str:
"""Get KiCAD version"""
if not self.is_connected():
raise ConnectionError("Not connected")
return self._pcbnew.GetBuildVersion()
# Project Operations
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
"""Create project using existing SWIG implementation"""
if not self.is_connected():
raise ConnectionError("Not connected")
# Import existing implementation
from commands.project import ProjectCommands
try:
result = ProjectCommands.create_project(str(path), name)
return result
except Exception as e:
logger.error(f"Failed to create project: {e}")
raise
def open_project(self, path: Path) -> Dict[str, Any]:
"""Open project using existing SWIG implementation"""
if not self.is_connected():
raise ConnectionError("Not connected")
from commands.project import ProjectCommands
try:
result = ProjectCommands().open_project({"filename": str(path)})
return result
except Exception as e:
logger.error(f"Failed to open project: {e}")
raise
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
"""Save project using existing SWIG implementation"""
if not self.is_connected():
raise ConnectionError("Not connected")
from commands.project import ProjectCommands
try:
params: Dict[str, Any] = {}
if path:
params["filename"] = str(path)
result = ProjectCommands().save_project(params)
return result
except Exception as e:
logger.error(f"Failed to save project: {e}")
raise
def close_project(self) -> None:
"""Close project (SWIG doesn't have explicit close)"""
logger.info("Closing project (SWIG backend)")
# SWIG backend doesn't maintain project state,
# so this is essentially a no-op
# Board Operations
def get_board(self) -> BoardAPI:
"""Get board API"""
if not self.is_connected():
raise ConnectionError("Not connected")
return SWIGBoardAPI(self._pcbnew)
class SWIGBoardAPI(BoardAPI):
"""Board API implementation wrapping SWIG/pcbnew"""
def __init__(self, pcbnew_module: Any) -> None:
self.pcbnew = pcbnew_module
self._board = None
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
"""Set board size using existing implementation"""
from commands.board import BoardCommands
try:
result = BoardCommands(board=self._board).set_board_size(
{"width": width, "height": height, "unit": unit}
)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to set board size: {e}")
return False
def get_size(self) -> Dict[str, Any]:
"""Get board size"""
# TODO: Implement using existing SWIG code
raise NotImplementedError("get_size not yet wrapped")
def add_layer(self, layer_name: str, layer_type: str) -> bool:
"""Add layer using existing implementation"""
from commands.board import BoardCommands
try:
result = BoardCommands.add_layer(layer_name, layer_type)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to add layer: {e}")
return False
def list_components(self) -> List[Dict[str, Any]]:
"""List components using existing implementation"""
from commands.component import ComponentCommands
try:
result = ComponentCommands(board=self._board).get_component_list({})
if result.get("success"):
return result.get("components", [])
return []
except Exception as e:
logger.error(f"Failed to list components: {e}")
return []
def place_component(
self,
reference: str,
footprint: str,
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu",
value: str = "",
) -> bool:
"""Place component using existing implementation"""
from commands.component import ComponentCommands
try:
result = ComponentCommands(board=self._board).place_component(
{
"componentId": footprint,
"position": {"x": x, "y": y, "unit": "mm"},
"reference": reference,
"rotation": rotation,
"layer": layer,
}
)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to place component: {e}")
return False
# This backend serves as a wrapper during the migration period.
# Once IPC backend is fully implemented, this can be deprecated.
"""
SWIG Backend (Legacy - DEPRECATED)
Uses the legacy SWIG-based pcbnew Python bindings.
This backend wraps the existing implementation for backward compatibility.
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 Any, Dict, List, Optional
from kicad_api.base import APINotAvailableError, BoardAPI, ConnectionError, KiCADBackend
logger = logging.getLogger(__name__)
class SWIGBackend(KiCADBackend):
"""
Legacy SWIG-based backend
Wraps existing commands/project.py, commands/component.py, etc.
for compatibility during migration period.
"""
def __init__(self) -> None:
self._connected = False
self._pcbnew = None
logger.warning(
"⚠️ Using DEPRECATED SWIG backend. "
"This will be removed in KiCAD 10.0. "
"Please migrate to IPC API."
)
def connect(self) -> bool:
"""
'Connect' to SWIG API (just validates pcbnew import)
Returns:
True if pcbnew module available
"""
try:
import pcbnew
self._pcbnew = pcbnew
version = pcbnew.GetBuildVersion()
logger.info(f"✓ Connected to pcbnew (SWIG): {version}")
self._connected = True
return True
except ImportError as e:
logger.error("pcbnew module not found")
raise APINotAvailableError(
"SWIG backend requires pcbnew module. "
"Ensure KiCAD Python module is in PYTHONPATH."
) from e
def disconnect(self) -> None:
"""Disconnect from SWIG API (no-op)"""
self._connected = False
self._pcbnew = None
logger.info("Disconnected from SWIG backend")
def is_connected(self) -> bool:
"""Check if connected"""
return self._connected
def get_version(self) -> str:
"""Get KiCAD version"""
if not self.is_connected():
raise ConnectionError("Not connected")
return self._pcbnew.GetBuildVersion()
# Project Operations
def create_project(self, path: Path, name: str) -> Dict[str, Any]:
"""Create project using existing SWIG implementation"""
if not self.is_connected():
raise ConnectionError("Not connected")
# Import existing implementation
from commands.project import ProjectCommands
try:
result = ProjectCommands.create_project(str(path), name)
return result
except Exception as e:
logger.error(f"Failed to create project: {e}")
raise
def open_project(self, path: Path) -> Dict[str, Any]:
"""Open project using existing SWIG implementation"""
if not self.is_connected():
raise ConnectionError("Not connected")
from commands.project import ProjectCommands
try:
result = ProjectCommands().open_project({"filename": str(path)})
return result
except Exception as e:
logger.error(f"Failed to open project: {e}")
raise
def save_project(self, path: Optional[Path] = None) -> Dict[str, Any]:
"""Save project using existing SWIG implementation"""
if not self.is_connected():
raise ConnectionError("Not connected")
from commands.project import ProjectCommands
try:
params: Dict[str, Any] = {}
if path:
params["filename"] = str(path)
result = ProjectCommands().save_project(params)
return result
except Exception as e:
logger.error(f"Failed to save project: {e}")
raise
def close_project(self) -> None:
"""Close project (SWIG doesn't have explicit close)"""
logger.info("Closing project (SWIG backend)")
# SWIG backend doesn't maintain project state,
# so this is essentially a no-op
# Board Operations
def get_board(self) -> BoardAPI:
"""Get board API"""
if not self.is_connected():
raise ConnectionError("Not connected")
return SWIGBoardAPI(self._pcbnew)
class SWIGBoardAPI(BoardAPI):
"""Board API implementation wrapping SWIG/pcbnew"""
def __init__(self, pcbnew_module: Any) -> None:
self.pcbnew = pcbnew_module
self._board = None
def set_size(self, width: float, height: float, unit: str = "mm") -> bool:
"""Set board size using existing implementation"""
from commands.board import BoardCommands
try:
result = BoardCommands(board=self._board).set_board_size(
{"width": width, "height": height, "unit": unit}
)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to set board size: {e}")
return False
def get_size(self) -> Dict[str, Any]:
"""Get board size"""
# TODO: Implement using existing SWIG code
raise NotImplementedError("get_size not yet wrapped")
def add_layer(self, layer_name: str, layer_type: str) -> bool:
"""Add layer using existing implementation"""
from commands.board import BoardCommands
try:
result = BoardCommands.add_layer(layer_name, layer_type)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to add layer: {e}")
return False
def list_components(self) -> List[Dict[str, Any]]:
"""List components using existing implementation"""
from commands.component import ComponentCommands
try:
result = ComponentCommands(board=self._board).get_component_list({})
if result.get("success"):
return result.get("components", [])
return []
except Exception as e:
logger.error(f"Failed to list components: {e}")
return []
def place_component(
self,
reference: str,
footprint: str,
x: float,
y: float,
rotation: float = 0,
layer: str = "F.Cu",
value: str = "",
) -> bool:
"""Place component using existing implementation"""
from commands.component import ComponentCommands
try:
result = ComponentCommands(board=self._board).place_component(
{
"componentId": footprint,
"position": {"x": x, "y": y, "unit": "mm"},
"reference": reference,
"rotation": rotation,
"layer": layer,
}
)
return result.get("success", False)
except Exception as e:
logger.error(f"Failed to place component: {e}")
return False
# This backend serves as a wrapper during the migration period.
# Once IPC backend is fully implemented, this can be deprecated.