chore: enable strict mypy checks and fix pre-commit mypy hook
Add type annotations to all previously untyped functions and remove 9 suppressed error codes (call-arg, assignment, return-value, operator, has-type, dict-item, misc, list-item, annotation-unchecked) by fixing the underlying type issues. Add [[tool.mypy.overrides]] with ignore_missing_imports for KiCAD-specific modules (pcbnew, sexpdata, skip, cairosvg, kipy, PIL) so the pre-commit mypy hook passes in its isolated venv. Add types-requests and pytest to additional_dependencies in .pre-commit-config.yaml. Also fixes several real bugs uncovered by stricter checks: incorrect static calls to instance methods in swig_backend, wrong return type on get_size, missing value param in BoardAPI.place_component, variable shadowing in kicad_process.py, unqualified LibraryManager reference in kicad_interface, and missing top-level Path import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,9 @@ repos:
|
|||||||
files: ^python/
|
files: ^python/
|
||||||
exclude: ^python/commands/board\.py$
|
exclude: ^python/commands/board\.py$
|
||||||
args: [--config-file=pyproject.toml]
|
args: [--config-file=pyproject.toml]
|
||||||
additional_dependencies: []
|
additional_dependencies:
|
||||||
|
- types-requests
|
||||||
|
- pytest
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -23,15 +23,20 @@ disable_error_code = [
|
|||||||
"arg-type",
|
"arg-type",
|
||||||
"attr-defined",
|
"attr-defined",
|
||||||
"union-attr",
|
"union-attr",
|
||||||
"call-arg",
|
|
||||||
"assignment",
|
|
||||||
"var-annotated",
|
"var-annotated",
|
||||||
"annotation-unchecked",
|
|
||||||
"index",
|
"index",
|
||||||
"has-type",
|
|
||||||
"dict-item",
|
|
||||||
"misc",
|
|
||||||
"list-item",
|
|
||||||
"return-value",
|
|
||||||
"operator",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = [
|
||||||
|
"pcbnew",
|
||||||
|
"cairosvg",
|
||||||
|
"sexpdata",
|
||||||
|
"skip",
|
||||||
|
"kipy",
|
||||||
|
"kipy.*",
|
||||||
|
"schematic",
|
||||||
|
"PIL",
|
||||||
|
"PIL.*",
|
||||||
|
]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class ComponentManager:
|
|||||||
_dynamic_loader = None
|
_dynamic_loader = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_dynamic_loader(cls):
|
def get_dynamic_loader(cls) -> Any:
|
||||||
"""Get or create dynamic symbol loader instance"""
|
"""Get or create dynamic symbol loader instance"""
|
||||||
if cls._dynamic_loader is None and DYNAMIC_LOADING_AVAILABLE:
|
if cls._dynamic_loader is None and DYNAMIC_LOADING_AVAILABLE:
|
||||||
cls._dynamic_loader = DynamicSymbolLoader()
|
cls._dynamic_loader = DynamicSymbolLoader()
|
||||||
@@ -86,7 +86,7 @@ class ComponentManager:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Helper function to check if template exists in schematic
|
# Helper function to check if template exists in schematic
|
||||||
def template_exists(schematic, template_ref):
|
def template_exists(schematic: Any, template_ref: str) -> bool:
|
||||||
"""Check if template exists by iterating symbols (handles special characters)"""
|
"""Check if template exists by iterating symbols (handles special characters)"""
|
||||||
for symbol in schematic.symbol:
|
for symbol in schematic.symbol:
|
||||||
if (
|
if (
|
||||||
@@ -165,7 +165,7 @@ class ComponentManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def add_component(
|
def add_component(
|
||||||
schematic: Schematic, component_def: dict, schematic_path: Optional[Path] = None
|
schematic: Schematic, component_def: dict, schematic_path: Optional[Path] = None
|
||||||
):
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Add a component to the schematic by cloning from template
|
Add a component to the schematic by cloning from template
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ class ComponentManager:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_component(schematic: Schematic, component_ref: str):
|
def remove_component(schematic: Schematic, component_ref: str) -> bool:
|
||||||
"""Remove a component from the schematic by reference designator"""
|
"""Remove a component from the schematic by reference designator"""
|
||||||
try:
|
try:
|
||||||
# kicad-skip doesn't have a direct remove_symbol method by reference.
|
# kicad-skip doesn't have a direct remove_symbol method by reference.
|
||||||
@@ -288,7 +288,7 @@ class ComponentManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_component(schematic: Schematic, component_ref: str, new_properties: dict):
|
def update_component(schematic: Schematic, component_ref: str, new_properties: dict) -> bool:
|
||||||
"""Update component properties by reference designator"""
|
"""Update component properties by reference designator"""
|
||||||
try:
|
try:
|
||||||
symbol_to_update = None
|
symbol_to_update = None
|
||||||
@@ -313,7 +313,7 @@ class ComponentManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_component(schematic: Schematic, component_ref: str):
|
def get_component(schematic: Schematic, component_ref: str) -> Any:
|
||||||
"""Get a component by reference designator"""
|
"""Get a component by reference designator"""
|
||||||
for symbol in schematic.symbol:
|
for symbol in schematic.symbol:
|
||||||
if symbol.reference == component_ref:
|
if symbol.reference == component_ref:
|
||||||
@@ -323,7 +323,7 @@ class ComponentManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_components(schematic: Schematic, query: str):
|
def search_components(schematic: Schematic, query: str) -> List[Any]:
|
||||||
"""Search for components matching criteria (basic implementation)"""
|
"""Search for components matching criteria (basic implementation)"""
|
||||||
# This is a basic search, could be expanded to use regex or more complex logic
|
# This is a basic search, could be expanded to use regex or more complex logic
|
||||||
matching_components = []
|
matching_components = []
|
||||||
@@ -342,7 +342,7 @@ class ComponentManager:
|
|||||||
return matching_components
|
return matching_components
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all_components(schematic: Schematic):
|
def get_all_components(schematic: Schematic) -> List[Any]:
|
||||||
"""Get all components in schematic"""
|
"""Get all components in schematic"""
|
||||||
logger.debug(f"Retrieving all {len(schematic.symbol)} components.")
|
logger.debug(f"Retrieving all {len(schematic.symbol)} components.")
|
||||||
return list(schematic.symbol)
|
return list(schematic.symbol)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
|
|
||||||
@@ -25,14 +25,14 @@ class ConnectionManager:
|
|||||||
_pin_locator = None
|
_pin_locator = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_pin_locator(cls):
|
def get_pin_locator(cls) -> Any:
|
||||||
"""Get or create pin locator instance"""
|
"""Get or create pin locator instance"""
|
||||||
if cls._pin_locator is None and WIRE_MANAGER_AVAILABLE:
|
if cls._pin_locator is None and WIRE_MANAGER_AVAILABLE:
|
||||||
cls._pin_locator = PinLocator()
|
cls._pin_locator = PinLocator()
|
||||||
return cls._pin_locator
|
return cls._pin_locator
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_net_label(schematic: Schematic, net_name: str, position: list):
|
def add_net_label(schematic: Schematic, net_name: str, position: list) -> Any:
|
||||||
"""
|
"""
|
||||||
Add a net label to the schematic
|
Add a net label to the schematic
|
||||||
|
|
||||||
@@ -57,7 +57,9 @@ class ConnectionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def connect_to_net(schematic_path: Path, component_ref: str, pin_name: str, net_name: str):
|
def connect_to_net(
|
||||||
|
schematic_path: Path, component_ref: str, pin_name: str, net_name: str
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Connect a component pin to a named net using a wire stub and label
|
Connect a component pin to a named net using a wire stub and label
|
||||||
|
|
||||||
@@ -132,7 +134,7 @@ class ConnectionManager:
|
|||||||
target_ref: str,
|
target_ref: str,
|
||||||
net_prefix: str = "PIN",
|
net_prefix: str = "PIN",
|
||||||
pin_offset: int = 0,
|
pin_offset: int = 0,
|
||||||
):
|
) -> Dict[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
Connect all pins of source_ref to matching pins of target_ref via shared net labels.
|
Connect all pins of source_ref to matching pins of target_ref via shared net labels.
|
||||||
Useful for passthrough adapters: J1 pin N <-> J2 pin N on net {net_prefix}_{N}.
|
Useful for passthrough adapters: J1 pin N <-> J2 pin N on net {net_prefix}_{N}.
|
||||||
@@ -203,7 +205,7 @@ class ConnectionManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_net_connections(
|
def get_net_connections(
|
||||||
schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None
|
schematic: Schematic, net_name: str, schematic_path: Optional[Path] = None
|
||||||
):
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Get all connections for a named net using wire graph analysis
|
Get all connections for a named net using wire graph analysis
|
||||||
|
|
||||||
@@ -221,7 +223,7 @@ class ConnectionManager:
|
|||||||
connections = []
|
connections = []
|
||||||
tolerance = 0.5 # 0.5mm tolerance for point coincidence (grid spacing consideration)
|
tolerance = 0.5 # 0.5mm tolerance for point coincidence (grid spacing consideration)
|
||||||
|
|
||||||
def points_coincide(p1, p2):
|
def points_coincide(p1: Any, p2: Any) -> bool:
|
||||||
"""Check if two points are the same (within tolerance)"""
|
"""Check if two points are the same (within tolerance)"""
|
||||||
if not p1 or not p2:
|
if not p1 or not p2:
|
||||||
return False
|
return False
|
||||||
@@ -324,8 +326,8 @@ class ConnectionManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if pin coincides with any wire point
|
# Check if pin coincides with any wire point
|
||||||
for wire_pt in connected_wire_points:
|
for wire_pt_tup in connected_wire_points:
|
||||||
if points_coincide(pin_loc, list(wire_pt)):
|
if points_coincide(pin_loc, list(wire_pt_tup)):
|
||||||
connections.append({"component": ref, "pin": pin_num})
|
connections.append({"component": ref, "pin": pin_num})
|
||||||
break # Pin found, no need to check more wire points
|
break # Pin found, no need to check more wire points
|
||||||
|
|
||||||
@@ -344,8 +346,10 @@ class ConnectionManager:
|
|||||||
symbol_y = float(symbol_pos[1])
|
symbol_y = float(symbol_pos[1])
|
||||||
|
|
||||||
# Check if symbol is near any wire point (within 10mm)
|
# Check if symbol is near any wire point (within 10mm)
|
||||||
for wire_pt in connected_wire_points:
|
for wire_pt_tup in connected_wire_points:
|
||||||
dist = ((symbol_x - wire_pt[0]) ** 2 + (symbol_y - wire_pt[1]) ** 2) ** 0.5
|
dist = (
|
||||||
|
(symbol_x - wire_pt_tup[0]) ** 2 + (symbol_y - wire_pt_tup[1]) ** 2
|
||||||
|
) ** 0.5
|
||||||
if dist < 10.0: # 10mm proximity threshold
|
if dist < 10.0: # 10mm proximity threshold
|
||||||
connections.append({"component": ref, "pin": "unknown"})
|
connections.append({"component": ref, "pin": "unknown"})
|
||||||
break # Only add once per component
|
break # Only add once per component
|
||||||
@@ -361,7 +365,9 @@ class ConnectionManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_netlist(schematic: Schematic, schematic_path: Optional[Path] = None):
|
def generate_netlist(
|
||||||
|
schematic: Schematic, schematic_path: Optional[Path] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Generate a netlist from the schematic
|
Generate a netlist from the schematic
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ No API key required.
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger("kicad_interface")
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class DatasheetManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_lib_symbols_range(lines: List[str]):
|
def _find_lib_symbols_range(lines: List[str]) -> Tuple[Optional[int], Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Find the line range of the (lib_symbols ...) section.
|
Find the line range of the (lib_symbols ...) section.
|
||||||
Returns (start, end) line indices or (None, None) if not found.
|
Returns (start, end) line indices or (None, None) if not found.
|
||||||
|
|||||||
@@ -238,7 +238,10 @@ class FootprintCreator:
|
|||||||
changes.append(f"drill (inserted)→{new_drill}")
|
changes.append(f"drill (inserted)→{new_drill}")
|
||||||
if shape:
|
if shape:
|
||||||
block, n = re.subn(
|
block, n = re.subn(
|
||||||
r'(pad\s+"[^"]*"\s+\w+\s+)\w+', lambda m: m.group(1) + shape, block, count=1
|
r'(pad\s+"[^"]*"\s+\w+\s+)\w+',
|
||||||
|
lambda m: str(m.group(1)) + shape,
|
||||||
|
block,
|
||||||
|
count=1,
|
||||||
)
|
)
|
||||||
if n:
|
if n:
|
||||||
changes.append(f"shape→{shape}")
|
changes.append(f"shape→{shape}")
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ def _build_freerouting_cmd(
|
|||||||
"""Build the command to run Freerouting."""
|
"""Build the command to run Freerouting."""
|
||||||
if use_docker:
|
if use_docker:
|
||||||
docker_exe = _find_docker()
|
docker_exe = _find_docker()
|
||||||
|
if docker_exe is None:
|
||||||
|
raise RuntimeError("Docker/Podman executable not found")
|
||||||
board_dir = os.path.dirname(dsn_path)
|
board_dir = os.path.dirname(dsn_path)
|
||||||
dsn_name = os.path.basename(dsn_path)
|
dsn_name = os.path.basename(dsn_path)
|
||||||
ses_name = os.path.basename(ses_path)
|
ses_name = os.path.basename(ses_path)
|
||||||
@@ -120,6 +122,8 @@ def _build_freerouting_cmd(
|
|||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
java_exe = _find_java()
|
java_exe = _find_java()
|
||||||
|
if java_exe is None:
|
||||||
|
raise RuntimeError("Java executable not found")
|
||||||
return [
|
return [
|
||||||
java_exe,
|
java_exe,
|
||||||
"-jar",
|
"-jar",
|
||||||
@@ -136,7 +140,7 @@ def _build_freerouting_cmd(
|
|||||||
class FreeroutingCommands:
|
class FreeroutingCommands:
|
||||||
"""Handles Freerouting autoroute operations."""
|
"""Handles Freerouting autoroute operations."""
|
||||||
|
|
||||||
def __init__(self, board=None):
|
def __init__(self, board: Any = None) -> None:
|
||||||
self.board = board
|
self.board = board
|
||||||
|
|
||||||
def _resolve_execution_mode(self, jar_path: str) -> Dict[str, Any]:
|
def _resolve_execution_mode(self, jar_path: str) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import os
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger("kicad_interface")
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
@@ -38,10 +38,10 @@ class JLCPCBPartsManager:
|
|||||||
db_path = str(data_dir / "jlcpcb_parts.db")
|
db_path = str(data_dir / "jlcpcb_parts.db")
|
||||||
|
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.conn = None
|
self.conn: Optional[sqlite3.Connection] = None
|
||||||
self._init_database()
|
self._init_database()
|
||||||
|
|
||||||
def _init_database(self):
|
def _init_database(self) -> None:
|
||||||
"""Initialize SQLite database with schema"""
|
"""Initialize SQLite database with schema"""
|
||||||
self.conn = sqlite3.connect(self.db_path)
|
self.conn = sqlite3.connect(self.db_path)
|
||||||
self.conn.row_factory = sqlite3.Row # Return rows as dicts
|
self.conn.row_factory = sqlite3.Row # Return rows as dicts
|
||||||
@@ -90,7 +90,9 @@ class JLCPCBPartsManager:
|
|||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
logger.info(f"Initialized JLCPCB parts database at {self.db_path}")
|
logger.info(f"Initialized JLCPCB parts database at {self.db_path}")
|
||||||
|
|
||||||
def import_parts(self, parts: List[Dict], progress_callback=None):
|
def import_parts(
|
||||||
|
self, parts: List[Dict], progress_callback: Optional[Callable[..., Any]] = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Import parts into database from JLCPCB API response
|
Import parts into database from JLCPCB API response
|
||||||
|
|
||||||
@@ -167,7 +169,9 @@ class JLCPCBPartsManager:
|
|||||||
else:
|
else:
|
||||||
return "Extended" # Default to Extended
|
return "Extended" # Default to Extended
|
||||||
|
|
||||||
def import_jlcsearch_parts(self, parts: List[Dict], progress_callback=None):
|
def import_jlcsearch_parts(
|
||||||
|
self, parts: List[Dict], progress_callback: Optional[Callable[..., Any]] = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Import parts into database from JLCSearch API response
|
Import parts into database from JLCSearch API response
|
||||||
|
|
||||||
@@ -452,7 +456,7 @@ class JLCPCBPartsManager:
|
|||||||
alternatives = [p for p in alternatives if p["lcsc"] != lcsc_number]
|
alternatives = [p for p in alternatives if p["lcsc"] != lcsc_number]
|
||||||
|
|
||||||
# Sort by: Basic first, then by price, then by stock
|
# Sort by: Basic first, then by price, then by stock
|
||||||
def sort_key(p):
|
def sort_key(p: Dict[str, Any]) -> Tuple[int, float, int]:
|
||||||
is_basic = 1 if p.get("library_type") == "Basic" else 0
|
is_basic = 1 if p.get("library_type") == "Basic" else 0
|
||||||
try:
|
try:
|
||||||
prices = json.loads(p.get("price_json", "[]"))
|
prices = json.loads(p.get("price_json", "[]"))
|
||||||
@@ -467,7 +471,7 @@ class JLCPCBPartsManager:
|
|||||||
|
|
||||||
return alternatives[:limit]
|
return alternatives[:limit]
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
"""Close database connection"""
|
"""Close database connection"""
|
||||||
if self.conn:
|
if self.conn:
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jlcsearch service at https://jlcsearch.tscircuit.com/
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional, Union
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ class JLCSearchClient:
|
|||||||
|
|
||||||
BASE_URL = "https://jlcsearch.tscircuit.com"
|
BASE_URL = "https://jlcsearch.tscircuit.com"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize JLCSearch API client"""
|
"""Initialize JLCSearch API client"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def search_components(
|
def search_components(
|
||||||
self, category: str = "components", limit: int = 100, offset: int = 0, **filters
|
self, category: str = "components", limit: int = 100, offset: int = 0, **filters: Dict
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Search components in JLCSearch database
|
Search components in JLCSearch database
|
||||||
@@ -87,7 +87,7 @@ class JLCSearchClient:
|
|||||||
- stock: Available stock
|
- stock: Available stock
|
||||||
- price1: Price per unit
|
- price1: Price per unit
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters: Dict[str, Any] = {}
|
||||||
if resistance is not None:
|
if resistance is not None:
|
||||||
filters["resistance"] = resistance
|
filters["resistance"] = resistance
|
||||||
if package:
|
if package:
|
||||||
@@ -109,7 +109,7 @@ class JLCSearchClient:
|
|||||||
Returns:
|
Returns:
|
||||||
List of capacitor dicts
|
List of capacitor dicts
|
||||||
"""
|
"""
|
||||||
filters = {}
|
filters: Dict[str, Any] = {}
|
||||||
if capacitance is not None:
|
if capacitance is not None:
|
||||||
filters["capacitance"] = capacitance
|
filters["capacitance"] = capacitance
|
||||||
if package:
|
if package:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class LibraryManager:
|
|||||||
self.footprint_cache: Dict[str, List[str]] = {} # library -> [footprint names]
|
self.footprint_cache: Dict[str, List[str]] = {} # library -> [footprint names]
|
||||||
self._load_libraries()
|
self._load_libraries()
|
||||||
|
|
||||||
def _load_libraries(self):
|
def _load_libraries(self) -> None:
|
||||||
"""Load libraries from fp-lib-table files"""
|
"""Load libraries from fp-lib-table files"""
|
||||||
# Load global libraries
|
# Load global libraries
|
||||||
global_table = self._get_global_fp_lib_table()
|
global_table = self._get_global_fp_lib_table()
|
||||||
@@ -78,7 +78,7 @@ class LibraryManager:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_fp_lib_table(self, table_path: Path):
|
def _parse_fp_lib_table(self, table_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Parse fp-lib-table file
|
Parse fp-lib-table file
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
|
|
||||||
# Symbol class might not be directly importable in the current version
|
# Symbol class might not be directly importable in the current version
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ class LibraryManager:
|
|||||||
"""Manage symbol libraries"""
|
"""Manage symbol libraries"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_available_libraries(search_paths=None):
|
def list_available_libraries(search_paths: Optional[List[str]] = None) -> Dict[str, List[str]]:
|
||||||
"""List all available symbol libraries"""
|
"""List all available symbol libraries"""
|
||||||
if search_paths is None:
|
if search_paths is None:
|
||||||
# Default library paths based on common KiCAD installations
|
# Default library paths based on common KiCAD installations
|
||||||
@@ -46,7 +47,7 @@ class LibraryManager:
|
|||||||
return {"paths": libraries, "names": library_names}
|
return {"paths": libraries, "names": library_names}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def list_library_symbols(library_path):
|
def list_library_symbols(library_path: str) -> List[Any]:
|
||||||
"""List all symbols in a library"""
|
"""List all symbols in a library"""
|
||||||
try:
|
try:
|
||||||
# kicad-skip doesn't provide a direct way to simply list symbols in a library
|
# kicad-skip doesn't provide a direct way to simply list symbols in a library
|
||||||
@@ -66,7 +67,7 @@ class LibraryManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_symbol_details(library_path, symbol_name):
|
def get_symbol_details(library_path: str, symbol_name: str) -> Dict[str, Any]:
|
||||||
"""Get detailed information about a symbol"""
|
"""Get detailed information about a symbol"""
|
||||||
try:
|
try:
|
||||||
# Similar to list_library_symbols, this might require a more direct approach
|
# Similar to list_library_symbols, this might require a more direct approach
|
||||||
@@ -80,7 +81,7 @@ class LibraryManager:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def search_symbols(query, search_paths=None):
|
def search_symbols(query: str, search_paths: Optional[List[str]] = None) -> List[Any]:
|
||||||
"""Search for symbols matching criteria"""
|
"""Search for symbols matching criteria"""
|
||||||
try:
|
try:
|
||||||
# This would typically involve:
|
# This would typically involve:
|
||||||
@@ -101,7 +102,9 @@ class LibraryManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_default_symbol_for_component_type(component_type, search_paths=None):
|
def get_default_symbol_for_component_type(
|
||||||
|
component_type: str, search_paths: Optional[List[str]] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
"""Get a recommended default symbol for a given component type"""
|
"""Get a recommended default symbol for a given component type"""
|
||||||
# This method provides a simplified way to get a symbol for common component types
|
# This method provides a simplified way to get a symbol for common component types
|
||||||
# It's useful when the user doesn't specify a particular library/symbol
|
# It's useful when the user doesn't specify a particular library/symbol
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class SymbolLibraryManager:
|
|||||||
self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo]
|
self.symbol_cache: Dict[str, List[SymbolInfo]] = {} # library -> [SymbolInfo]
|
||||||
self._load_libraries()
|
self._load_libraries()
|
||||||
|
|
||||||
def _load_libraries(self):
|
def _load_libraries(self) -> None:
|
||||||
"""Load libraries from sym-lib-table files"""
|
"""Load libraries from sym-lib-table files"""
|
||||||
# Load global libraries
|
# Load global libraries
|
||||||
global_table = self._get_global_sym_lib_table()
|
global_table = self._get_global_sym_lib_table()
|
||||||
@@ -98,7 +98,7 @@ class SymbolLibraryManager:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_sym_lib_table(self, table_path: Path):
|
def _parse_sym_lib_table(self, table_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Parse sym-lib-table file
|
Parse sym-lib-table file
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ class SymbolLibraryManager:
|
|||||||
query_lower = query.lower()
|
query_lower = query.lower()
|
||||||
|
|
||||||
# Determine which libraries to search
|
# Determine which libraries to search
|
||||||
libraries_to_search = self.libraries.keys()
|
libraries_to_search: list[str] = list(self.libraries.keys())
|
||||||
if library_filter:
|
if library_filter:
|
||||||
filter_lower = library_filter.lower()
|
filter_lower = library_filter.lower()
|
||||||
libraries_to_search = [
|
libraries_to_search = [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import sexpdata
|
import sexpdata
|
||||||
from sexpdata import Symbol
|
from sexpdata import Symbol
|
||||||
@@ -21,7 +21,7 @@ logger = logging.getLogger("kicad_interface")
|
|||||||
class PinLocator:
|
class PinLocator:
|
||||||
"""Locate pins on symbol instances in KiCad schematics"""
|
"""Locate pins on symbol instances in KiCad schematics"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize pin locator with empty cache"""
|
"""Initialize pin locator with empty cache"""
|
||||||
self.pin_definition_cache = {} # Cache: "lib_id:symbol_name" -> pin_data
|
self.pin_definition_cache = {} # Cache: "lib_id:symbol_name" -> pin_data
|
||||||
self._schematic_cache: Dict[str, object] = {} # Cache: path -> loaded Schematic
|
self._schematic_cache: Dict[str, object] = {} # Cache: path -> loaded Schematic
|
||||||
@@ -41,9 +41,9 @@ class PinLocator:
|
|||||||
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"}
|
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
pins = {}
|
pins: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
def extract_pins_recursive(sexp):
|
def extract_pins_recursive(sexp: Any) -> None:
|
||||||
"""Recursively search for pin definitions"""
|
"""Recursively search for pin definitions"""
|
||||||
if not isinstance(sexp, list):
|
if not isinstance(sexp, list):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class RoutingCommands:
|
|||||||
"errorDetails": f"'{ref}' does not exist on the board",
|
"errorDetails": f"'{ref}' does not exist on the board",
|
||||||
}
|
}
|
||||||
|
|
||||||
def find_pad(ref: str, pad_num: str):
|
def find_pad(ref: str, pad_num: str) -> Any:
|
||||||
fp = footprints[ref]
|
fp = footprints[ref]
|
||||||
for pad in fp.Pads():
|
for pad in fp.Pads():
|
||||||
if pad.GetNumber() == pad_num:
|
if pad.GetNumber() == pad_num:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ class SchematicManager:
|
|||||||
"""Core schematic operations using kicad-skip"""
|
"""Core schematic operations using kicad-skip"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_schematic(name, metadata=None):
|
def create_schematic(name: str, metadata: Optional[Any] = None) -> Any:
|
||||||
"""Create a new empty schematic from template"""
|
"""Create a new empty schematic from template"""
|
||||||
try:
|
try:
|
||||||
# Determine template path (use template_with_symbols for component cloning support)
|
# Determine template path (use template_with_symbols for component cloning support)
|
||||||
@@ -70,7 +71,7 @@ class SchematicManager:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_schematic(file_path):
|
def load_schematic(file_path: str) -> Optional[Any]:
|
||||||
"""Load an existing schematic"""
|
"""Load an existing schematic"""
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
logger.error(f"Schematic file not found at {file_path}")
|
logger.error(f"Schematic file not found at {file_path}")
|
||||||
@@ -84,7 +85,7 @@ class SchematicManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_schematic(schematic, file_path):
|
def save_schematic(schematic: Any, file_path: str) -> bool:
|
||||||
"""Save a schematic to file"""
|
"""Save a schematic to file"""
|
||||||
try:
|
try:
|
||||||
# kicad-skip uses write method, not save
|
# kicad-skip uses write method, not save
|
||||||
@@ -96,7 +97,7 @@ class SchematicManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_schematic_metadata(schematic):
|
def get_schematic_metadata(schematic: Any) -> dict[str, Any]:
|
||||||
"""Extract metadata from schematic"""
|
"""Extract metadata from schematic"""
|
||||||
# kicad-skip doesn't expose a direct metadata object on Schematic.
|
# kicad-skip doesn't expose a direct metadata object on Schematic.
|
||||||
# We can return basic info like version and generator.
|
# We can return basic info like version and generator.
|
||||||
|
|||||||
@@ -782,7 +782,7 @@ def find_wires_crossing_symbols(schematic_path: Path) -> List[Dict[str, Any]]:
|
|||||||
collisions = []
|
collisions = []
|
||||||
|
|
||||||
# Pre-compute per-symbol data
|
# Pre-compute per-symbol data
|
||||||
symbol_data = []
|
symbol_data: List[Dict[str, Any]] = []
|
||||||
for sym in symbols:
|
for sym in symbols:
|
||||||
ref = sym["reference"]
|
ref = sym["reference"]
|
||||||
if sym["is_power"] or ref.startswith("_TEMPLATE") or not ref:
|
if sym["is_power"] or ref.startswith("_TEMPLATE") or not ref:
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
|
|||||||
cx_ = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2
|
cx_ = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2
|
||||||
cy_ = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2
|
cy_ = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2
|
||||||
|
|
||||||
def angle(ux, uy, vx, vy):
|
def angle(ux: float, uy: float, vx: float, vy: float) -> float:
|
||||||
a = math.acos(
|
a = math.acos(
|
||||||
max(-1, min(1, (ux * vx + uy * vy) / (math.hypot(ux, uy) * math.hypot(vx, vy))))
|
max(-1, min(1, (ux * vx + uy * vy) / (math.hypot(ux, uy) * math.hypot(vx, vy))))
|
||||||
)
|
)
|
||||||
@@ -293,10 +293,10 @@ def _parse_path_tokens(tokens: List[str]) -> List[Polygon]:
|
|||||||
def _parse_transform(transform_str: str) -> List[List[float]]:
|
def _parse_transform(transform_str: str) -> List[List[float]]:
|
||||||
"""Parse SVG transform attribute, return list of 3×3 matrix rows [a,b,c; d,e,f; 0,0,1]."""
|
"""Parse SVG transform attribute, return list of 3×3 matrix rows [a,b,c; d,e,f; 0,0,1]."""
|
||||||
|
|
||||||
def identity():
|
def identity() -> List[List[float]]:
|
||||||
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
||||||
|
|
||||||
def mat_mul(A, B):
|
def mat_mul(A: List[List[float]], B: List[List[float]]) -> List[List[float]]:
|
||||||
return [[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)] for r in range(3)]
|
return [[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)] for r in range(3)]
|
||||||
|
|
||||||
result = identity()
|
result = identity()
|
||||||
@@ -345,7 +345,7 @@ def _apply_transform(pts: List[Point], mat: List[List[float]]) -> List[Point]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _mat_mul(A, B):
|
def _mat_mul(A: List[List[float]], B: List[List[float]]) -> List[List[float]]:
|
||||||
return [[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)] for r in range(3)]
|
return [[sum(A[r][k] * B[k][c] for k in range(3)) for c in range(3)] for r in range(3)]
|
||||||
|
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ def _get_attr(el: ET.Element, name: str, default: Optional[str] = None) -> Optio
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _identity():
|
def _identity() -> List[List[float]]:
|
||||||
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ coordinate matching, mirroring KiCad's own connectivity algorithm.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Set, Tuple
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
from commands.pin_locator import PinLocator
|
from commands.pin_locator import PinLocator
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ def _to_iu(x_mm: float, y_mm: float) -> Tuple[int, int]:
|
|||||||
return (round(x_mm * _IU_PER_MM), round(y_mm * _IU_PER_MM))
|
return (round(x_mm * _IU_PER_MM), round(y_mm * _IU_PER_MM))
|
||||||
|
|
||||||
|
|
||||||
def _parse_wires(schematic) -> List[List[Tuple[int, int]]]:
|
def _parse_wires(schematic: Any) -> List[List[Tuple[int, int]]]:
|
||||||
"""Extract wire endpoints from a schematic object as IU tuples."""
|
"""Extract wire endpoints from a schematic object as IU tuples."""
|
||||||
all_wires = []
|
all_wires = []
|
||||||
for wire in schematic.wire:
|
for wire in schematic.wire:
|
||||||
@@ -67,7 +67,9 @@ def _build_adjacency(
|
|||||||
return adjacency, iu_to_wires
|
return adjacency, iu_to_wires
|
||||||
|
|
||||||
|
|
||||||
def _parse_virtual_connections(schematic, schematic_path):
|
def _parse_virtual_connections(
|
||||||
|
schematic: Any, schematic_path: Any
|
||||||
|
) -> Tuple[Dict[Tuple[int, int], str], Dict[str, List[Tuple[int, int]]]]:
|
||||||
"""Return virtual connectivity from net labels and power symbols.
|
"""Return virtual connectivity from net labels and power symbols.
|
||||||
|
|
||||||
Returns a tuple of:
|
Returns a tuple of:
|
||||||
@@ -175,8 +177,8 @@ def _find_connected_wires(
|
|||||||
|
|
||||||
def _find_pins_on_net(
|
def _find_pins_on_net(
|
||||||
net_points: Set[Tuple[int, int]],
|
net_points: Set[Tuple[int, int]],
|
||||||
schematic_path,
|
schematic_path: Any,
|
||||||
schematic,
|
schematic: Any,
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""Find component pins that land on net points using exact IU matching.
|
"""Find component pins that land on net points using exact IU matching.
|
||||||
|
|
||||||
@@ -216,7 +218,7 @@ def _find_pins_on_net(
|
|||||||
|
|
||||||
|
|
||||||
def get_wire_connections(
|
def get_wire_connections(
|
||||||
schematic, schematic_path: str, x_mm: float, y_mm: float
|
schematic: Any, schematic_path: str, x_mm: float, y_mm: float
|
||||||
) -> Optional[Dict]:
|
) -> Optional[Dict]:
|
||||||
"""Find all component pins reachable from a point via connected wires, net labels, and power symbols.
|
"""Find all component pins reachable from a point via connected wires, net labels, and power symbols.
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ All methods operate on in-memory sexpdata lists (no disk I/O).
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import sexpdata
|
import sexpdata
|
||||||
from sexpdata import Symbol
|
from sexpdata import Symbol
|
||||||
@@ -55,7 +55,7 @@ class WireDragger:
|
|||||||
"""Pure-logic helpers for wire-endpoint dragging during component moves."""
|
"""Pure-logic helpers for wire-endpoint dragging during component moves."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_symbol(sch_data: list, reference: str):
|
def find_symbol(sch_data: list, reference: str) -> Any:
|
||||||
"""
|
"""
|
||||||
Find a placed symbol by reference designator.
|
Find a placed symbol by reference designator.
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ class WireDragger:
|
|||||||
junction_k = _K["junction"]
|
junction_k = _K["junction"]
|
||||||
at_k = _K["at"]
|
at_k = _K["at"]
|
||||||
|
|
||||||
def find_new(x: float, y: float):
|
def find_new(x: float, y: float) -> Optional[Tuple[float, float]]:
|
||||||
for (ox, oy), (nx, ny) in old_to_new.items():
|
for (ox, oy), (nx, ny) in old_to_new.items():
|
||||||
if _coords_match(x, y, ox, oy, eps):
|
if _coords_match(x, y, ox, oy, eps):
|
||||||
return nx, ny
|
return nx, ny
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import math
|
|||||||
import tempfile
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
import sexpdata
|
import sexpdata
|
||||||
from sexpdata import Symbol
|
from sexpdata import Symbol
|
||||||
@@ -255,7 +255,7 @@ class WireManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_wire(
|
def _parse_wire(
|
||||||
wire_item,
|
wire_item: Any,
|
||||||
) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
|
) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
|
||||||
"""
|
"""
|
||||||
Parse a wire S-expression item in a single pass.
|
Parse a wire S-expression item in a single pass.
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class BoardAPI(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_size(self) -> Dict[str, float]:
|
def get_size(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get current board size
|
Get current board size
|
||||||
|
|
||||||
@@ -169,6 +169,7 @@ class BoardAPI(ABC):
|
|||||||
y: float,
|
y: float,
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
layer: str = "F.Cu",
|
layer: str = "F.Cu",
|
||||||
|
value: str = "",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Place a component on the board
|
Place a component on the board
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ class IPCBackend(KiCADBackend):
|
|||||||
without requiring manual reload.
|
without requiring manual reload.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._kicad = None
|
self._kicad = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._version = None
|
self._version: Optional[str] = None
|
||||||
self._on_change_callbacks: List[Callable] = []
|
self._on_change_callbacks: List[Callable] = []
|
||||||
|
|
||||||
def connect(self, socket_path: Optional[str] = None) -> bool:
|
def connect(self, socket_path: Optional[str] = None) -> bool:
|
||||||
@@ -257,13 +257,13 @@ class IPCBoardAPI(BoardAPI):
|
|||||||
Uses transactions for proper undo/redo support.
|
Uses transactions for proper undo/redo support.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, kicad_instance, notify_callback: Callable):
|
def __init__(self, kicad_instance: Any, notify_callback: Callable) -> None:
|
||||||
self._kicad = kicad_instance
|
self._kicad = kicad_instance
|
||||||
self._board = None
|
self._board = None
|
||||||
self._notify = notify_callback
|
self._notify = notify_callback
|
||||||
self._current_commit = None
|
self._current_commit = None
|
||||||
|
|
||||||
def _get_board(self):
|
def _get_board(self) -> Any:
|
||||||
"""Get board instance, connecting if needed."""
|
"""Get board instance, connecting if needed."""
|
||||||
if self._board is None:
|
if self._board is None:
|
||||||
try:
|
try:
|
||||||
@@ -350,7 +350,7 @@ class IPCBoardAPI(BoardAPI):
|
|||||||
logger.error(f"Failed to set board size: {e}")
|
logger.error(f"Failed to set board size: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_size(self) -> Dict[str, float]:
|
def get_size(self) -> Dict[str, Any]:
|
||||||
"""Get current board size from bounding box."""
|
"""Get current board size from bounding box."""
|
||||||
try:
|
try:
|
||||||
board = self._get_board()
|
board = self._get_board()
|
||||||
@@ -490,7 +490,7 @@ class IPCBoardAPI(BoardAPI):
|
|||||||
logger.error(f"Failed to place component: {e}")
|
logger.error(f"Failed to place component: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _load_footprint_from_library(self, footprint_path: str):
|
def _load_footprint_from_library(self, footprint_path: str) -> Any:
|
||||||
"""
|
"""
|
||||||
Load a footprint from the library using pcbnew SWIG API.
|
Load a footprint from the library using pcbnew SWIG API.
|
||||||
|
|
||||||
@@ -546,7 +546,14 @@ class IPCBoardAPI(BoardAPI):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _place_loaded_footprint(
|
def _place_loaded_footprint(
|
||||||
self, loaded_fp, reference: str, x: float, y: float, rotation: float, layer: str, value: str
|
self,
|
||||||
|
loaded_fp: Any,
|
||||||
|
reference: str,
|
||||||
|
x: float,
|
||||||
|
y: float,
|
||||||
|
rotation: float,
|
||||||
|
layer: str,
|
||||||
|
value: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Place a loaded pcbnew footprint onto the board.
|
Place a loaded pcbnew footprint onto the board.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SWIGBackend(KiCADBackend):
|
|||||||
for compatibility during migration period.
|
for compatibility during migration period.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._connected = False
|
self._connected = False
|
||||||
self._pcbnew = None
|
self._pcbnew = None
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -98,7 +98,7 @@ class SWIGBackend(KiCADBackend):
|
|||||||
from commands.project import ProjectCommands
|
from commands.project import ProjectCommands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = ProjectCommands.open_project(str(path))
|
result = ProjectCommands().open_project({"filename": str(path)})
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to open project: {e}")
|
logger.error(f"Failed to open project: {e}")
|
||||||
@@ -112,8 +112,10 @@ class SWIGBackend(KiCADBackend):
|
|||||||
from commands.project import ProjectCommands
|
from commands.project import ProjectCommands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path_str = str(path) if path else None
|
params: Dict[str, Any] = {}
|
||||||
result = ProjectCommands.save_project(path_str)
|
if path:
|
||||||
|
params["filename"] = str(path)
|
||||||
|
result = ProjectCommands().save_project(params)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to save project: {e}")
|
logger.error(f"Failed to save project: {e}")
|
||||||
@@ -137,7 +139,7 @@ class SWIGBackend(KiCADBackend):
|
|||||||
class SWIGBoardAPI(BoardAPI):
|
class SWIGBoardAPI(BoardAPI):
|
||||||
"""Board API implementation wrapping SWIG/pcbnew"""
|
"""Board API implementation wrapping SWIG/pcbnew"""
|
||||||
|
|
||||||
def __init__(self, pcbnew_module):
|
def __init__(self, pcbnew_module: Any) -> None:
|
||||||
self.pcbnew = pcbnew_module
|
self.pcbnew = pcbnew_module
|
||||||
self._board = None
|
self._board = None
|
||||||
|
|
||||||
@@ -146,13 +148,15 @@ class SWIGBoardAPI(BoardAPI):
|
|||||||
from commands.board import BoardCommands
|
from commands.board import BoardCommands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = BoardCommands.set_board_size(width, height, unit)
|
result = BoardCommands(board=self._board).set_board_size(
|
||||||
|
{"width": width, "height": height, "unit": unit}
|
||||||
|
)
|
||||||
return result.get("success", False)
|
return result.get("success", False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to set board size: {e}")
|
logger.error(f"Failed to set board size: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_size(self) -> Dict[str, float]:
|
def get_size(self) -> Dict[str, Any]:
|
||||||
"""Get board size"""
|
"""Get board size"""
|
||||||
# TODO: Implement using existing SWIG code
|
# TODO: Implement using existing SWIG code
|
||||||
raise NotImplementedError("get_size not yet wrapped")
|
raise NotImplementedError("get_size not yet wrapped")
|
||||||
@@ -173,7 +177,7 @@ class SWIGBoardAPI(BoardAPI):
|
|||||||
from commands.component import ComponentCommands
|
from commands.component import ComponentCommands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = ComponentCommands.get_component_list()
|
result = ComponentCommands(board=self._board).get_component_list({})
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
return result.get("components", [])
|
return result.get("components", [])
|
||||||
return []
|
return []
|
||||||
@@ -189,17 +193,20 @@ class SWIGBoardAPI(BoardAPI):
|
|||||||
y: float,
|
y: float,
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
layer: str = "F.Cu",
|
layer: str = "F.Cu",
|
||||||
|
value: str = "",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Place component using existing implementation"""
|
"""Place component using existing implementation"""
|
||||||
from commands.component import ComponentCommands
|
from commands.component import ComponentCommands
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = ComponentCommands.place_component(
|
result = ComponentCommands(board=self._board).place_component(
|
||||||
component_id=footprint,
|
{
|
||||||
position={"x": x, "y": y, "unit": "mm"},
|
"componentId": footprint,
|
||||||
reference=reference,
|
"position": {"x": x, "y": y, "unit": "mm"},
|
||||||
rotation=rotation,
|
"reference": reference,
|
||||||
layer=layer,
|
"rotation": rotation,
|
||||||
|
"layer": layer,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return result.get("success", False)
|
return result.get("success", False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
|
from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
|
||||||
@@ -241,7 +242,7 @@ except ImportError as e:
|
|||||||
class KiCADInterface:
|
class KiCADInterface:
|
||||||
"""Main interface class to handle KiCAD operations"""
|
"""Main interface class to handle KiCAD operations"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
"""Initialize the interface and command handlers"""
|
"""Initialize the interface and command handlers"""
|
||||||
self.board = None
|
self.board = None
|
||||||
self.project_filename = None
|
self.project_filename = None
|
||||||
@@ -560,7 +561,7 @@ class KiCADInterface:
|
|||||||
"connect_passthrough",
|
"connect_passthrough",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _auto_save_board(self):
|
def _auto_save_board(self) -> None:
|
||||||
"""Save board to disk after SWIG mutations.
|
"""Save board to disk after SWIG mutations.
|
||||||
Called automatically after every board-mutating SWIG command so that
|
Called automatically after every board-mutating SWIG command so that
|
||||||
data is not lost if Claude hits the context limit before save_project.
|
data is not lost if Claude hits the context limit before save_project.
|
||||||
@@ -574,7 +575,7 @@ class KiCADInterface:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Auto-save failed: {e}")
|
logger.warning(f"Auto-save failed: {e}")
|
||||||
|
|
||||||
def _update_command_handlers(self):
|
def _update_command_handlers(self) -> None:
|
||||||
"""Update board reference in all command handlers"""
|
"""Update board reference in all command handlers"""
|
||||||
logger.debug("Updating board reference in command handlers")
|
logger.debug("Updating board reference in command handlers")
|
||||||
self.project_commands.board = self.board
|
self.project_commands.board = self.board
|
||||||
@@ -586,7 +587,7 @@ class KiCADInterface:
|
|||||||
self.freerouting_commands.board = self.board
|
self.freerouting_commands.board = self.board
|
||||||
|
|
||||||
# Schematic command handlers
|
# Schematic command handlers
|
||||||
def _handle_create_schematic(self, params):
|
def _handle_create_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create a new schematic"""
|
"""Create a new schematic"""
|
||||||
logger.info("Creating schematic")
|
logger.info("Creating schematic")
|
||||||
try:
|
try:
|
||||||
@@ -623,7 +624,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error creating schematic: {str(e)}")
|
logger.error(f"Error creating schematic: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_load_schematic(self, params):
|
def _handle_load_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Load an existing schematic"""
|
"""Load an existing schematic"""
|
||||||
logger.info("Loading schematic")
|
logger.info("Loading schematic")
|
||||||
try:
|
try:
|
||||||
@@ -644,7 +645,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error loading schematic: {str(e)}")
|
logger.error(f"Error loading schematic: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_place_component(self, params):
|
def _handle_place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Place a component on the PCB, with project-local fp-lib-table support.
|
"""Place a component on the PCB, with project-local fp-lib-table support.
|
||||||
If boardPath is given and differs from the currently loaded board, the
|
If boardPath is given and differs from the currently loaded board, the
|
||||||
board is reloaded from boardPath before placing — prevents silent failures
|
board is reloaded from boardPath before placing — prevents silent failures
|
||||||
@@ -679,7 +680,7 @@ class KiCADInterface:
|
|||||||
|
|
||||||
return self.component_commands.place_component(params)
|
return self.component_commands.place_component(params)
|
||||||
|
|
||||||
def _handle_add_schematic_component(self, params):
|
def _handle_add_schematic_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add a component to a schematic using text-based injection (no sexpdata)"""
|
"""Add a component to a schematic using text-based injection (no sexpdata)"""
|
||||||
logger.info("Adding component to schematic")
|
logger.info("Adding component to schematic")
|
||||||
try:
|
try:
|
||||||
@@ -732,7 +733,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_delete_schematic_component(self, params):
|
def _handle_delete_schematic_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Remove a placed symbol from a schematic using text-based manipulation (no skip writes)"""
|
"""Remove a placed symbol from a schematic using text-based manipulation (no skip writes)"""
|
||||||
logger.info("Deleting schematic component")
|
logger.info("Deleting schematic component")
|
||||||
try:
|
try:
|
||||||
@@ -757,7 +758,7 @@ class KiCADInterface:
|
|||||||
with open(sch_file, "r", encoding="utf-8") as f:
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
def find_matching_paren(s, start):
|
def find_matching_paren(s: str, start: int) -> int:
|
||||||
"""Find the closing paren matching the opening paren at start."""
|
"""Find the closing paren matching the opening paren at start."""
|
||||||
depth = 0
|
depth = 0
|
||||||
i = start
|
i = start
|
||||||
@@ -838,7 +839,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_edit_schematic_component(self, params):
|
def _handle_edit_schematic_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Update properties of a placed symbol in a schematic (footprint, value, reference).
|
"""Update properties of a placed symbol in a schematic (footprint, value, reference).
|
||||||
Uses text-based in-place editing – preserves position, UUID and all other fields.
|
Uses text-based in-place editing – preserves position, UUID and all other fields.
|
||||||
"""
|
"""
|
||||||
@@ -883,7 +884,7 @@ class KiCADInterface:
|
|||||||
with open(sch_file, "r", encoding="utf-8") as f:
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
def find_matching_paren(s, start):
|
def find_matching_paren(s: str, start: int) -> int:
|
||||||
"""Find the position of the closing paren matching the opening paren at start."""
|
"""Find the position of the closing paren matching the opening paren at start."""
|
||||||
depth = 0
|
depth = 0
|
||||||
i = start
|
i = start
|
||||||
@@ -928,7 +929,7 @@ class KiCADInterface:
|
|||||||
break
|
break
|
||||||
search_start = end + 1
|
search_start = end + 1
|
||||||
|
|
||||||
if block_start is None:
|
if block_start is None or block_end is None:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Component '{reference}' not found in schematic",
|
"message": f"Component '{reference}' not found in schematic",
|
||||||
@@ -991,7 +992,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_get_schematic_component(self, params):
|
def _handle_get_schematic_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Return full component info: position and all field values with their (at x y angle) positions."""
|
"""Return full component info: position and all field values with their (at x y angle) positions."""
|
||||||
logger.info("Getting schematic component info")
|
logger.info("Getting schematic component info")
|
||||||
try:
|
try:
|
||||||
@@ -1016,7 +1017,7 @@ class KiCADInterface:
|
|||||||
with open(sch_file, "r", encoding="utf-8") as f:
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
def find_matching_paren(s, start):
|
def find_matching_paren(s: str, start: int) -> int:
|
||||||
depth = 0
|
depth = 0
|
||||||
i = start
|
i = start
|
||||||
while i < len(s):
|
while i < len(s):
|
||||||
@@ -1058,7 +1059,7 @@ class KiCADInterface:
|
|||||||
break
|
break
|
||||||
search_start = end + 1
|
search_start = end + 1
|
||||||
|
|
||||||
if block_start is None:
|
if block_start is None or block_end is None:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Component '{reference}' not found in schematic",
|
"message": f"Component '{reference}' not found in schematic",
|
||||||
@@ -1114,7 +1115,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_wire(self, params):
|
def _handle_add_schematic_wire(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add a wire to a schematic using WireManager, with optional pin snapping"""
|
"""Add a wire to a schematic using WireManager, with optional pin snapping"""
|
||||||
logger.info("Adding wire to schematic")
|
logger.info("Adding wire to schematic")
|
||||||
try:
|
try:
|
||||||
@@ -1164,7 +1165,7 @@ class KiCADInterface:
|
|||||||
for pin_num, coords in pin_locs.items():
|
for pin_num, coords in pin_locs.items():
|
||||||
all_pins.append((ref, pin_num, coords))
|
all_pins.append((ref, pin_num, coords))
|
||||||
|
|
||||||
def find_nearest_pin(point, tolerance):
|
def find_nearest_pin(point: Any, tolerance: Any) -> Any:
|
||||||
"""Find the nearest pin within tolerance of a point."""
|
"""Find the nearest pin within tolerance of a point."""
|
||||||
best = None
|
best = None
|
||||||
best_dist = tolerance
|
best_dist = tolerance
|
||||||
@@ -1238,7 +1239,7 @@ class KiCADInterface:
|
|||||||
"errorDetails": traceback.format_exc(),
|
"errorDetails": traceback.format_exc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_add_schematic_junction(self, params):
|
def _handle_add_schematic_junction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add a junction (connection dot) to a schematic using WireManager"""
|
"""Add a junction (connection dot) to a schematic using WireManager"""
|
||||||
logger.info("Adding junction to schematic")
|
logger.info("Adding junction to schematic")
|
||||||
try:
|
try:
|
||||||
@@ -1271,19 +1272,19 @@ class KiCADInterface:
|
|||||||
"errorDetails": traceback.format_exc(),
|
"errorDetails": traceback.format_exc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_list_schematic_libraries(self, params):
|
def _handle_list_schematic_libraries(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List available symbol libraries"""
|
"""List available symbol libraries"""
|
||||||
logger.info("Listing schematic libraries")
|
logger.info("Listing schematic libraries")
|
||||||
try:
|
try:
|
||||||
search_paths = params.get("searchPaths")
|
search_paths = params.get("searchPaths")
|
||||||
|
|
||||||
libraries = LibraryManager.list_available_libraries(search_paths)
|
libraries = SchematicLibraryManager.list_available_libraries(search_paths)
|
||||||
return {"success": True, "libraries": libraries}
|
return {"success": True, "libraries": libraries}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing schematic libraries: {str(e)}")
|
logger.error(f"Error listing schematic libraries: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_find_unconnected_pins(self, params):
|
def _handle_find_unconnected_pins(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List component pins with no wire/label/power symbol touching them"""
|
"""List component pins with no wire/label/power symbol touching them"""
|
||||||
logger.info("Finding unconnected pins")
|
logger.info("Finding unconnected pins")
|
||||||
try:
|
try:
|
||||||
@@ -1303,7 +1304,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error finding unconnected pins: {e}")
|
logger.error(f"Error finding unconnected pins: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_check_wire_collisions(self, params):
|
def _handle_check_wire_collisions(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Detect wires passing through component bodies without connecting to pins"""
|
"""Detect wires passing through component bodies without connecting to pins"""
|
||||||
logger.info("Checking wire collisions")
|
logger.info("Checking wire collisions")
|
||||||
try:
|
try:
|
||||||
@@ -1327,7 +1328,7 @@ class KiCADInterface:
|
|||||||
# Footprint handlers #
|
# Footprint handlers #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def _handle_create_footprint(self, params):
|
def _handle_create_footprint(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create a new .kicad_mod footprint file in a .pretty library."""
|
"""Create a new .kicad_mod footprint file in a .pretty library."""
|
||||||
logger.info(f"create_footprint: {params.get('name')} in {params.get('libraryPath')}")
|
logger.info(f"create_footprint: {params.get('name')} in {params.get('libraryPath')}")
|
||||||
try:
|
try:
|
||||||
@@ -1349,7 +1350,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"create_footprint error: {e}")
|
logger.error(f"create_footprint error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_edit_footprint_pad(self, params):
|
def _handle_edit_footprint_pad(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Edit an existing pad in a .kicad_mod file."""
|
"""Edit an existing pad in a .kicad_mod file."""
|
||||||
logger.info(
|
logger.info(
|
||||||
f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}"
|
f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}"
|
||||||
@@ -1368,7 +1369,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"edit_footprint_pad error: {e}")
|
logger.error(f"edit_footprint_pad error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_list_footprint_libraries(self, params):
|
def _handle_list_footprint_libraries(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List .pretty footprint libraries and their contents."""
|
"""List .pretty footprint libraries and their contents."""
|
||||||
logger.info("list_footprint_libraries")
|
logger.info("list_footprint_libraries")
|
||||||
try:
|
try:
|
||||||
@@ -1378,7 +1379,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"list_footprint_libraries error: {e}")
|
logger.error(f"list_footprint_libraries error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_register_footprint_library(self, params):
|
def _handle_register_footprint_library(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Register a .pretty library in KiCAD's fp-lib-table."""
|
"""Register a .pretty library in KiCAD's fp-lib-table."""
|
||||||
logger.info(f"register_footprint_library: {params.get('libraryPath')}")
|
logger.info(f"register_footprint_library: {params.get('libraryPath')}")
|
||||||
try:
|
try:
|
||||||
@@ -1398,7 +1399,7 @@ class KiCADInterface:
|
|||||||
# Symbol creator handlers #
|
# Symbol creator handlers #
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def _handle_create_symbol(self, params):
|
def _handle_create_symbol(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create a new symbol in a .kicad_sym library."""
|
"""Create a new symbol in a .kicad_sym library."""
|
||||||
logger.info(f"create_symbol: {params.get('name')} in {params.get('libraryPath')}")
|
logger.info(f"create_symbol: {params.get('name')} in {params.get('libraryPath')}")
|
||||||
try:
|
try:
|
||||||
@@ -1422,7 +1423,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"create_symbol error: {e}")
|
logger.error(f"create_symbol error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_delete_symbol(self, params):
|
def _handle_delete_symbol(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Delete a symbol from a .kicad_sym library."""
|
"""Delete a symbol from a .kicad_sym library."""
|
||||||
logger.info(f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}")
|
logger.info(f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}")
|
||||||
try:
|
try:
|
||||||
@@ -1435,7 +1436,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"delete_symbol error: {e}")
|
logger.error(f"delete_symbol error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_list_symbols_in_library(self, params):
|
def _handle_list_symbols_in_library(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List all symbols in a .kicad_sym file."""
|
"""List all symbols in a .kicad_sym file."""
|
||||||
logger.info(f"list_symbols_in_library: {params.get('libraryPath')}")
|
logger.info(f"list_symbols_in_library: {params.get('libraryPath')}")
|
||||||
try:
|
try:
|
||||||
@@ -1447,7 +1448,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"list_symbols_in_library error: {e}")
|
logger.error(f"list_symbols_in_library error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_register_symbol_library(self, params):
|
def _handle_register_symbol_library(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Register a .kicad_sym library in KiCAD's sym-lib-table."""
|
"""Register a .kicad_sym library in KiCAD's sym-lib-table."""
|
||||||
logger.info(f"register_symbol_library: {params.get('libraryPath')}")
|
logger.info(f"register_symbol_library: {params.get('libraryPath')}")
|
||||||
try:
|
try:
|
||||||
@@ -1463,7 +1464,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"register_symbol_library error: {e}")
|
logger.error(f"register_symbol_library error: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
def _handle_export_schematic_pdf(self, params):
|
def _handle_export_schematic_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Export schematic to PDF"""
|
"""Export schematic to PDF"""
|
||||||
logger.info("Exporting schematic to PDF")
|
logger.info("Exporting schematic to PDF")
|
||||||
try:
|
try:
|
||||||
@@ -1512,7 +1513,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error exporting schematic to PDF: {str(e)}")
|
logger.error(f"Error exporting schematic to PDF: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_net_label(self, params):
|
def _handle_add_schematic_net_label(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add a net label to schematic using WireManager"""
|
"""Add a net label to schematic using WireManager"""
|
||||||
logger.info("Adding net label to schematic")
|
logger.info("Adding net label to schematic")
|
||||||
try:
|
try:
|
||||||
@@ -1558,7 +1559,7 @@ class KiCADInterface:
|
|||||||
"errorDetails": traceback.format_exc(),
|
"errorDetails": traceback.format_exc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_connect_to_net(self, params):
|
def _handle_connect_to_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Connect a component pin to a named net using wire stub and label"""
|
"""Connect a component pin to a named net using wire stub and label"""
|
||||||
logger.info("Connecting component pin to net")
|
logger.info("Connecting component pin to net")
|
||||||
try:
|
try:
|
||||||
@@ -1595,7 +1596,7 @@ class KiCADInterface:
|
|||||||
"errorDetails": traceback.format_exc(),
|
"errorDetails": traceback.format_exc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_connect_passthrough(self, params):
|
def _handle_connect_passthrough(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Connect all pins of source connector to matching pins of target connector"""
|
"""Connect all pins of source connector to matching pins of target connector"""
|
||||||
logger.info("Connecting passthrough between two connectors")
|
logger.info("Connecting passthrough between two connectors")
|
||||||
try:
|
try:
|
||||||
@@ -1632,7 +1633,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_get_schematic_pin_locations(self, params):
|
def _handle_get_schematic_pin_locations(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Return exact pin endpoint coordinates for a schematic component"""
|
"""Return exact pin endpoint coordinates for a schematic component"""
|
||||||
logger.info("Getting schematic pin locations")
|
logger.info("Getting schematic pin locations")
|
||||||
try:
|
try:
|
||||||
@@ -1687,7 +1688,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_get_schematic_view(self, params):
|
def _handle_get_schematic_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get a rasterised image of the schematic (SVG export → optional PNG conversion)"""
|
"""Get a rasterised image of the schematic (SVG export → optional PNG conversion)"""
|
||||||
logger.info("Getting schematic view")
|
logger.info("Getting schematic view")
|
||||||
import base64
|
import base64
|
||||||
@@ -1776,7 +1777,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_list_schematic_components(self, params):
|
def _handle_list_schematic_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List all components in a schematic"""
|
"""List all components in a schematic"""
|
||||||
logger.info("Listing schematic components")
|
logger.info("Listing schematic components")
|
||||||
try:
|
try:
|
||||||
@@ -1869,7 +1870,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_list_schematic_nets(self, params):
|
def _handle_list_schematic_nets(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List all nets in a schematic with their connections"""
|
"""List all nets in a schematic with their connections"""
|
||||||
logger.info("Listing schematic nets")
|
logger.info("Listing schematic nets")
|
||||||
try:
|
try:
|
||||||
@@ -1915,7 +1916,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_list_schematic_wires(self, params):
|
def _handle_list_schematic_wires(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List all wires in a schematic"""
|
"""List all wires in a schematic"""
|
||||||
logger.info("Listing schematic wires")
|
logger.info("Listing schematic wires")
|
||||||
try:
|
try:
|
||||||
@@ -1958,7 +1959,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_list_schematic_labels(self, params):
|
def _handle_list_schematic_labels(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List all net labels and power flags in a schematic"""
|
"""List all net labels and power flags in a schematic"""
|
||||||
logger.info("Listing schematic labels")
|
logger.info("Listing schematic labels")
|
||||||
try:
|
try:
|
||||||
@@ -2037,7 +2038,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_move_schematic_component(self, params):
|
def _handle_move_schematic_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Move a schematic component to a new position, dragging connected wires."""
|
"""Move a schematic component to a new position, dragging connected wires."""
|
||||||
logger.info("Moving schematic component")
|
logger.info("Moving schematic component")
|
||||||
try:
|
try:
|
||||||
@@ -2121,7 +2122,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_rotate_schematic_component(self, params):
|
def _handle_rotate_schematic_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Rotate a schematic component"""
|
"""Rotate a schematic component"""
|
||||||
logger.info("Rotating schematic component")
|
logger.info("Rotating schematic component")
|
||||||
try:
|
try:
|
||||||
@@ -2171,7 +2172,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_annotate_schematic(self, params):
|
def _handle_annotate_schematic(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Annotate unannotated components in schematic (R? -> R1, R2, ...)"""
|
"""Annotate unannotated components in schematic (R? -> R1, R2, ...)"""
|
||||||
logger.info("Annotating schematic")
|
logger.info("Annotating schematic")
|
||||||
try:
|
try:
|
||||||
@@ -2249,7 +2250,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_delete_schematic_wire(self, params):
|
def _handle_delete_schematic_wire(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Delete a wire from the schematic matching start/end points"""
|
"""Delete a wire from the schematic matching start/end points"""
|
||||||
logger.info("Deleting schematic wire")
|
logger.info("Deleting schematic wire")
|
||||||
try:
|
try:
|
||||||
@@ -2280,7 +2281,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_delete_schematic_net_label(self, params):
|
def _handle_delete_schematic_net_label(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Delete a net label from the schematic"""
|
"""Delete a net label from the schematic"""
|
||||||
logger.info("Deleting schematic net label")
|
logger.info("Deleting schematic net label")
|
||||||
try:
|
try:
|
||||||
@@ -2315,7 +2316,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_export_schematic_svg(self, params):
|
def _handle_export_schematic_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Export schematic to SVG using kicad-cli"""
|
"""Export schematic to SVG using kicad-cli"""
|
||||||
logger.info("Exporting schematic SVG")
|
logger.info("Exporting schematic SVG")
|
||||||
import glob
|
import glob
|
||||||
@@ -2389,7 +2390,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error exporting schematic SVG: {e}")
|
logger.error(f"Error exporting schematic SVG: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_get_net_connections(self, params):
|
def _handle_get_net_connections(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get all connections for a named net"""
|
"""Get all connections for a named net"""
|
||||||
logger.info("Getting net connections")
|
logger.info("Getting net connections")
|
||||||
try:
|
try:
|
||||||
@@ -2409,7 +2410,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error getting net connections: {str(e)}")
|
logger.error(f"Error getting net connections: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_get_wire_connections(self, params):
|
def _handle_get_wire_connections(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Find all component pins reachable from a point via connected wires"""
|
"""Find all component pins reachable from a point via connected wires"""
|
||||||
logger.info("Getting wire connections")
|
logger.info("Getting wire connections")
|
||||||
try:
|
try:
|
||||||
@@ -2456,7 +2457,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_run_erc(self, params):
|
def _handle_run_erc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Run Electrical Rules Check on a schematic via kicad-cli"""
|
"""Run Electrical Rules Check on a schematic via kicad-cli"""
|
||||||
logger.info("Running ERC on schematic")
|
logger.info("Running ERC on schematic")
|
||||||
import os
|
import os
|
||||||
@@ -2552,7 +2553,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error running ERC: {str(e)}")
|
logger.error(f"Error running ERC: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_generate_netlist(self, params):
|
def _handle_generate_netlist(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Generate netlist from schematic"""
|
"""Generate netlist from schematic"""
|
||||||
logger.info("Generating netlist from schematic")
|
logger.info("Generating netlist from schematic")
|
||||||
try:
|
try:
|
||||||
@@ -2571,7 +2572,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error generating netlist: {str(e)}")
|
logger.error(f"Error generating netlist: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_sync_schematic_to_board(self, params):
|
def _handle_sync_schematic_to_board(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Sync schematic netlist to PCB board (equivalent to KiCAD F8 'Update PCB from Schematic').
|
"""Sync schematic netlist to PCB board (equivalent to KiCAD F8 'Update PCB from Schematic').
|
||||||
Reads net connections from the schematic and assigns them to the matching pads in the PCB.
|
Reads net connections from the schematic and assigns them to the matching pads in the PCB.
|
||||||
"""
|
"""
|
||||||
@@ -2694,7 +2695,7 @@ class KiCADInterface:
|
|||||||
# Schematic analysis tools (read-only)
|
# Schematic analysis tools (read-only)
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
def _handle_get_schematic_view_region(self, params):
|
def _handle_get_schematic_view_region(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Export a cropped region of the schematic as an image"""
|
"""Export a cropped region of the schematic as an image"""
|
||||||
logger.info("Exporting schematic view region")
|
logger.info("Exporting schematic view region")
|
||||||
import base64
|
import base64
|
||||||
@@ -2809,7 +2810,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_find_overlapping_elements(self, params):
|
def _handle_find_overlapping_elements(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Detect spatially overlapping symbols, wires, and labels"""
|
"""Detect spatially overlapping symbols, wires, and labels"""
|
||||||
logger.info("Finding overlapping elements in schematic")
|
logger.info("Finding overlapping elements in schematic")
|
||||||
try:
|
try:
|
||||||
@@ -2835,7 +2836,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_get_elements_in_region(self, params):
|
def _handle_get_elements_in_region(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List all wires, labels, and symbols within a rectangular region"""
|
"""List all wires, labels, and symbols within a rectangular region"""
|
||||||
logger.info("Getting elements in schematic region")
|
logger.info("Getting elements in schematic region")
|
||||||
try:
|
try:
|
||||||
@@ -2865,7 +2866,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_find_wires_crossing_symbols(self, params):
|
def _handle_find_wires_crossing_symbols(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Find wires that cross over component symbol bodies"""
|
"""Find wires that cross over component symbol bodies"""
|
||||||
logger.info("Finding wires crossing symbols in schematic")
|
logger.info("Finding wires crossing symbols in schematic")
|
||||||
try:
|
try:
|
||||||
@@ -2891,7 +2892,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_import_svg_logo(self, params):
|
def _handle_import_svg_logo(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Import an SVG file as PCB graphic polygons on the silkscreen"""
|
"""Import an SVG file as PCB graphic polygons on the silkscreen"""
|
||||||
logger.info("Importing SVG logo into PCB")
|
logger.info("Importing SVG logo into PCB")
|
||||||
try:
|
try:
|
||||||
@@ -2938,7 +2939,7 @@ class KiCADInterface:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_snapshot_project(self, params):
|
def _handle_snapshot_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Copy the entire project folder to a snapshot directory for checkpoint/resume."""
|
"""Copy the entire project folder to a snapshot directory for checkpoint/resume."""
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -3022,7 +3023,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"snapshot_project error: {e}")
|
logger.error(f"snapshot_project error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_check_kicad_ui(self, params):
|
def _handle_check_kicad_ui(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Check if KiCAD UI is running"""
|
"""Check if KiCAD UI is running"""
|
||||||
logger.info("Checking if KiCAD UI is running")
|
logger.info("Checking if KiCAD UI is running")
|
||||||
try:
|
try:
|
||||||
@@ -3040,7 +3041,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error checking KiCAD UI status: {str(e)}")
|
logger.error(f"Error checking KiCAD UI status: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_launch_kicad_ui(self, params):
|
def _handle_launch_kicad_ui(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Launch KiCAD UI"""
|
"""Launch KiCAD UI"""
|
||||||
logger.info("Launching KiCAD UI")
|
logger.info("Launching KiCAD UI")
|
||||||
try:
|
try:
|
||||||
@@ -3059,7 +3060,7 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error launching KiCAD UI: {str(e)}")
|
logger.error(f"Error launching KiCAD UI: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_refill_zones(self, params):
|
def _handle_refill_zones(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Refill all copper pour zones on the board.
|
"""Refill all copper pour zones on the board.
|
||||||
|
|
||||||
pcbnew.ZONE_FILLER.Fill() can cause a C++ access violation (0xC0000005)
|
pcbnew.ZONE_FILLER.Fill() can cause a C++ access violation (0xC0000005)
|
||||||
@@ -3146,7 +3147,7 @@ print("ok")
|
|||||||
# These methods are called automatically when IPC is available
|
# These methods are called automatically when IPC is available
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def _ipc_route_trace(self, params):
|
def _ipc_route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for route_trace - adds track with real-time UI update"""
|
"""IPC handler for route_trace - adds track with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
# Extract parameters matching the existing route_trace interface
|
# Extract parameters matching the existing route_trace interface
|
||||||
@@ -3189,7 +3190,7 @@ print("ok")
|
|||||||
logger.error(f"IPC route_trace error: {e}")
|
logger.error(f"IPC route_trace error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_add_via(self, params):
|
def _ipc_add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for add_via - adds via with real-time UI update"""
|
"""IPC handler for add_via - adds via with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
position = params.get("position", {})
|
position = params.get("position", {})
|
||||||
@@ -3222,7 +3223,7 @@ print("ok")
|
|||||||
logger.error(f"IPC add_via error: {e}")
|
logger.error(f"IPC add_via error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_add_net(self, params):
|
def _ipc_add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for add_net"""
|
"""IPC handler for add_net"""
|
||||||
# Note: Net creation via IPC is limited - nets are typically created
|
# Note: Net creation via IPC is limited - nets are typically created
|
||||||
# when components are placed. Return success for compatibility.
|
# when components are placed. Return success for compatibility.
|
||||||
@@ -3234,7 +3235,7 @@ print("ok")
|
|||||||
"net": {"name": name},
|
"net": {"name": name},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _ipc_add_copper_pour(self, params):
|
def _ipc_add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for add_copper_pour - adds zone with real-time UI update"""
|
"""IPC handler for add_copper_pour - adds zone with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
layer = params.get("layer", "F.Cu")
|
layer = params.get("layer", "F.Cu")
|
||||||
@@ -3289,7 +3290,7 @@ print("ok")
|
|||||||
logger.error(f"IPC add_copper_pour error: {e}")
|
logger.error(f"IPC add_copper_pour error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_refill_zones(self, params):
|
def _ipc_refill_zones(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for refill_zones - refills all zones with real-time UI update"""
|
"""IPC handler for refill_zones - refills all zones with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
success = self.ipc_board_api.refill_zones()
|
success = self.ipc_board_api.refill_zones()
|
||||||
@@ -3304,7 +3305,7 @@ print("ok")
|
|||||||
logger.error(f"IPC refill_zones error: {e}")
|
logger.error(f"IPC refill_zones error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_add_text(self, params):
|
def _ipc_add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for add_text/add_board_text - adds text with real-time UI update"""
|
"""IPC handler for add_text/add_board_text - adds text with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
text = params.get("text", "")
|
text = params.get("text", "")
|
||||||
@@ -3331,7 +3332,7 @@ print("ok")
|
|||||||
logger.error(f"IPC add_text error: {e}")
|
logger.error(f"IPC add_text error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_set_board_size(self, params):
|
def _ipc_set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for set_board_size"""
|
"""IPC handler for set_board_size"""
|
||||||
try:
|
try:
|
||||||
width = params.get("width", 100)
|
width = params.get("width", 100)
|
||||||
@@ -3353,7 +3354,7 @@ print("ok")
|
|||||||
logger.error(f"IPC set_board_size error: {e}")
|
logger.error(f"IPC set_board_size error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_get_board_info(self, params):
|
def _ipc_get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for get_board_info"""
|
"""IPC handler for get_board_info"""
|
||||||
try:
|
try:
|
||||||
size = self.ipc_board_api.get_size()
|
size = self.ipc_board_api.get_size()
|
||||||
@@ -3378,7 +3379,7 @@ print("ok")
|
|||||||
logger.error(f"IPC get_board_info error: {e}")
|
logger.error(f"IPC get_board_info error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_place_component(self, params):
|
def _ipc_place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for place_component - places component with real-time UI update"""
|
"""IPC handler for place_component - places component with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
reference = params.get("reference", params.get("componentId", ""))
|
reference = params.get("reference", params.get("componentId", ""))
|
||||||
@@ -3419,7 +3420,7 @@ print("ok")
|
|||||||
logger.error(f"IPC place_component error: {e}")
|
logger.error(f"IPC place_component error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_move_component(self, params):
|
def _ipc_move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for move_component - moves component with real-time UI update"""
|
"""IPC handler for move_component - moves component with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
reference = params.get("reference", params.get("componentId", ""))
|
reference = params.get("reference", params.get("componentId", ""))
|
||||||
@@ -3444,7 +3445,7 @@ print("ok")
|
|||||||
logger.error(f"IPC move_component error: {e}")
|
logger.error(f"IPC move_component error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_delete_component(self, params):
|
def _ipc_delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for delete_component - deletes component with real-time UI update"""
|
"""IPC handler for delete_component - deletes component with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
reference = params.get("reference", params.get("componentId", ""))
|
reference = params.get("reference", params.get("componentId", ""))
|
||||||
@@ -3463,7 +3464,7 @@ print("ok")
|
|||||||
logger.error(f"IPC delete_component error: {e}")
|
logger.error(f"IPC delete_component error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_get_component_list(self, params):
|
def _ipc_get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for get_component_list"""
|
"""IPC handler for get_component_list"""
|
||||||
try:
|
try:
|
||||||
components = self.ipc_board_api.list_components()
|
components = self.ipc_board_api.list_components()
|
||||||
@@ -3473,7 +3474,7 @@ print("ok")
|
|||||||
logger.error(f"IPC get_component_list error: {e}")
|
logger.error(f"IPC get_component_list error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_save_project(self, params):
|
def _ipc_save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for save_project"""
|
"""IPC handler for save_project"""
|
||||||
try:
|
try:
|
||||||
success = self.ipc_board_api.save()
|
success = self.ipc_board_api.save()
|
||||||
@@ -3486,14 +3487,14 @@ print("ok")
|
|||||||
logger.error(f"IPC save_project error: {e}")
|
logger.error(f"IPC save_project error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_delete_trace(self, params):
|
def _ipc_delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for delete_trace - Note: IPC doesn't support direct trace deletion yet"""
|
"""IPC handler for delete_trace - Note: IPC doesn't support direct trace deletion yet"""
|
||||||
# IPC API doesn't have a direct delete track method
|
# IPC API doesn't have a direct delete track method
|
||||||
# Fall back to SWIG for this operation
|
# Fall back to SWIG for this operation
|
||||||
logger.info("delete_trace: Falling back to SWIG (IPC doesn't support trace deletion)")
|
logger.info("delete_trace: Falling back to SWIG (IPC doesn't support trace deletion)")
|
||||||
return self.routing_commands.delete_trace(params)
|
return self.routing_commands.delete_trace(params)
|
||||||
|
|
||||||
def _ipc_get_nets_list(self, params):
|
def _ipc_get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for get_nets_list - gets nets with real-time data"""
|
"""IPC handler for get_nets_list - gets nets with real-time data"""
|
||||||
try:
|
try:
|
||||||
nets = self.ipc_board_api.get_nets()
|
nets = self.ipc_board_api.get_nets()
|
||||||
@@ -3503,7 +3504,7 @@ print("ok")
|
|||||||
logger.error(f"IPC get_nets_list error: {e}")
|
logger.error(f"IPC get_nets_list error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_add_board_outline(self, params):
|
def _ipc_add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for add_board_outline - adds board edge with real-time UI update.
|
"""IPC handler for add_board_outline - adds board edge with real-time UI update.
|
||||||
Rounded rectangles are delegated to the SWIG path because the IPC BoardSegment
|
Rounded rectangles are delegated to the SWIG path because the IPC BoardSegment
|
||||||
type cannot represent arcs; the SWIG path writes directly to the .kicad_pcb file
|
type cannot represent arcs; the SWIG path writes directly to the .kicad_pcb file
|
||||||
@@ -3566,7 +3567,7 @@ print("ok")
|
|||||||
logger.error(f"IPC add_board_outline error: {e}")
|
logger.error(f"IPC add_board_outline error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_add_mounting_hole(self, params):
|
def _ipc_add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for add_mounting_hole - adds mounting hole with real-time UI update"""
|
"""IPC handler for add_mounting_hole - adds mounting hole with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
from kipy.board_types import BoardCircle
|
from kipy.board_types import BoardCircle
|
||||||
@@ -3601,7 +3602,7 @@ print("ok")
|
|||||||
logger.error(f"IPC add_mounting_hole error: {e}")
|
logger.error(f"IPC add_mounting_hole error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_get_layer_list(self, params):
|
def _ipc_get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for get_layer_list - gets enabled layers"""
|
"""IPC handler for get_layer_list - gets enabled layers"""
|
||||||
try:
|
try:
|
||||||
layers = self.ipc_board_api.get_enabled_layers()
|
layers = self.ipc_board_api.get_enabled_layers()
|
||||||
@@ -3611,7 +3612,7 @@ print("ok")
|
|||||||
logger.error(f"IPC get_layer_list error: {e}")
|
logger.error(f"IPC get_layer_list error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_rotate_component(self, params):
|
def _ipc_rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for rotate_component - rotates component with real-time UI update"""
|
"""IPC handler for rotate_component - rotates component with real-time UI update"""
|
||||||
try:
|
try:
|
||||||
reference = params.get("reference", params.get("componentId", ""))
|
reference = params.get("reference", params.get("componentId", ""))
|
||||||
@@ -3653,7 +3654,7 @@ print("ok")
|
|||||||
logger.error(f"IPC rotate_component error: {e}")
|
logger.error(f"IPC rotate_component error: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _ipc_get_component_properties(self, params):
|
def _ipc_get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""IPC handler for get_component_properties - gets detailed component info"""
|
"""IPC handler for get_component_properties - gets detailed component info"""
|
||||||
try:
|
try:
|
||||||
reference = params.get("reference", params.get("componentId", ""))
|
reference = params.get("reference", params.get("componentId", ""))
|
||||||
@@ -3677,7 +3678,7 @@ print("ok")
|
|||||||
# Legacy IPC command handlers (explicit ipc_* commands)
|
# Legacy IPC command handlers (explicit ipc_* commands)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
def _handle_get_backend_info(self, params):
|
def _handle_get_backend_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get information about the current backend"""
|
"""Get information about the current backend"""
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -3692,7 +3693,7 @@ print("ok")
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_ipc_add_track(self, params):
|
def _handle_ipc_add_track(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add a track using IPC backend (real-time)"""
|
"""Add a track using IPC backend (real-time)"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3718,7 +3719,7 @@ print("ok")
|
|||||||
logger.error(f"Error adding track via IPC: {e}")
|
logger.error(f"Error adding track via IPC: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_ipc_add_via(self, params):
|
def _handle_ipc_add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add a via using IPC backend (real-time)"""
|
"""Add a via using IPC backend (real-time)"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3741,7 +3742,7 @@ print("ok")
|
|||||||
logger.error(f"Error adding via via IPC: {e}")
|
logger.error(f"Error adding via via IPC: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_ipc_add_text(self, params):
|
def _handle_ipc_add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Add text using IPC backend (real-time)"""
|
"""Add text using IPC backend (real-time)"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3766,7 +3767,7 @@ print("ok")
|
|||||||
logger.error(f"Error adding text via IPC: {e}")
|
logger.error(f"Error adding text via IPC: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_ipc_list_components(self, params):
|
def _handle_ipc_list_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""List components using IPC backend"""
|
"""List components using IPC backend"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3778,7 +3779,7 @@ print("ok")
|
|||||||
logger.error(f"Error listing components via IPC: {e}")
|
logger.error(f"Error listing components via IPC: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_ipc_get_tracks(self, params):
|
def _handle_ipc_get_tracks(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get tracks using IPC backend"""
|
"""Get tracks using IPC backend"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3790,7 +3791,7 @@ print("ok")
|
|||||||
logger.error(f"Error getting tracks via IPC: {e}")
|
logger.error(f"Error getting tracks via IPC: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_ipc_get_vias(self, params):
|
def _handle_ipc_get_vias(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get vias using IPC backend"""
|
"""Get vias using IPC backend"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3802,7 +3803,7 @@ print("ok")
|
|||||||
logger.error(f"Error getting vias via IPC: {e}")
|
logger.error(f"Error getting vias via IPC: {e}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_ipc_save_board(self, params):
|
def _handle_ipc_save_board(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Save board using IPC backend"""
|
"""Save board using IPC backend"""
|
||||||
if not self.use_ipc or not self.ipc_board_api:
|
if not self.use_ipc or not self.ipc_board_api:
|
||||||
return {"success": False, "message": "IPC backend not available"}
|
return {"success": False, "message": "IPC backend not available"}
|
||||||
@@ -3819,7 +3820,7 @@ print("ok")
|
|||||||
|
|
||||||
# JLCPCB API handlers
|
# JLCPCB API handlers
|
||||||
|
|
||||||
def _handle_download_jlcpcb_database(self, params):
|
def _handle_download_jlcpcb_database(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Download JLCPCB parts database from JLCSearch API"""
|
"""Download JLCPCB parts database from JLCSearch API"""
|
||||||
try:
|
try:
|
||||||
force = params.get("force", False)
|
force = params.get("force", False)
|
||||||
@@ -3870,7 +3871,7 @@ print("ok")
|
|||||||
"message": f"Failed to download database: {str(e)}",
|
"message": f"Failed to download database: {str(e)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_search_jlcpcb_parts(self, params):
|
def _handle_search_jlcpcb_parts(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Search JLCPCB parts database"""
|
"""Search JLCPCB parts database"""
|
||||||
try:
|
try:
|
||||||
query = params.get("query")
|
query = params.get("query")
|
||||||
@@ -3909,7 +3910,7 @@ print("ok")
|
|||||||
logger.error(f"Error searching JLCPCB parts: {e}", exc_info=True)
|
logger.error(f"Error searching JLCPCB parts: {e}", exc_info=True)
|
||||||
return {"success": False, "message": f"Search failed: {str(e)}"}
|
return {"success": False, "message": f"Search failed: {str(e)}"}
|
||||||
|
|
||||||
def _handle_get_jlcpcb_part(self, params):
|
def _handle_get_jlcpcb_part(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get detailed information for a specific JLCPCB part"""
|
"""Get detailed information for a specific JLCPCB part"""
|
||||||
try:
|
try:
|
||||||
lcsc_number = params.get("lcsc_number")
|
lcsc_number = params.get("lcsc_number")
|
||||||
@@ -3929,7 +3930,7 @@ print("ok")
|
|||||||
logger.error(f"Error getting JLCPCB part: {e}", exc_info=True)
|
logger.error(f"Error getting JLCPCB part: {e}", exc_info=True)
|
||||||
return {"success": False, "message": f"Failed to get part info: {str(e)}"}
|
return {"success": False, "message": f"Failed to get part info: {str(e)}"}
|
||||||
|
|
||||||
def _handle_get_jlcpcb_database_stats(self, params):
|
def _handle_get_jlcpcb_database_stats(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Get statistics about JLCPCB database"""
|
"""Get statistics about JLCPCB database"""
|
||||||
try:
|
try:
|
||||||
stats = self.jlcpcb_parts.get_database_stats()
|
stats = self.jlcpcb_parts.get_database_stats()
|
||||||
@@ -3939,7 +3940,7 @@ print("ok")
|
|||||||
logger.error(f"Error getting database stats: {e}", exc_info=True)
|
logger.error(f"Error getting database stats: {e}", exc_info=True)
|
||||||
return {"success": False, "message": f"Failed to get stats: {str(e)}"}
|
return {"success": False, "message": f"Failed to get stats: {str(e)}"}
|
||||||
|
|
||||||
def _handle_suggest_jlcpcb_alternatives(self, params):
|
def _handle_suggest_jlcpcb_alternatives(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Suggest alternative JLCPCB parts"""
|
"""Suggest alternative JLCPCB parts"""
|
||||||
try:
|
try:
|
||||||
lcsc_number = params.get("lcsc_number")
|
lcsc_number = params.get("lcsc_number")
|
||||||
@@ -3980,7 +3981,7 @@ print("ok")
|
|||||||
"message": f"Failed to suggest alternatives: {str(e)}",
|
"message": f"Failed to suggest alternatives: {str(e)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_enrich_datasheets(self, params):
|
def _handle_enrich_datasheets(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Enrich schematic Datasheet fields from LCSC numbers"""
|
"""Enrich schematic Datasheet fields from LCSC numbers"""
|
||||||
try:
|
try:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -3998,7 +3999,7 @@ print("ok")
|
|||||||
"message": f"Failed to enrich datasheets: {str(e)}",
|
"message": f"Failed to enrich datasheets: {str(e)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_get_datasheet_url(self, params):
|
def _handle_get_datasheet_url(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Return LCSC datasheet and product URLs for a part number"""
|
"""Return LCSC datasheet and product URLs for a part number"""
|
||||||
try:
|
try:
|
||||||
lcsc = params.get("lcsc", "")
|
lcsc = params.get("lcsc", "")
|
||||||
@@ -4024,7 +4025,7 @@ print("ok")
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _write_response(response_fd, response):
|
def _write_response(response_fd: Any, response: Any) -> None:
|
||||||
"""Write a JSON response to the original stdout fd.
|
"""Write a JSON response to the original stdout fd.
|
||||||
|
|
||||||
All response output goes through this function so that stray C-level
|
All response output goes through this function so that stray C-level
|
||||||
@@ -4035,7 +4036,7 @@ def _write_response(response_fd, response):
|
|||||||
os.write(response_fd, payload.encode("utf-8"))
|
os.write(response_fd, payload.encode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
"""Main entry point"""
|
"""Main entry point"""
|
||||||
# --- Redirect stdout so pcbnew C++ noise never reaches the TS host ---
|
# --- Redirect stdout so pcbnew C++ noise never reaches the TS host ---
|
||||||
# Save the real stdout fd for our exclusive JSON response channel.
|
# Save the real stdout fd for our exclusive JSON response channel.
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ RESOURCE_DEFINITIONS = [
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
def handle_resource_read(uri: str, interface) -> Dict[str, Any]:
|
def handle_resource_read(uri: str, interface: Any) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Handle reading a resource by URI
|
Handle reading a resource by URI
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ def handle_resource_read(uri: str, interface) -> Dict[str, Any]:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
def _get_project_info(interface) -> Dict[str, Any]:
|
def _get_project_info(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get current project information"""
|
"""Get current project information"""
|
||||||
result = interface.project_commands.get_project_info({})
|
result = interface.project_commands.get_project_info({})
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ def _get_project_info(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_board_info(interface) -> Dict[str, Any]:
|
def _get_board_info(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get board properties and metadata"""
|
"""Get board properties and metadata"""
|
||||||
result = interface.board_commands.get_board_info({})
|
result = interface.board_commands.get_board_info({})
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ def _get_board_info(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_components(interface) -> Dict[str, Any]:
|
def _get_components(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get list of all components"""
|
"""Get list of all components"""
|
||||||
result = interface.component_commands.get_component_list({})
|
result = interface.component_commands.get_component_list({})
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ def _get_components(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_nets(interface) -> Dict[str, Any]:
|
def _get_nets(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get list of electrical nets"""
|
"""Get list of electrical nets"""
|
||||||
result = interface.routing_commands.get_nets_list({})
|
result = interface.routing_commands.get_nets_list({})
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ def _get_nets(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_layers(interface) -> Dict[str, Any]:
|
def _get_layers(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get layer stack information"""
|
"""Get layer stack information"""
|
||||||
result = interface.board_commands.get_layer_list({})
|
result = interface.board_commands.get_layer_list({})
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ def _get_layers(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_design_rules(interface) -> Dict[str, Any]:
|
def _get_design_rules(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get design rule settings"""
|
"""Get design rule settings"""
|
||||||
result = interface.design_rule_commands.get_design_rules({})
|
result = interface.design_rule_commands.get_design_rules({})
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ def _get_design_rules(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_drc_report(interface) -> Dict[str, Any]:
|
def _get_drc_report(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get DRC violations"""
|
"""Get DRC violations"""
|
||||||
result = interface.design_rule_commands.get_drc_violations({})
|
result = interface.design_rule_commands.get_drc_violations({})
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ def _get_drc_report(interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_board_preview(interface) -> Dict[str, Any]:
|
def _get_board_preview(interface: Any) -> Dict[str, Any]:
|
||||||
"""Get board preview as PNG image"""
|
"""Get board preview as PNG image"""
|
||||||
result = interface.board_commands.get_board_2d_view({"width": 800, "height": 600})
|
result = interface.board_commands.get_board_2d_view({"width": 800, "height": 600})
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Usage:
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
# Add parent directory to path
|
# Add parent directory to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
@@ -29,7 +30,7 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_connection():
|
def test_connection() -> Optional[Any]:
|
||||||
"""Test basic IPC connection to KiCAD."""
|
"""Test basic IPC connection to KiCAD."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 1: IPC Connection")
|
print("TEST 1: IPC Connection")
|
||||||
@@ -62,7 +63,7 @@ def test_connection():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def test_board_access(backend):
|
def test_board_access(backend: Any) -> Optional[Any]:
|
||||||
"""Test board access and component listing."""
|
"""Test board access and component listing."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 2: Board Access")
|
print("TEST 2: Board Access")
|
||||||
@@ -93,7 +94,7 @@ def test_board_access(backend):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def test_board_info(board_api):
|
def test_board_info(board_api: Any) -> bool:
|
||||||
"""Test getting board information."""
|
"""Test getting board information."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 3: Board Information")
|
print("TEST 3: Board Information")
|
||||||
@@ -134,7 +135,7 @@ def test_board_info(board_api):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_track(board_api, interactive=False):
|
def test_realtime_track(board_api: Any, interactive: bool = False) -> bool:
|
||||||
"""Test adding a track in real-time (appears immediately in KiCAD UI)."""
|
"""Test adding a track in real-time (appears immediately in KiCAD UI)."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 4: Real-time Track Addition")
|
print("TEST 4: Real-time Track Addition")
|
||||||
@@ -168,7 +169,7 @@ def test_realtime_track(board_api, interactive=False):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_via(board_api, interactive=False):
|
def test_realtime_via(board_api: Any, interactive: bool = False) -> bool:
|
||||||
"""Test adding a via in real-time (appears immediately in KiCAD UI)."""
|
"""Test adding a via in real-time (appears immediately in KiCAD UI)."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 5: Real-time Via Addition")
|
print("TEST 5: Real-time Via Addition")
|
||||||
@@ -200,7 +201,7 @@ def test_realtime_via(board_api, interactive=False):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_realtime_text(board_api, interactive=False):
|
def test_realtime_text(board_api: Any, interactive: bool = False) -> bool:
|
||||||
"""Test adding text in real-time."""
|
"""Test adding text in real-time."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 6: Real-time Text Addition")
|
print("TEST 6: Real-time Text Addition")
|
||||||
@@ -229,7 +230,7 @@ def test_realtime_text(board_api, interactive=False):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_selection(board_api, interactive=False):
|
def test_selection(board_api: Any, interactive: bool = False) -> bool:
|
||||||
"""Test getting the current selection from KiCAD UI."""
|
"""Test getting the current selection from KiCAD UI."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("TEST 7: UI Selection")
|
print("TEST 7: UI Selection")
|
||||||
@@ -255,7 +256,7 @@ def test_selection(board_api, interactive=False):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run_all_tests(interactive=False):
|
def run_all_tests(interactive: bool = False) -> bool:
|
||||||
"""Run all IPC backend tests."""
|
"""Run all IPC backend tests."""
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("KiCAD IPC Backend Test Suite")
|
print("KiCAD IPC Backend Test Suite")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -121,32 +122,32 @@ class TestDeleteDetectionRegex:
|
|||||||
OLD_PATTERN = re.compile(r"^\s*\(symbol\s+\(lib_id\s+\"", re.MULTILINE)
|
OLD_PATTERN = re.compile(r"^\s*\(symbol\s+\(lib_id\s+\"", re.MULTILINE)
|
||||||
NEW_PATTERN = re.compile(r'\(symbol\s+\(lib_id\s+"')
|
NEW_PATTERN = re.compile(r'\(symbol\s+\(lib_id\s+"')
|
||||||
|
|
||||||
def test_old_regex_fails_on_multiline_format(self):
|
def test_old_regex_fails_on_multiline_format(self) -> None:
|
||||||
"""Regression: old line-by-line regex must NOT match the multi-line format."""
|
"""Regression: old line-by-line regex must NOT match the multi-line format."""
|
||||||
# The old code used re.match on individual lines; simulate that here.
|
# The old code used re.match on individual lines; simulate that here.
|
||||||
lines = PLACED_RESISTOR_MULTILINE.split("\n")
|
lines = PLACED_RESISTOR_MULTILINE.split("\n")
|
||||||
matches = [l for l in lines if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", l)]
|
matches = [l for l in lines if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", l)]
|
||||||
assert matches == [], "Old regex should not match multi-line KiCAD format"
|
assert matches == [], "Old regex should not match multi-line KiCAD format"
|
||||||
|
|
||||||
def test_old_regex_matches_inline_format(self):
|
def test_old_regex_matches_inline_format(self) -> None:
|
||||||
"""Old regex did work on single-line (inline) format."""
|
"""Old regex did work on single-line (inline) format."""
|
||||||
lines = PLACED_RESISTOR_INLINE.split("\n")
|
lines = PLACED_RESISTOR_INLINE.split("\n")
|
||||||
matches = [l for l in lines if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", l)]
|
matches = [l for l in lines if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", l)]
|
||||||
assert len(matches) == 1
|
assert len(matches) == 1
|
||||||
|
|
||||||
def test_new_pattern_matches_multiline_format(self):
|
def test_new_pattern_matches_multiline_format(self) -> None:
|
||||||
"""New content-string pattern must find blocks in multi-line format."""
|
"""New content-string pattern must find blocks in multi-line format."""
|
||||||
assert self.NEW_PATTERN.search(PLACED_RESISTOR_MULTILINE) is not None
|
assert self.NEW_PATTERN.search(PLACED_RESISTOR_MULTILINE) is not None
|
||||||
|
|
||||||
def test_new_pattern_matches_inline_format(self):
|
def test_new_pattern_matches_inline_format(self) -> None:
|
||||||
"""New content-string pattern also works on inline format."""
|
"""New content-string pattern also works on inline format."""
|
||||||
assert self.NEW_PATTERN.search(PLACED_RESISTOR_INLINE) is not None
|
assert self.NEW_PATTERN.search(PLACED_RESISTOR_INLINE) is not None
|
||||||
|
|
||||||
def test_new_pattern_matches_power_symbol_multiline(self):
|
def test_new_pattern_matches_power_symbol_multiline(self) -> None:
|
||||||
"""New pattern must find #PWR030 power symbol in multi-line format."""
|
"""New pattern must find #PWR030 power symbol in multi-line format."""
|
||||||
assert self.NEW_PATTERN.search(PLACED_POWER_SYMBOL_MULTILINE) is not None
|
assert self.NEW_PATTERN.search(PLACED_POWER_SYMBOL_MULTILINE) is not None
|
||||||
|
|
||||||
def test_reference_extraction_from_multiline_block(self):
|
def test_reference_extraction_from_multiline_block(self) -> None:
|
||||||
"""Reference property can be found inside a multi-line block."""
|
"""Reference property can be found inside a multi-line block."""
|
||||||
ref_pattern = re.compile(r'\(property\s+"Reference"\s+"#PWR030"')
|
ref_pattern = re.compile(r'\(property\s+"Reference"\s+"#PWR030"')
|
||||||
assert ref_pattern.search(PLACED_POWER_SYMBOL_MULTILINE) is not None
|
assert ref_pattern.search(PLACED_POWER_SYMBOL_MULTILINE) is not None
|
||||||
@@ -159,49 +160,49 @@ class TestDeleteDetectionRegex:
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
class TestDeleteSchematicComponentIntegration:
|
class TestDeleteSchematicComponentIntegration:
|
||||||
def _get_handler(self):
|
def _get_handler(self) -> Any:
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
iface = KiCADInterface.__new__(KiCADInterface)
|
iface = KiCADInterface.__new__(KiCADInterface)
|
||||||
return iface._handle_delete_schematic_component
|
return iface._handle_delete_schematic_component
|
||||||
|
|
||||||
def test_delete_inline_format_succeeds(self, tmp_path):
|
def test_delete_inline_format_succeeds(self, tmp_path: Any) -> None:
|
||||||
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_INLINE)
|
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_INLINE)
|
||||||
result = self._get_handler()({"schematicPath": str(sch), "reference": "R1"})
|
result = self._get_handler()({"schematicPath": str(sch), "reference": "R1"})
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["deleted_count"] == 1
|
assert result["deleted_count"] == 1
|
||||||
|
|
||||||
def test_delete_multiline_format_succeeds(self, tmp_path):
|
def test_delete_multiline_format_succeeds(self, tmp_path: Any) -> None:
|
||||||
"""Regression: must succeed when KiCAD writes (symbol and (lib_id on separate lines."""
|
"""Regression: must succeed when KiCAD writes (symbol and (lib_id on separate lines."""
|
||||||
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_MULTILINE)
|
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_MULTILINE)
|
||||||
result = self._get_handler()({"schematicPath": str(sch), "reference": "R2"})
|
result = self._get_handler()({"schematicPath": str(sch), "reference": "R2"})
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["deleted_count"] == 1
|
assert result["deleted_count"] == 1
|
||||||
|
|
||||||
def test_delete_power_symbol_multiline_succeeds(self, tmp_path):
|
def test_delete_power_symbol_multiline_succeeds(self, tmp_path: Any) -> None:
|
||||||
"""Regression: #PWR030 multi-line power symbol must be deletable."""
|
"""Regression: #PWR030 multi-line power symbol must be deletable."""
|
||||||
sch = _make_test_schematic(tmp_path, PLACED_POWER_SYMBOL_MULTILINE)
|
sch = _make_test_schematic(tmp_path, PLACED_POWER_SYMBOL_MULTILINE)
|
||||||
result = self._get_handler()({"schematicPath": str(sch), "reference": "#PWR030"})
|
result = self._get_handler()({"schematicPath": str(sch), "reference": "#PWR030"})
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["deleted_count"] == 1
|
assert result["deleted_count"] == 1
|
||||||
|
|
||||||
def test_component_absent_after_delete(self, tmp_path):
|
def test_component_absent_after_delete(self, tmp_path: Any) -> None:
|
||||||
sch = _make_test_schematic(tmp_path, PLACED_POWER_SYMBOL_MULTILINE)
|
sch = _make_test_schematic(tmp_path, PLACED_POWER_SYMBOL_MULTILINE)
|
||||||
self._get_handler()({"schematicPath": str(sch), "reference": "#PWR030"})
|
self._get_handler()({"schematicPath": str(sch), "reference": "#PWR030"})
|
||||||
remaining = sch.read_text(encoding="utf-8")
|
remaining = sch.read_text(encoding="utf-8")
|
||||||
assert '"#PWR030"' not in remaining
|
assert '"#PWR030"' not in remaining
|
||||||
|
|
||||||
def test_unknown_reference_returns_failure(self, tmp_path):
|
def test_unknown_reference_returns_failure(self, tmp_path: Any) -> None:
|
||||||
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_INLINE)
|
sch = _make_test_schematic(tmp_path, PLACED_RESISTOR_INLINE)
|
||||||
result = self._get_handler()({"schematicPath": str(sch), "reference": "U99"})
|
result = self._get_handler()({"schematicPath": str(sch), "reference": "U99"})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "not found" in result["message"]
|
assert "not found" in result["message"]
|
||||||
|
|
||||||
def test_missing_schematic_path_returns_failure(self, tmp_path):
|
def test_missing_schematic_path_returns_failure(self, tmp_path: Any) -> None:
|
||||||
result = self._get_handler()({"reference": "R1"})
|
result = self._get_handler()({"reference": "R1"})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
|
|
||||||
def test_missing_reference_returns_failure(self, tmp_path):
|
def test_missing_reference_returns_failure(self, tmp_path: Any) -> None:
|
||||||
sch = _make_test_schematic(tmp_path)
|
sch = _make_test_schematic(tmp_path)
|
||||||
result = self._get_handler()({"schematicPath": str(sch)})
|
result = self._get_handler()({"schematicPath": str(sch)})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Covers:
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -33,7 +34,7 @@ pcbnew_mock = sys.modules["pcbnew"]
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_pcbnew_mock():
|
def reset_pcbnew_mock() -> Any:
|
||||||
"""Reset pcbnew mock before each test."""
|
"""Reset pcbnew mock before each test."""
|
||||||
pcbnew_mock.reset_mock()
|
pcbnew_mock.reset_mock()
|
||||||
pcbnew_mock.ExportSpecctraDSN.side_effect = None
|
pcbnew_mock.ExportSpecctraDSN.side_effect = None
|
||||||
@@ -44,7 +45,7 @@ def reset_pcbnew_mock():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_board():
|
def mock_board() -> Any:
|
||||||
board = MagicMock()
|
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 = []
|
board.GetTracks.return_value = []
|
||||||
@@ -52,16 +53,16 @@ def mock_board():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cmds(mock_board):
|
def cmds(mock_board: Any) -> Any:
|
||||||
return FreeroutingCommands(board=mock_board)
|
return FreeroutingCommands(board=mock_board)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cmds_no_board():
|
def cmds_no_board() -> Any:
|
||||||
return FreeroutingCommands(board=None)
|
return FreeroutingCommands(board=None)
|
||||||
|
|
||||||
|
|
||||||
def _patch_direct_java():
|
def _patch_direct_java() -> Any:
|
||||||
"""Patch to simulate Java 21+ available locally."""
|
"""Patch to simulate Java 21+ available locally."""
|
||||||
return patch.object(
|
return patch.object(
|
||||||
FreeroutingCommands,
|
FreeroutingCommands,
|
||||||
@@ -70,7 +71,7 @@ def _patch_direct_java():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _patch_docker_mode():
|
def _patch_docker_mode() -> Any:
|
||||||
"""Patch to simulate Docker execution mode."""
|
"""Patch to simulate Docker execution mode."""
|
||||||
return patch.object(
|
return patch.object(
|
||||||
FreeroutingCommands,
|
FreeroutingCommands,
|
||||||
@@ -79,7 +80,7 @@ def _patch_docker_mode():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _patch_no_runtime():
|
def _patch_no_runtime() -> Any:
|
||||||
"""Patch to simulate no Java and no Docker."""
|
"""Patch to simulate no Java and no Docker."""
|
||||||
return patch.object(
|
return patch.object(
|
||||||
FreeroutingCommands,
|
FreeroutingCommands,
|
||||||
@@ -97,7 +98,7 @@ def _patch_no_runtime():
|
|||||||
|
|
||||||
|
|
||||||
class TestCheckFreerouting:
|
class TestCheckFreerouting:
|
||||||
def test_no_java_no_docker(self, cmds):
|
def test_no_java_no_docker(self, cmds: Any) -> None:
|
||||||
with (
|
with (
|
||||||
patch("commands.freerouting._find_java", return_value=None),
|
patch("commands.freerouting._find_java", return_value=None),
|
||||||
patch(
|
patch(
|
||||||
@@ -112,7 +113,7 @@ class TestCheckFreerouting:
|
|||||||
assert result["ready"] is False
|
assert result["ready"] is False
|
||||||
assert result["execution_mode"] == "none"
|
assert result["execution_mode"] == "none"
|
||||||
|
|
||||||
def test_java_too_old_docker_available(self, cmds, tmp_path):
|
def test_java_too_old_docker_available(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
with (
|
with (
|
||||||
@@ -135,7 +136,7 @@ class TestCheckFreerouting:
|
|||||||
assert result["ready"] is True
|
assert result["ready"] is True
|
||||||
assert result["execution_mode"] == "docker"
|
assert result["execution_mode"] == "docker"
|
||||||
|
|
||||||
def test_java_21_direct(self, cmds, tmp_path):
|
def test_java_21_direct(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
with (
|
with (
|
||||||
@@ -165,12 +166,12 @@ class TestCheckFreerouting:
|
|||||||
|
|
||||||
|
|
||||||
class TestExportDsn:
|
class TestExportDsn:
|
||||||
def test_no_board(self, cmds_no_board):
|
def test_no_board(self, cmds_no_board: Any) -> None:
|
||||||
result = cmds_no_board.export_dsn({})
|
result = cmds_no_board.export_dsn({})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "No board" in result["message"]
|
assert "No board" in result["message"]
|
||||||
|
|
||||||
def test_export_success(self, cmds, tmp_path):
|
def test_export_success(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
board_path = str(tmp_path / "test.kicad_pcb")
|
board_path = str(tmp_path / "test.kicad_pcb")
|
||||||
dsn_path = str(tmp_path / "test.dsn")
|
dsn_path = str(tmp_path / "test.dsn")
|
||||||
cmds.board.GetFileName.return_value = board_path
|
cmds.board.GetFileName.return_value = board_path
|
||||||
@@ -182,7 +183,7 @@ class TestExportDsn:
|
|||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["path"] == dsn_path
|
assert result["path"] == dsn_path
|
||||||
|
|
||||||
def test_export_custom_path(self, cmds, tmp_path):
|
def test_export_custom_path(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
output = str(tmp_path / "custom.dsn")
|
output = str(tmp_path / "custom.dsn")
|
||||||
pcbnew_mock.ExportSpecctraDSN.return_value = True
|
pcbnew_mock.ExportSpecctraDSN.return_value = True
|
||||||
Path(output).write_text("(pcb test)")
|
Path(output).write_text("(pcb test)")
|
||||||
@@ -191,7 +192,7 @@ class TestExportDsn:
|
|||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["path"] == output
|
assert result["path"] == output
|
||||||
|
|
||||||
def test_export_failure(self, cmds):
|
def test_export_failure(self, cmds: Any) -> None:
|
||||||
pcbnew_mock.ExportSpecctraDSN.side_effect = Exception("DSN error")
|
pcbnew_mock.ExportSpecctraDSN.side_effect = Exception("DSN error")
|
||||||
result = cmds.export_dsn({})
|
result = cmds.export_dsn({})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
@@ -204,22 +205,22 @@ class TestExportDsn:
|
|||||||
|
|
||||||
|
|
||||||
class TestImportSes:
|
class TestImportSes:
|
||||||
def test_no_board(self, cmds_no_board):
|
def test_no_board(self, cmds_no_board: Any) -> None:
|
||||||
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 result["success"] is False
|
||||||
assert "No board" in result["message"]
|
assert "No board" in result["message"]
|
||||||
|
|
||||||
def test_missing_ses_path(self, cmds):
|
def test_missing_ses_path(self, cmds: Any) -> None:
|
||||||
result = cmds.import_ses({})
|
result = cmds.import_ses({})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Missing sesPath" in result["message"]
|
assert "Missing sesPath" in result["message"]
|
||||||
|
|
||||||
def test_ses_file_not_found(self, cmds):
|
def test_ses_file_not_found(self, cmds: Any) -> None:
|
||||||
result = cmds.import_ses({"sesPath": "/nonexistent/test.ses"})
|
result = cmds.import_ses({"sesPath": "/nonexistent/test.ses"})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "not found" in result["message"]
|
assert "not found" in result["message"]
|
||||||
|
|
||||||
def test_import_success(self, cmds, tmp_path):
|
def test_import_success(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
ses_file = tmp_path / "test.ses"
|
ses_file = tmp_path / "test.ses"
|
||||||
ses_file.write_text("(session test)")
|
ses_file.write_text("(session test)")
|
||||||
|
|
||||||
@@ -229,7 +230,7 @@ class TestImportSes:
|
|||||||
result = cmds.import_ses({"sesPath": str(ses_file)})
|
result = cmds.import_ses({"sesPath": str(ses_file)})
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
||||||
def test_import_failure(self, cmds, tmp_path):
|
def test_import_failure(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
ses_file = tmp_path / "test.ses"
|
ses_file = tmp_path / "test.ses"
|
||||||
ses_file.write_text("(session test)")
|
ses_file.write_text("(session test)")
|
||||||
|
|
||||||
@@ -245,12 +246,12 @@ class TestImportSes:
|
|||||||
|
|
||||||
|
|
||||||
class TestAutoroute:
|
class TestAutoroute:
|
||||||
def test_no_board(self, cmds_no_board):
|
def test_no_board(self, cmds_no_board: Any) -> None:
|
||||||
result = cmds_no_board.autoroute({})
|
result = cmds_no_board.autoroute({})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "No board" in result["message"]
|
assert "No board" in result["message"]
|
||||||
|
|
||||||
def test_no_runtime(self, cmds, tmp_path):
|
def test_no_runtime(self, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
with _patch_no_runtime():
|
with _patch_no_runtime():
|
||||||
@@ -258,13 +259,13 @@ class TestAutoroute:
|
|||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "No suitable Java runtime" in result["message"]
|
assert "No suitable Java runtime" in result["message"]
|
||||||
|
|
||||||
def test_no_jar(self, cmds):
|
def test_no_jar(self, cmds: Any) -> None:
|
||||||
result = cmds.autoroute({"freeroutingJar": "/nonexistent/freerouting.jar"})
|
result = cmds.autoroute({"freeroutingJar": "/nonexistent/freerouting.jar"})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "JAR not found" in result["message"]
|
assert "JAR not found" in result["message"]
|
||||||
|
|
||||||
@patch("commands.freerouting.subprocess.run")
|
@patch("commands.freerouting.subprocess.run")
|
||||||
def test_dsn_export_fails(self, mock_run, cmds, tmp_path):
|
def test_dsn_export_fails(self, mock_run: Any, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
|
|
||||||
@@ -276,7 +277,7 @@ class TestAutoroute:
|
|||||||
assert "DSN export failed" in result["message"]
|
assert "DSN export failed" in result["message"]
|
||||||
|
|
||||||
@patch("commands.freerouting.subprocess.run")
|
@patch("commands.freerouting.subprocess.run")
|
||||||
def test_freerouting_timeout(self, mock_run, cmds, tmp_path):
|
def test_freerouting_timeout(self, mock_run: Any, cmds: Any, tmp_path: Any) -> None:
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
@@ -300,7 +301,7 @@ class TestAutoroute:
|
|||||||
assert "timed out" in result["message"]
|
assert "timed out" in result["message"]
|
||||||
|
|
||||||
@patch("commands.freerouting.subprocess.run")
|
@patch("commands.freerouting.subprocess.run")
|
||||||
def test_full_success_direct(self, mock_run, cmds, tmp_path):
|
def test_full_success_direct(self, mock_run: Any, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
board_dir = tmp_path / "project"
|
board_dir = tmp_path / "project"
|
||||||
@@ -336,7 +337,7 @@ class TestAutoroute:
|
|||||||
assert "elapsed_seconds" in result
|
assert "elapsed_seconds" in result
|
||||||
|
|
||||||
@patch("commands.freerouting.subprocess.run")
|
@patch("commands.freerouting.subprocess.run")
|
||||||
def test_full_success_docker(self, mock_run, cmds, tmp_path):
|
def test_full_success_docker(self, mock_run: Any, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
board_dir = tmp_path / "project"
|
board_dir = tmp_path / "project"
|
||||||
@@ -375,7 +376,7 @@ class TestAutoroute:
|
|||||||
assert "--rm" in call_args
|
assert "--rm" in call_args
|
||||||
|
|
||||||
@patch("commands.freerouting.subprocess.run")
|
@patch("commands.freerouting.subprocess.run")
|
||||||
def test_freerouting_nonzero_exit(self, mock_run, cmds, tmp_path):
|
def test_freerouting_nonzero_exit(self, mock_run: Any, cmds: Any, tmp_path: Any) -> None:
|
||||||
jar = tmp_path / "freerouting.jar"
|
jar = tmp_path / "freerouting.jar"
|
||||||
jar.touch()
|
jar.touch()
|
||||||
board_dir = tmp_path / "project"
|
board_dir = tmp_path / "project"
|
||||||
@@ -404,14 +405,14 @@ class TestAutoroute:
|
|||||||
|
|
||||||
|
|
||||||
class TestFindJava:
|
class TestFindJava:
|
||||||
def test_finds_via_which(self):
|
def test_finds_via_which(self) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
"commands.freerouting.shutil.which",
|
"commands.freerouting.shutil.which",
|
||||||
return_value="/usr/bin/java",
|
return_value="/usr/bin/java",
|
||||||
):
|
):
|
||||||
assert _find_java() == "/usr/bin/java"
|
assert _find_java() == "/usr/bin/java"
|
||||||
|
|
||||||
def test_none_when_not_found(self):
|
def test_none_when_not_found(self) -> None:
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"commands.freerouting.shutil.which",
|
"commands.freerouting.shutil.which",
|
||||||
@@ -423,21 +424,21 @@ class TestFindJava:
|
|||||||
|
|
||||||
|
|
||||||
class TestFindDocker:
|
class TestFindDocker:
|
||||||
def test_finds_docker(self):
|
def test_finds_docker(self) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
"commands.freerouting.shutil.which",
|
"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"
|
assert _find_docker() == "/usr/bin/docker"
|
||||||
|
|
||||||
def test_finds_podman(self):
|
def test_finds_podman(self) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
"commands.freerouting.shutil.which",
|
"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"
|
assert _find_docker() == "/usr/bin/podman"
|
||||||
|
|
||||||
def test_none_when_not_found(self):
|
def test_none_when_not_found(self) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
"commands.freerouting.shutil.which",
|
"commands.freerouting.shutil.which",
|
||||||
return_value=None,
|
return_value=None,
|
||||||
@@ -446,7 +447,7 @@ class TestFindDocker:
|
|||||||
|
|
||||||
|
|
||||||
class TestDockerAvailable:
|
class TestDockerAvailable:
|
||||||
def test_docker_found(self):
|
def test_docker_found(self) -> None:
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"commands.freerouting._find_docker",
|
"commands.freerouting._find_docker",
|
||||||
@@ -457,14 +458,14 @@ class TestDockerAvailable:
|
|||||||
mock_run.return_value = MagicMock(returncode=0)
|
mock_run.return_value = MagicMock(returncode=0)
|
||||||
assert _docker_available() is True
|
assert _docker_available() is True
|
||||||
|
|
||||||
def test_docker_not_installed(self):
|
def test_docker_not_installed(self) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
"commands.freerouting._find_docker",
|
"commands.freerouting._find_docker",
|
||||||
return_value=None,
|
return_value=None,
|
||||||
):
|
):
|
||||||
assert _docker_available() is False
|
assert _docker_available() is False
|
||||||
|
|
||||||
def test_docker_not_running(self):
|
def test_docker_not_running(self) -> None:
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"commands.freerouting._find_docker",
|
"commands.freerouting._find_docker",
|
||||||
@@ -477,17 +478,17 @@ class TestDockerAvailable:
|
|||||||
|
|
||||||
|
|
||||||
class TestJavaVersionOk:
|
class TestJavaVersionOk:
|
||||||
def test_java_21(self):
|
def test_java_21(self) -> None:
|
||||||
with patch("commands.freerouting.subprocess.run") as mock_run:
|
with patch("commands.freerouting.subprocess.run") as mock_run:
|
||||||
mock_run.return_value = MagicMock(stderr='openjdk version "21.0.1"', stdout="")
|
mock_run.return_value = MagicMock(stderr='openjdk version "21.0.1"', stdout="")
|
||||||
assert _java_version_ok("/usr/bin/java") is True
|
assert _java_version_ok("/usr/bin/java") is True
|
||||||
|
|
||||||
def test_java_17(self):
|
def test_java_17(self) -> None:
|
||||||
with patch("commands.freerouting.subprocess.run") as mock_run:
|
with patch("commands.freerouting.subprocess.run") as mock_run:
|
||||||
mock_run.return_value = MagicMock(stderr='openjdk version "17.0.18"', stdout="")
|
mock_run.return_value = MagicMock(stderr='openjdk version "17.0.18"', stdout="")
|
||||||
assert _java_version_ok("/usr/bin/java") is False
|
assert _java_version_ok("/usr/bin/java") is False
|
||||||
|
|
||||||
def test_java_error(self):
|
def test_java_error(self) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
"commands.freerouting.subprocess.run",
|
"commands.freerouting.subprocess.run",
|
||||||
side_effect=Exception("not found"),
|
side_effect=Exception("not found"),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -32,7 +33,7 @@ def _sym(name: str) -> Symbol:
|
|||||||
return Symbol(name)
|
return Symbol(name)
|
||||||
|
|
||||||
|
|
||||||
def _make_wire(x1, y1, x2, y2):
|
def _make_wire(x1: Any, y1: Any, x2: Any, y2: Any) -> Any:
|
||||||
return [
|
return [
|
||||||
_sym("wire"),
|
_sym("wire"),
|
||||||
[_sym("pts"), [_sym("xy"), x1, y1], [_sym("xy"), x2, y2]],
|
[_sym("pts"), [_sym("xy"), x1, y1], [_sym("xy"), x2, y2]],
|
||||||
@@ -41,7 +42,7 @@ def _make_wire(x1, y1, x2, y2):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _make_junction(x, y):
|
def _make_junction(x: Any, y: Any) -> Any:
|
||||||
return [
|
return [
|
||||||
_sym("junction"),
|
_sym("junction"),
|
||||||
[_sym("at"), x, y],
|
[_sym("at"), x, y],
|
||||||
@@ -51,7 +52,9 @@ def _make_junction(x, y):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _make_symbol(ref, x, y, rotation=0, lib_id="Device:R", mirror=None):
|
def _make_symbol(
|
||||||
|
ref: Any, x: Any, y: Any, rotation: Any = 0, lib_id: str = "Device:R", mirror: Any = None
|
||||||
|
) -> Any:
|
||||||
"""Build a minimal placed-symbol s-expression."""
|
"""Build a minimal placed-symbol s-expression."""
|
||||||
item = [
|
item = [
|
||||||
_sym("symbol"),
|
_sym("symbol"),
|
||||||
@@ -66,7 +69,7 @@ def _make_symbol(ref, x, y, rotation=0, lib_id="Device:R", mirror=None):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
def _make_lib_symbol_r():
|
def _make_lib_symbol_r() -> Any:
|
||||||
"""Minimal Device:R lib_symbols entry — pins at (0, 3.81) and (0, -3.81)."""
|
"""Minimal Device:R lib_symbols entry — pins at (0, 3.81) and (0, -3.81)."""
|
||||||
return [
|
return [
|
||||||
_sym("symbol"),
|
_sym("symbol"),
|
||||||
@@ -112,7 +115,7 @@ def _make_lib_symbol_r():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _make_sch_data(extra_items=None):
|
def _make_sch_data(extra_items: Any = None) -> Any:
|
||||||
"""Build a minimal sch_data list with lib_symbols and sheet_instances."""
|
"""Build a minimal sch_data list with lib_symbols and sheet_instances."""
|
||||||
data = [
|
data = [
|
||||||
_sym("kicad_sch"),
|
_sym("kicad_sch"),
|
||||||
@@ -133,20 +136,20 @@ def _make_sch_data(extra_items=None):
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestRotatePoint:
|
class TestRotatePoint:
|
||||||
def test_zero_rotation(self):
|
def test_zero_rotation(self) -> None:
|
||||||
assert _rotate(1.0, 2.0, 0) == (1.0, 2.0)
|
assert _rotate(1.0, 2.0, 0) == (1.0, 2.0)
|
||||||
|
|
||||||
def test_90_degrees(self):
|
def test_90_degrees(self) -> None:
|
||||||
rx, ry = _rotate(1.0, 0.0, 90)
|
rx, ry = _rotate(1.0, 0.0, 90)
|
||||||
assert abs(rx - 0.0) < 1e-9
|
assert abs(rx - 0.0) < 1e-9
|
||||||
assert abs(ry - 1.0) < 1e-9
|
assert abs(ry - 1.0) < 1e-9
|
||||||
|
|
||||||
def test_180_degrees(self):
|
def test_180_degrees(self) -> None:
|
||||||
rx, ry = _rotate(1.0, 0.0, 180)
|
rx, ry = _rotate(1.0, 0.0, 180)
|
||||||
assert abs(rx - (-1.0)) < 1e-9
|
assert abs(rx - (-1.0)) < 1e-9
|
||||||
assert abs(ry - 0.0) < 1e-9
|
assert abs(ry - 0.0) < 1e-9
|
||||||
|
|
||||||
def test_270_degrees(self):
|
def test_270_degrees(self) -> None:
|
||||||
rx, ry = _rotate(0.0, 1.0, 270)
|
rx, ry = _rotate(0.0, 1.0, 270)
|
||||||
assert abs(rx - 1.0) < 1e-6
|
assert abs(rx - 1.0) < 1e-6
|
||||||
assert abs(ry - 0.0) < 1e-6
|
assert abs(ry - 0.0) < 1e-6
|
||||||
@@ -159,11 +162,11 @@ class TestRotatePoint:
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestFindSymbol:
|
class TestFindSymbol:
|
||||||
def test_returns_none_for_missing_reference(self):
|
def test_returns_none_for_missing_reference(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 10, 20)])
|
sch = _make_sch_data([_make_symbol("R1", 10, 20)])
|
||||||
assert WireDragger.find_symbol(sch, "R99") is None
|
assert WireDragger.find_symbol(sch, "R99") is None
|
||||||
|
|
||||||
def test_returns_item_and_position(self):
|
def test_returns_item_and_position(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 10.5, 20.5, rotation=90)])
|
sch = _make_sch_data([_make_symbol("R1", 10.5, 20.5, rotation=90)])
|
||||||
result = WireDragger.find_symbol(sch, "R1")
|
result = WireDragger.find_symbol(sch, "R1")
|
||||||
assert result is not None
|
assert result is not None
|
||||||
@@ -175,14 +178,14 @@ class TestFindSymbol:
|
|||||||
assert mirror_x is False
|
assert mirror_x is False
|
||||||
assert mirror_y is False
|
assert mirror_y is False
|
||||||
|
|
||||||
def test_detects_mirror_x(self):
|
def test_detects_mirror_x(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 0, 0, mirror="x")])
|
sch = _make_sch_data([_make_symbol("R1", 0, 0, mirror="x")])
|
||||||
result = WireDragger.find_symbol(sch, "R1")
|
result = WireDragger.find_symbol(sch, "R1")
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result[5] is True # mirror_x
|
assert result[5] is True # mirror_x
|
||||||
assert result[6] is False # mirror_y
|
assert result[6] is False # mirror_y
|
||||||
|
|
||||||
def test_detects_mirror_y(self):
|
def test_detects_mirror_y(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 0, 0, mirror="y")])
|
sch = _make_sch_data([_make_symbol("R1", 0, 0, mirror="y")])
|
||||||
result = WireDragger.find_symbol(sch, "R1")
|
result = WireDragger.find_symbol(sch, "R1")
|
||||||
assert result is not None
|
assert result is not None
|
||||||
@@ -197,7 +200,7 @@ class TestFindSymbol:
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestComputePinPositions:
|
class TestComputePinPositions:
|
||||||
def test_resistor_at_origin_no_rotation(self):
|
def test_resistor_at_origin_no_rotation(self) -> None:
|
||||||
"""Device:R at (0, 0) rot=0 — pins at (0, 3.81) and (0, -3.81)."""
|
"""Device:R at (0, 0) rot=0 — pins at (0, 3.81) and (0, -3.81)."""
|
||||||
sch = _make_sch_data([_make_symbol("R1", 0, 0)])
|
sch = _make_sch_data([_make_symbol("R1", 0, 0)])
|
||||||
positions = WireDragger.compute_pin_positions(sch, "R1", 10, 20)
|
positions = WireDragger.compute_pin_positions(sch, "R1", 10, 20)
|
||||||
@@ -216,7 +219,7 @@ class TestComputePinPositions:
|
|||||||
assert abs(new2[0] - 10) < 1e-4
|
assert abs(new2[0] - 10) < 1e-4
|
||||||
assert abs(new2[1] - 16.19) < 1e-4
|
assert abs(new2[1] - 16.19) < 1e-4
|
||||||
|
|
||||||
def test_resistor_rotated_90(self):
|
def test_resistor_rotated_90(self) -> None:
|
||||||
"""Device:R at (100, 100) rot=90 — pins should be at (100+3.81, 100) and (100-3.81, 100)."""
|
"""Device:R at (100, 100) rot=90 — pins should be at (100+3.81, 100) and (100-3.81, 100)."""
|
||||||
sch = _make_sch_data([_make_symbol("R1", 100, 100, rotation=90)])
|
sch = _make_sch_data([_make_symbol("R1", 100, 100, rotation=90)])
|
||||||
positions = WireDragger.compute_pin_positions(sch, "R1", 100, 100)
|
positions = WireDragger.compute_pin_positions(sch, "R1", 100, 100)
|
||||||
@@ -229,12 +232,12 @@ class TestComputePinPositions:
|
|||||||
assert abs(old1[0] - 96.19) < 1e-3
|
assert abs(old1[0] - 96.19) < 1e-3
|
||||||
assert abs(old1[1] - 100) < 1e-3
|
assert abs(old1[1] - 100) < 1e-3
|
||||||
|
|
||||||
def test_returns_empty_for_missing_component(self):
|
def test_returns_empty_for_missing_component(self) -> None:
|
||||||
sch = _make_sch_data()
|
sch = _make_sch_data()
|
||||||
result = WireDragger.compute_pin_positions(sch, "MISSING", 0, 0)
|
result = WireDragger.compute_pin_positions(sch, "MISSING", 0, 0)
|
||||||
assert result == {}
|
assert result == {}
|
||||||
|
|
||||||
def test_delta_is_consistent(self):
|
def test_delta_is_consistent(self) -> None:
|
||||||
"""new_xy - old_xy should equal (new_x - old_x, new_y - old_y) for any rotation."""
|
"""new_xy - old_xy should equal (new_x - old_x, new_y - old_y) for any rotation."""
|
||||||
sch = _make_sch_data([_make_symbol("R1", 50, 50, rotation=45)])
|
sch = _make_sch_data([_make_symbol("R1", 50, 50, rotation=45)])
|
||||||
positions = WireDragger.compute_pin_positions(sch, "R1", 60, 70)
|
positions = WireDragger.compute_pin_positions(sch, "R1", 60, 70)
|
||||||
@@ -252,13 +255,13 @@ class TestComputePinPositions:
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestDragWires:
|
class TestDragWires:
|
||||||
def test_no_wires_returns_zero_counts(self):
|
def test_no_wires_returns_zero_counts(self) -> None:
|
||||||
sch = _make_sch_data()
|
sch = _make_sch_data()
|
||||||
result = WireDragger.drag_wires(sch, {(0.0, 0.0): (10.0, 10.0)})
|
result = WireDragger.drag_wires(sch, {(0.0, 0.0): (10.0, 10.0)})
|
||||||
assert result["endpoints_moved"] == 0
|
assert result["endpoints_moved"] == 0
|
||||||
assert result["wires_removed"] == 0
|
assert result["wires_removed"] == 0
|
||||||
|
|
||||||
def test_wire_start_endpoint_moved(self):
|
def test_wire_start_endpoint_moved(self) -> None:
|
||||||
wire = _make_wire(0, 3.81, 0, 10)
|
wire = _make_wire(0, 3.81, 0, 10)
|
||||||
sch = _make_sch_data([wire])
|
sch = _make_sch_data([wire])
|
||||||
result = WireDragger.drag_wires(sch, {(0.0, 3.81): (10.0, 23.81)})
|
result = WireDragger.drag_wires(sch, {(0.0, 3.81): (10.0, 23.81)})
|
||||||
@@ -271,7 +274,7 @@ class TestDragWires:
|
|||||||
assert abs(xy1[1] - 10.0) < EPS
|
assert abs(xy1[1] - 10.0) < EPS
|
||||||
assert abs(xy1[2] - 23.81) < EPS
|
assert abs(xy1[2] - 23.81) < EPS
|
||||||
|
|
||||||
def test_wire_end_endpoint_moved(self):
|
def test_wire_end_endpoint_moved(self) -> None:
|
||||||
wire = _make_wire(0, 10, 0, -3.81)
|
wire = _make_wire(0, 10, 0, -3.81)
|
||||||
sch = _make_sch_data([wire])
|
sch = _make_sch_data([wire])
|
||||||
result = WireDragger.drag_wires(sch, {(0.0, -3.81): (10.0, 16.19)})
|
result = WireDragger.drag_wires(sch, {(0.0, -3.81): (10.0, 16.19)})
|
||||||
@@ -282,7 +285,7 @@ class TestDragWires:
|
|||||||
assert abs(xy2[1] - 10.0) < EPS
|
assert abs(xy2[1] - 10.0) < EPS
|
||||||
assert abs(xy2[2] - 16.19) < EPS
|
assert abs(xy2[2] - 16.19) < EPS
|
||||||
|
|
||||||
def test_zero_length_wire_removed(self):
|
def test_zero_length_wire_removed(self) -> None:
|
||||||
"""When both endpoints of a wire are moved to the same point, wire is deleted."""
|
"""When both endpoints of a wire are moved to the same point, wire is deleted."""
|
||||||
wire = _make_wire(0, 3.81, 0, -3.81)
|
wire = _make_wire(0, 3.81, 0, -3.81)
|
||||||
sch = _make_sch_data([wire])
|
sch = _make_sch_data([wire])
|
||||||
@@ -298,7 +301,7 @@ class TestDragWires:
|
|||||||
wires_remaining = [i for i in sch if isinstance(i, list) and i and i[0] == Symbol("wire")]
|
wires_remaining = [i for i in sch if isinstance(i, list) and i and i[0] == Symbol("wire")]
|
||||||
assert len(wires_remaining) == 0
|
assert len(wires_remaining) == 0
|
||||||
|
|
||||||
def test_unrelated_wire_not_touched(self):
|
def test_unrelated_wire_not_touched(self) -> None:
|
||||||
"""A wire whose endpoints don't match any old pin is not changed."""
|
"""A wire whose endpoints don't match any old pin is not changed."""
|
||||||
wire = _make_wire(50, 50, 60, 50)
|
wire = _make_wire(50, 50, 60, 50)
|
||||||
sch = _make_sch_data([wire])
|
sch = _make_sch_data([wire])
|
||||||
@@ -311,7 +314,7 @@ class TestDragWires:
|
|||||||
assert abs(xy1[1] - 50.0) < EPS
|
assert abs(xy1[1] - 50.0) < EPS
|
||||||
assert abs(xy1[2] - 50.0) < EPS
|
assert abs(xy1[2] - 50.0) < EPS
|
||||||
|
|
||||||
def test_both_endpoints_on_moved_component(self):
|
def test_both_endpoints_on_moved_component(self) -> None:
|
||||||
"""Wire connecting two pins of same component — both endpoints shift together."""
|
"""Wire connecting two pins of same component — both endpoints shift together."""
|
||||||
wire = _make_wire(0, 3.81, 0, -3.81)
|
wire = _make_wire(0, 3.81, 0, -3.81)
|
||||||
sch = _make_sch_data([wire])
|
sch = _make_sch_data([wire])
|
||||||
@@ -325,7 +328,7 @@ class TestDragWires:
|
|||||||
assert result["endpoints_moved"] == 2
|
assert result["endpoints_moved"] == 2
|
||||||
assert result["wires_removed"] == 0
|
assert result["wires_removed"] == 0
|
||||||
|
|
||||||
def test_junction_moved_with_endpoint(self):
|
def test_junction_moved_with_endpoint(self) -> None:
|
||||||
junction = _make_junction(0, 3.81)
|
junction = _make_junction(0, 3.81)
|
||||||
sch = _make_sch_data([junction])
|
sch = _make_sch_data([junction])
|
||||||
WireDragger.drag_wires(sch, {(0.0, 3.81): (10.0, 23.81)})
|
WireDragger.drag_wires(sch, {(0.0, 3.81): (10.0, 23.81)})
|
||||||
@@ -336,7 +339,7 @@ class TestDragWires:
|
|||||||
assert abs(at_sub[1] - 10.0) < EPS
|
assert abs(at_sub[1] - 10.0) < EPS
|
||||||
assert abs(at_sub[2] - 23.81) < EPS
|
assert abs(at_sub[2] - 23.81) < EPS
|
||||||
|
|
||||||
def test_junction_at_unrelated_position_not_touched(self):
|
def test_junction_at_unrelated_position_not_touched(self) -> None:
|
||||||
junction = _make_junction(99, 99)
|
junction = _make_junction(99, 99)
|
||||||
sch = _make_sch_data([junction])
|
sch = _make_sch_data([junction])
|
||||||
WireDragger.drag_wires(sch, {(0.0, 3.81): (10.0, 23.81)})
|
WireDragger.drag_wires(sch, {(0.0, 3.81): (10.0, 23.81)})
|
||||||
@@ -355,7 +358,7 @@ class TestDragWires:
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
class TestUpdateSymbolPosition:
|
class TestUpdateSymbolPosition:
|
||||||
def test_updates_position(self):
|
def test_updates_position(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 10, 20)])
|
sch = _make_sch_data([_make_symbol("R1", 10, 20)])
|
||||||
result = WireDragger.update_symbol_position(sch, "R1", 30, 40)
|
result = WireDragger.update_symbol_position(sch, "R1", 30, 40)
|
||||||
assert result is True
|
assert result is True
|
||||||
@@ -363,17 +366,17 @@ class TestUpdateSymbolPosition:
|
|||||||
assert abs(found[1] - 30) < EPS
|
assert abs(found[1] - 30) < EPS
|
||||||
assert abs(found[2] - 40) < EPS
|
assert abs(found[2] - 40) < EPS
|
||||||
|
|
||||||
def test_returns_false_for_missing(self):
|
def test_returns_false_for_missing(self) -> None:
|
||||||
sch = _make_sch_data()
|
sch = _make_sch_data()
|
||||||
assert WireDragger.update_symbol_position(sch, "MISSING", 0, 0) is False
|
assert WireDragger.update_symbol_position(sch, "MISSING", 0, 0) is False
|
||||||
|
|
||||||
def test_preserves_rotation(self):
|
def test_preserves_rotation(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 10, 20, rotation=90)])
|
sch = _make_sch_data([_make_symbol("R1", 10, 20, rotation=90)])
|
||||||
WireDragger.update_symbol_position(sch, "R1", 30, 40)
|
WireDragger.update_symbol_position(sch, "R1", 30, 40)
|
||||||
found = WireDragger.find_symbol(sch, "R1")
|
found = WireDragger.find_symbol(sch, "R1")
|
||||||
assert abs(found[3] - 90) < EPS # rotation preserved
|
assert abs(found[3] - 90) < EPS # rotation preserved
|
||||||
|
|
||||||
def test_property_labels_follow_symbol_move(self):
|
def test_property_labels_follow_symbol_move(self) -> None:
|
||||||
"""Property (at ...) positions must shift by the same delta as the symbol."""
|
"""Property (at ...) positions must shift by the same delta as the symbol."""
|
||||||
sym = _make_symbol("R1", 100, 80)
|
sym = _make_symbol("R1", 100, 80)
|
||||||
sch = _make_sch_data([sym])
|
sch = _make_sch_data([sym])
|
||||||
@@ -421,7 +424,7 @@ class TestUpdateSymbolPosition:
|
|||||||
class TestMoveWithWirePreservation:
|
class TestMoveWithWirePreservation:
|
||||||
"""Integration tests using a real .kicad_sch file."""
|
"""Integration tests using a real .kicad_sch file."""
|
||||||
|
|
||||||
def _make_schematic(self, extra_sexp=""):
|
def _make_schematic(self, extra_sexp: Any = "") -> Any:
|
||||||
"""Copy empty.kicad_sch to a temp file and optionally append content."""
|
"""Copy empty.kicad_sch to a temp file and optionally append content."""
|
||||||
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
||||||
shutil.copy(TEMPLATE_PATH, tmp)
|
shutil.copy(TEMPLATE_PATH, tmp)
|
||||||
@@ -462,7 +465,7 @@ class TestMoveWithWirePreservation:
|
|||||||
path.write_text(content[:idx] + "\n" + sexp + "\n)", encoding="utf-8")
|
path.write_text(content[:idx] + "\n" + sexp + "\n)", encoding="utf-8")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _add_wire(self, path: Path, x1, y1, x2, y2) -> Path:
|
def _add_wire(self, path: Path, x1: float, y1: float, x2: float, y2: float) -> Path:
|
||||||
"""Append a wire to the schematic file."""
|
"""Append a wire to the schematic file."""
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -476,7 +479,7 @@ class TestMoveWithWirePreservation:
|
|||||||
path.write_text(content[:idx] + "\n" + wire_sexp + "\n)", encoding="utf-8")
|
path.write_text(content[:idx] + "\n" + wire_sexp + "\n)", encoding="utf-8")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _parse_wires(self, path: Path):
|
def _parse_wires(self, path: Path) -> Any:
|
||||||
"""Return list of ((x1,y1),(x2,y2)) for every wire in the file."""
|
"""Return list of ((x1,y1),(x2,y2)) for every wire in the file."""
|
||||||
content = path.read_text(encoding="utf-8")
|
content = path.read_text(encoding="utf-8")
|
||||||
data = sexpdata.loads(content)
|
data = sexpdata.loads(content)
|
||||||
@@ -502,7 +505,7 @@ class TestMoveWithWirePreservation:
|
|||||||
)
|
)
|
||||||
return wires
|
return wires
|
||||||
|
|
||||||
def _get_symbol_pos(self, path: Path, ref: str):
|
def _get_symbol_pos(self, path: Path, ref: str) -> Any:
|
||||||
content = path.read_text(encoding="utf-8")
|
content = path.read_text(encoding="utf-8")
|
||||||
data = sexpdata.loads(content)
|
data = sexpdata.loads(content)
|
||||||
found = WireDragger.find_symbol(data, ref)
|
found = WireDragger.find_symbol(data, ref)
|
||||||
@@ -510,7 +513,7 @@ class TestMoveWithWirePreservation:
|
|||||||
return None
|
return None
|
||||||
return found[1], found[2]
|
return found[1], found[2]
|
||||||
|
|
||||||
def test_symbol_position_updated(self):
|
def test_symbol_position_updated(self) -> None:
|
||||||
sch = self._make_schematic()
|
sch = self._make_schematic()
|
||||||
self._add_resistor(sch, "R1", 100, 100)
|
self._add_resistor(sch, "R1", 100, 100)
|
||||||
# Call handler directly
|
# Call handler directly
|
||||||
@@ -531,7 +534,7 @@ class TestMoveWithWirePreservation:
|
|||||||
assert abs(pos[0] - 120) < EPS
|
assert abs(pos[0] - 120) < EPS
|
||||||
assert abs(pos[1] - 130) < EPS
|
assert abs(pos[1] - 130) < EPS
|
||||||
|
|
||||||
def test_connected_wire_endpoint_follows_pin(self):
|
def test_connected_wire_endpoint_follows_pin(self) -> None:
|
||||||
"""Wire endpoint at pin 1 of R1 should move with the component."""
|
"""Wire endpoint at pin 1 of R1 should move with the component."""
|
||||||
sch = self._make_schematic()
|
sch = self._make_schematic()
|
||||||
# R1 at (100, 100) — pin 1 at (100, 103.81)
|
# R1 at (100, 100) — pin 1 at (100, 103.81)
|
||||||
@@ -563,7 +566,7 @@ class TestMoveWithWirePreservation:
|
|||||||
abs(ep[0] - new_pin1[0]) < 0.01 and abs(ep[1] - new_pin1[1]) < 0.01 for ep in endpoints
|
abs(ep[0] - new_pin1[0]) < 0.01 and abs(ep[1] - new_pin1[1]) < 0.01 for ep in endpoints
|
||||||
), f"Expected pin endpoint near {new_pin1}, got {endpoints}"
|
), f"Expected pin endpoint near {new_pin1}, got {endpoints}"
|
||||||
|
|
||||||
def test_unrelated_wire_unchanged(self):
|
def test_unrelated_wire_unchanged(self) -> None:
|
||||||
"""A wire not connected to R1 must not be modified."""
|
"""A wire not connected to R1 must not be modified."""
|
||||||
sch = self._make_schematic()
|
sch = self._make_schematic()
|
||||||
self._add_resistor(sch, "R1", 100, 100)
|
self._add_resistor(sch, "R1", 100, 100)
|
||||||
@@ -586,7 +589,7 @@ class TestMoveWithWirePreservation:
|
|||||||
unrelated = [(s, e) for s, e in wires if abs(s[0] - 50) < 0.01 and abs(s[1] - 50) < 0.01]
|
unrelated = [(s, e) for s, e in wires if abs(s[0] - 50) < 0.01 and abs(s[1] - 50) < 0.01]
|
||||||
assert len(unrelated) == 1
|
assert len(unrelated) == 1
|
||||||
|
|
||||||
def test_no_zero_length_wires_after_move(self):
|
def test_no_zero_length_wires_after_move(self) -> None:
|
||||||
"""No zero-length wires should appear in the file after a move."""
|
"""No zero-length wires should appear in the file after a move."""
|
||||||
sch = self._make_schematic()
|
sch = self._make_schematic()
|
||||||
self._add_resistor(sch, "R1", 100, 100)
|
self._add_resistor(sch, "R1", 100, 100)
|
||||||
@@ -612,7 +615,7 @@ class TestMoveWithWirePreservation:
|
|||||||
abs(start[0] - end[0]) < EPS and abs(start[1] - end[1]) < EPS
|
abs(start[0] - end[0]) < EPS and abs(start[1] - end[1]) < EPS
|
||||||
), f"Zero-length wire found at {start}"
|
), f"Zero-length wire found at {start}"
|
||||||
|
|
||||||
def test_preserve_wires_false_skips_wire_update(self):
|
def test_preserve_wires_false_skips_wire_update(self) -> None:
|
||||||
"""preserveWires=False should move the symbol but leave wires alone."""
|
"""preserveWires=False should move the symbol but leave wires alone."""
|
||||||
sch = self._make_schematic()
|
sch = self._make_schematic()
|
||||||
self._add_resistor(sch, "R1", 100, 100)
|
self._add_resistor(sch, "R1", 100, 100)
|
||||||
@@ -643,7 +646,7 @@ class TestMoveWithWirePreservation:
|
|||||||
abs(ep[0] - old_pin1[0]) < 0.01 and abs(ep[1] - old_pin1[1]) < 0.01 for ep in endpoints
|
abs(ep[0] - old_pin1[0]) < 0.01 and abs(ep[1] - old_pin1[1]) < 0.01 for ep in endpoints
|
||||||
), f"Wire should still be at {old_pin1}, got {endpoints}"
|
), f"Wire should still be at {old_pin1}, got {endpoints}"
|
||||||
|
|
||||||
def test_missing_component_returns_error(self):
|
def test_missing_component_returns_error(self) -> None:
|
||||||
sch = self._make_schematic()
|
sch = self._make_schematic()
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
@@ -670,7 +673,7 @@ class TestMoveWithWirePreservation:
|
|||||||
class TestSynthesizeTouchingPinWires:
|
class TestSynthesizeTouchingPinWires:
|
||||||
"""Unit tests for WireDragger.synthesize_touching_pin_wires."""
|
"""Unit tests for WireDragger.synthesize_touching_pin_wires."""
|
||||||
|
|
||||||
def _make_two_resistors(self, r1_x, r1_y, r2_x, r2_y):
|
def _make_two_resistors(self, r1_x: Any, r1_y: Any, r2_x: Any, r2_y: Any) -> Any:
|
||||||
"""Build sch_data with R1 and R2, each Device:R."""
|
"""Build sch_data with R1 and R2, each Device:R."""
|
||||||
return _make_sch_data(
|
return _make_sch_data(
|
||||||
[
|
[
|
||||||
@@ -679,14 +682,14 @@ class TestSynthesizeTouchingPinWires:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_no_stationary_symbols_returns_zero(self):
|
def test_no_stationary_symbols_returns_zero(self) -> None:
|
||||||
"""With only the moved component in sch_data, nothing is synthesized."""
|
"""With only the moved component in sch_data, nothing is synthesized."""
|
||||||
sch = _make_sch_data([_make_symbol("R1", 0, 0)])
|
sch = _make_sch_data([_make_symbol("R1", 0, 0)])
|
||||||
pin_positions = WireDragger.compute_pin_positions(sch, "R1", 10, 20)
|
pin_positions = WireDragger.compute_pin_positions(sch, "R1", 10, 20)
|
||||||
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", pin_positions)
|
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", pin_positions)
|
||||||
assert count == 0
|
assert count == 0
|
||||||
|
|
||||||
def test_touching_pin_gap_generates_wire(self):
|
def test_touching_pin_gap_generates_wire(self) -> None:
|
||||||
"""
|
"""
|
||||||
R1 at (0, 0) pin2 at (0, -3.81).
|
R1 at (0, 0) pin2 at (0, -3.81).
|
||||||
R2 at (0, -7.62) pin1 at (0, -3.81). ← pins touch
|
R2 at (0, -7.62) pin1 at (0, -3.81). ← pins touch
|
||||||
@@ -728,7 +731,7 @@ class TestSynthesizeTouchingPinWires:
|
|||||||
-3.81,
|
-3.81,
|
||||||
) in endpoints, f"Expected (10, -3.81) in wire endpoints, got {endpoints}"
|
) in endpoints, f"Expected (10, -3.81) in wire endpoints, got {endpoints}"
|
||||||
|
|
||||||
def test_no_wire_when_pin_didnt_move(self):
|
def test_no_wire_when_pin_didnt_move(self) -> None:
|
||||||
"""
|
"""
|
||||||
If old_xy == new_xy for a touching pin (component moved but this pin stayed put),
|
If old_xy == new_xy for a touching pin (component moved but this pin stayed put),
|
||||||
no wire should be synthesized.
|
no wire should be synthesized.
|
||||||
@@ -740,7 +743,7 @@ class TestSynthesizeTouchingPinWires:
|
|||||||
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", pin_positions)
|
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", pin_positions)
|
||||||
assert count == 0
|
assert count == 0
|
||||||
|
|
||||||
def test_no_wire_when_rejoins_other_stationary_pin(self):
|
def test_no_wire_when_rejoins_other_stationary_pin(self) -> None:
|
||||||
"""
|
"""
|
||||||
If the moved pin's new position coincides with another stationary pin,
|
If the moved pin's new position coincides with another stationary pin,
|
||||||
no wire should be synthesized (they touch again).
|
no wire should be synthesized (they touch again).
|
||||||
@@ -759,12 +762,12 @@ class TestSynthesizeTouchingPinWires:
|
|||||||
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", pin_positions)
|
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", pin_positions)
|
||||||
assert count == 0, f"Expected 0 synthesized wires (rejoined), got {count}"
|
assert count == 0, f"Expected 0 synthesized wires (rejoined), got {count}"
|
||||||
|
|
||||||
def test_empty_pin_positions_returns_zero(self):
|
def test_empty_pin_positions_returns_zero(self) -> None:
|
||||||
sch = _make_sch_data([_make_symbol("R1", 0, 0)])
|
sch = _make_sch_data([_make_symbol("R1", 0, 0)])
|
||||||
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", {})
|
count = WireDragger.synthesize_touching_pin_wires(sch, "R1", {})
|
||||||
assert count == 0
|
assert count == 0
|
||||||
|
|
||||||
def test_non_touching_pins_not_affected(self):
|
def test_non_touching_pins_not_affected(self) -> None:
|
||||||
"""
|
"""
|
||||||
When R1 and R2 are NOT touching (different positions), no wire is synthesized.
|
When R1 and R2 are NOT touching (different positions), no wire is synthesized.
|
||||||
"""
|
"""
|
||||||
@@ -784,7 +787,7 @@ class TestSynthesizeTouchingPinWires:
|
|||||||
class TestOldToNewCollision:
|
class TestOldToNewCollision:
|
||||||
"""Verify that coincident pins do not silently overwrite each other in old_to_new."""
|
"""Verify that coincident pins do not silently overwrite each other in old_to_new."""
|
||||||
|
|
||||||
def test_handler_logs_warning_on_collision(self, caplog):
|
def test_handler_logs_warning_on_collision(self, caplog: Any) -> None:
|
||||||
"""
|
"""
|
||||||
When two pins share the same old position, a warning should be logged
|
When two pins share the same old position, a warning should be logged
|
||||||
and the *first* mapping should be kept (not overwritten by the second).
|
and the *first* mapping should be kept (not overwritten by the second).
|
||||||
@@ -827,7 +830,7 @@ class TestOldToNewCollision:
|
|||||||
class TestTouchingPinIntegration:
|
class TestTouchingPinIntegration:
|
||||||
"""Integration tests for pin-touching connection wire synthesis."""
|
"""Integration tests for pin-touching connection wire synthesis."""
|
||||||
|
|
||||||
def _make_schematic(self, extra_sexp=""):
|
def _make_schematic(self, extra_sexp: Any = "") -> Any:
|
||||||
"""Copy empty.kicad_sch to a temp file."""
|
"""Copy empty.kicad_sch to a temp file."""
|
||||||
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
||||||
shutil.copy(TEMPLATE_PATH, tmp)
|
shutil.copy(TEMPLATE_PATH, tmp)
|
||||||
@@ -867,7 +870,7 @@ class TestTouchingPinIntegration:
|
|||||||
path.write_text(content[:idx] + "\n" + sexp + "\n)", encoding="utf-8")
|
path.write_text(content[:idx] + "\n" + sexp + "\n)", encoding="utf-8")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _parse_wires(self, path: Path):
|
def _parse_wires(self, path: Path) -> Any:
|
||||||
content = path.read_text(encoding="utf-8")
|
content = path.read_text(encoding="utf-8")
|
||||||
data = sexpdata.loads(content)
|
data = sexpdata.loads(content)
|
||||||
wires = []
|
wires = []
|
||||||
@@ -892,7 +895,7 @@ class TestTouchingPinIntegration:
|
|||||||
)
|
)
|
||||||
return wires
|
return wires
|
||||||
|
|
||||||
def test_touching_pin_wire_created_on_move(self):
|
def test_touching_pin_wire_created_on_move(self) -> None:
|
||||||
"""
|
"""
|
||||||
R1 at (100, 100) and R2 at (100, 92.38) share a touching pin:
|
R1 at (100, 100) and R2 at (100, 92.38) share a touching pin:
|
||||||
R1 pin2 = (100, 96.19), R2 pin1 = (100, 96.19).
|
R1 pin2 = (100, 96.19), R2 pin1 = (100, 96.19).
|
||||||
@@ -947,7 +950,7 @@ class TestTouchingPinIntegration:
|
|||||||
len(bridging) >= 1
|
len(bridging) >= 1
|
||||||
), f"Expected a bridging wire from {old_pin2} to {new_pin2}, got wires: {wires}"
|
), f"Expected a bridging wire from {old_pin2} to {new_pin2}, got wires: {wires}"
|
||||||
|
|
||||||
def test_no_wire_synthesized_when_no_touching_pins(self):
|
def test_no_wire_synthesized_when_no_touching_pins(self) -> None:
|
||||||
"""
|
"""
|
||||||
Two resistors with no pin overlap should not generate any synthesized wires.
|
Two resistors with no pin overlap should not generate any synthesized wires.
|
||||||
"""
|
"""
|
||||||
@@ -970,7 +973,7 @@ class TestTouchingPinIntegration:
|
|||||||
assert result["success"], result.get("message")
|
assert result["success"], result.get("message")
|
||||||
assert result.get("wiresSynthesized", 0) == 0
|
assert result.get("wiresSynthesized", 0) == 0
|
||||||
|
|
||||||
def test_existing_wires_still_dragged_with_touching_pins(self):
|
def test_existing_wires_still_dragged_with_touching_pins(self) -> None:
|
||||||
"""
|
"""
|
||||||
When R1 has both an explicit wire AND a touching-pin connection,
|
When R1 has both an explicit wire AND a touching-pin connection,
|
||||||
both should be handled: the wire dragged and the touching-pin bridged.
|
both should be handled: the wire dragged and the touching-pin bridged.
|
||||||
|
|||||||
@@ -130,42 +130,42 @@ def _make_led_sexp(ref: str, x: float, y: float, rotation: float = 0) -> str:
|
|||||||
class TestGeometryHelpers:
|
class TestGeometryHelpers:
|
||||||
"""Test low-level geometry utilities."""
|
"""Test low-level geometry utilities."""
|
||||||
|
|
||||||
def test_point_in_rect_inside(self):
|
def test_point_in_rect_inside(self) -> None:
|
||||||
assert _point_in_rect(5, 5, 0, 0, 10, 10) is True
|
assert _point_in_rect(5, 5, 0, 0, 10, 10) is True
|
||||||
|
|
||||||
def test_point_in_rect_outside(self):
|
def test_point_in_rect_outside(self) -> None:
|
||||||
assert _point_in_rect(15, 5, 0, 0, 10, 10) is False
|
assert _point_in_rect(15, 5, 0, 0, 10, 10) is False
|
||||||
|
|
||||||
def test_point_in_rect_boundary(self):
|
def test_point_in_rect_boundary(self) -> None:
|
||||||
assert _point_in_rect(0, 0, 0, 0, 10, 10) is True
|
assert _point_in_rect(0, 0, 0, 0, 10, 10) is True
|
||||||
|
|
||||||
def test_distance_zero(self):
|
def test_distance_zero(self) -> None:
|
||||||
assert _distance((0, 0), (0, 0)) == 0
|
assert _distance((0, 0), (0, 0)) == 0
|
||||||
|
|
||||||
def test_distance_unit(self):
|
def test_distance_unit(self) -> None:
|
||||||
assert abs(_distance((0, 0), (3, 4)) - 5.0) < 1e-9
|
assert abs(_distance((0, 0), (3, 4)) - 5.0) < 1e-9
|
||||||
|
|
||||||
def test_aabb_intersection_crossing(self):
|
def test_aabb_intersection_crossing(self) -> None:
|
||||||
# Line from (0,5) to (10,5) should intersect box (2,2)-(8,8)
|
# Line from (0,5) to (10,5) should intersect box (2,2)-(8,8)
|
||||||
assert _line_segment_intersects_aabb(0, 5, 10, 5, 2, 2, 8, 8) is True
|
assert _line_segment_intersects_aabb(0, 5, 10, 5, 2, 2, 8, 8) is True
|
||||||
|
|
||||||
def test_aabb_intersection_miss(self):
|
def test_aabb_intersection_miss(self) -> None:
|
||||||
# Line from (0,0) to (10,0) should miss box (2,2)-(8,8)
|
# Line from (0,0) to (10,0) should miss box (2,2)-(8,8)
|
||||||
assert _line_segment_intersects_aabb(0, 0, 10, 0, 2, 2, 8, 8) is False
|
assert _line_segment_intersects_aabb(0, 0, 10, 0, 2, 2, 8, 8) is False
|
||||||
|
|
||||||
def test_aabb_intersection_inside(self):
|
def test_aabb_intersection_inside(self) -> None:
|
||||||
# Line entirely inside the box
|
# Line entirely inside the box
|
||||||
assert _line_segment_intersects_aabb(3, 3, 7, 7, 2, 2, 8, 8) is True
|
assert _line_segment_intersects_aabb(3, 3, 7, 7, 2, 2, 8, 8) is True
|
||||||
|
|
||||||
def test_aabb_intersection_diagonal(self):
|
def test_aabb_intersection_diagonal(self) -> None:
|
||||||
# Diagonal line crossing through box
|
# Diagonal line crossing through box
|
||||||
assert _line_segment_intersects_aabb(0, 0, 10, 10, 2, 2, 8, 8) is True
|
assert _line_segment_intersects_aabb(0, 0, 10, 10, 2, 2, 8, 8) is True
|
||||||
|
|
||||||
def test_aabb_intersection_parallel_outside(self):
|
def test_aabb_intersection_parallel_outside(self) -> None:
|
||||||
# Horizontal line above the box
|
# Horizontal line above the box
|
||||||
assert _line_segment_intersects_aabb(0, 9, 10, 9, 2, 2, 8, 8) is False
|
assert _line_segment_intersects_aabb(0, 9, 10, 9, 2, 2, 8, 8) is False
|
||||||
|
|
||||||
def test_aabb_intersection_touching_edge(self):
|
def test_aabb_intersection_touching_edge(self) -> None:
|
||||||
# Line ending exactly at box edge
|
# Line ending exactly at box edge
|
||||||
assert _line_segment_intersects_aabb(0, 2, 2, 2, 2, 2, 8, 8) is True
|
assert _line_segment_intersects_aabb(0, 2, 2, 2, 2, 2, 8, 8) is True
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ class TestGeometryHelpers:
|
|||||||
class TestSexpParsers:
|
class TestSexpParsers:
|
||||||
"""Test S-expression parsing functions with synthetic data."""
|
"""Test S-expression parsing functions with synthetic data."""
|
||||||
|
|
||||||
def test_parse_wires_basic(self):
|
def test_parse_wires_basic(self) -> None:
|
||||||
sexp = sexpdata.loads("""(kicad_sch
|
sexp = sexpdata.loads("""(kicad_sch
|
||||||
(wire (pts (xy 10 20) (xy 30 40))
|
(wire (pts (xy 10 20) (xy 30 40))
|
||||||
(stroke (width 0) (type default))
|
(stroke (width 0) (type default))
|
||||||
@@ -189,11 +189,11 @@ class TestSexpParsers:
|
|||||||
assert wires[0]["start"] == (10.0, 20.0)
|
assert wires[0]["start"] == (10.0, 20.0)
|
||||||
assert wires[0]["end"] == (30.0, 40.0)
|
assert wires[0]["end"] == (30.0, 40.0)
|
||||||
|
|
||||||
def test_parse_wires_empty(self):
|
def test_parse_wires_empty(self) -> None:
|
||||||
sexp = sexpdata.loads("(kicad_sch)")
|
sexp = sexpdata.loads("(kicad_sch)")
|
||||||
assert _parse_wires(sexp) == []
|
assert _parse_wires(sexp) == []
|
||||||
|
|
||||||
def test_parse_labels_both_types(self):
|
def test_parse_labels_both_types(self) -> None:
|
||||||
sexp = sexpdata.loads("""(kicad_sch
|
sexp = sexpdata.loads("""(kicad_sch
|
||||||
(label "VCC" (at 10 20 0))
|
(label "VCC" (at 10 20 0))
|
||||||
(global_label "GND" (at 30 40 0))
|
(global_label "GND" (at 30 40 0))
|
||||||
@@ -205,7 +205,7 @@ class TestSexpParsers:
|
|||||||
assert labels[1]["name"] == "GND"
|
assert labels[1]["name"] == "GND"
|
||||||
assert labels[1]["type"] == "global_label"
|
assert labels[1]["type"] == "global_label"
|
||||||
|
|
||||||
def test_parse_symbols(self):
|
def test_parse_symbols(self) -> None:
|
||||||
sexp = sexpdata.loads("""(kicad_sch
|
sexp = sexpdata.loads("""(kicad_sch
|
||||||
(symbol (lib_id "Device:R") (at 100 100 0)
|
(symbol (lib_id "Device:R") (at 100 100 0)
|
||||||
(property "Reference" "R1" (at 0 0 0)))
|
(property "Reference" "R1" (at 0 0 0)))
|
||||||
@@ -228,20 +228,20 @@ class TestSexpParsers:
|
|||||||
class TestAABBOverlap:
|
class TestAABBOverlap:
|
||||||
"""Test AABB overlap helper."""
|
"""Test AABB overlap helper."""
|
||||||
|
|
||||||
def test_overlapping_boxes(self):
|
def test_overlapping_boxes(self) -> None:
|
||||||
assert _aabb_overlap((0, 0, 10, 10), (5, 5, 15, 15)) is True
|
assert _aabb_overlap((0, 0, 10, 10), (5, 5, 15, 15)) is True
|
||||||
|
|
||||||
def test_non_overlapping_boxes(self):
|
def test_non_overlapping_boxes(self) -> None:
|
||||||
assert _aabb_overlap((0, 0, 10, 10), (20, 20, 30, 30)) is False
|
assert _aabb_overlap((0, 0, 10, 10), (20, 20, 30, 30)) is False
|
||||||
|
|
||||||
def test_touching_boxes_no_overlap(self):
|
def test_touching_boxes_no_overlap(self) -> None:
|
||||||
# Touching edges are not overlapping (strict inequality)
|
# Touching edges are not overlapping (strict inequality)
|
||||||
assert _aabb_overlap((0, 0, 10, 10), (10, 0, 20, 10)) is False
|
assert _aabb_overlap((0, 0, 10, 10), (10, 0, 20, 10)) is False
|
||||||
|
|
||||||
def test_contained_box(self):
|
def test_contained_box(self) -> None:
|
||||||
assert _aabb_overlap((0, 0, 20, 20), (5, 5, 15, 15)) is True
|
assert _aabb_overlap((0, 0, 20, 20), (5, 5, 15, 15)) is True
|
||||||
|
|
||||||
def test_overlap_one_axis_only(self):
|
def test_overlap_one_axis_only(self) -> None:
|
||||||
# Overlap in X but not Y
|
# Overlap in X but not Y
|
||||||
assert _aabb_overlap((0, 0, 10, 10), (5, 15, 15, 25)) is False
|
assert _aabb_overlap((0, 0, 10, 10), (5, 15, 15, 25)) is False
|
||||||
|
|
||||||
@@ -249,12 +249,12 @@ class TestAABBOverlap:
|
|||||||
class TestFindOverlappingElements:
|
class TestFindOverlappingElements:
|
||||||
"""Test overlapping detection logic."""
|
"""Test overlapping detection logic."""
|
||||||
|
|
||||||
def test_no_overlaps_in_empty_schematic(self):
|
def test_no_overlaps_in_empty_schematic(self) -> None:
|
||||||
tmp = _make_temp_schematic()
|
tmp = _make_temp_schematic()
|
||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] == 0
|
assert result["totalOverlaps"] == 0
|
||||||
|
|
||||||
def test_overlapping_symbols_detected(self):
|
def test_overlapping_symbols_detected(self) -> None:
|
||||||
# Two resistors at nearly the same position — bboxes fully overlap
|
# 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)
|
tmp = _make_temp_schematic(extra)
|
||||||
@@ -262,13 +262,13 @@ class TestFindOverlappingElements:
|
|||||||
assert result["totalOverlaps"] >= 1
|
assert result["totalOverlaps"] >= 1
|
||||||
assert len(result["overlappingSymbols"]) >= 1
|
assert len(result["overlappingSymbols"]) >= 1
|
||||||
|
|
||||||
def test_well_separated_symbols_not_flagged(self):
|
def test_well_separated_symbols_not_flagged(self) -> None:
|
||||||
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)
|
tmp = _make_temp_schematic(extra)
|
||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] == 0
|
assert result["totalOverlaps"] == 0
|
||||||
|
|
||||||
def test_collinear_wire_overlap(self):
|
def test_collinear_wire_overlap(self) -> None:
|
||||||
extra = """
|
extra = """
|
||||||
(wire (pts (xy 10 50) (xy 30 50))
|
(wire (pts (xy 10 50) (xy 30 50))
|
||||||
(stroke (width 0) (type default))
|
(stroke (width 0) (type default))
|
||||||
@@ -281,7 +281,7 @@ class TestFindOverlappingElements:
|
|||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert len(result["overlappingWires"]) >= 1
|
assert len(result["overlappingWires"]) >= 1
|
||||||
|
|
||||||
def test_overlapping_bodies_different_centers(self):
|
def test_overlapping_bodies_different_centers(self) -> None:
|
||||||
"""Two resistors whose bodies overlap even though centers are ~5mm apart.
|
"""Two resistors whose bodies overlap even though centers are ~5mm apart.
|
||||||
|
|
||||||
Device:R pins are at y ±3.81 relative to center, so the body spans
|
Device:R pins are at y ±3.81 relative to center, so the body spans
|
||||||
@@ -299,7 +299,7 @@ class TestFindOverlappingElements:
|
|||||||
)
|
)
|
||||||
assert len(result["overlappingSymbols"]) >= 1
|
assert len(result["overlappingSymbols"]) >= 1
|
||||||
|
|
||||||
def test_adjacent_resistors_no_overlap(self):
|
def test_adjacent_resistors_no_overlap(self) -> None:
|
||||||
"""Two vertical resistors side by side should not overlap.
|
"""Two vertical resistors side by side should not overlap.
|
||||||
|
|
||||||
R pins at y ±3.81, but different X positions far enough apart.
|
R pins at y ±3.81, but different X positions far enough apart.
|
||||||
@@ -309,7 +309,7 @@ class TestFindOverlappingElements:
|
|||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert result["totalOverlaps"] == 0
|
assert result["totalOverlaps"] == 0
|
||||||
|
|
||||||
def test_resistor_and_led_overlapping_bodies(self):
|
def test_resistor_and_led_overlapping_bodies(self) -> None:
|
||||||
"""A resistor and an LED placed close enough that bodies overlap.
|
"""A resistor and an LED placed close enough that bodies overlap.
|
||||||
|
|
||||||
LED pins at x ±3.81, R pins at y ±3.81. Place LED at same position
|
LED pins at x ±3.81, R pins at y ±3.81. Place LED at same position
|
||||||
@@ -324,7 +324,7 @@ class TestFindOverlappingElements:
|
|||||||
class TestGetElementsInRegion:
|
class TestGetElementsInRegion:
|
||||||
"""Test region query logic."""
|
"""Test region query logic."""
|
||||||
|
|
||||||
def test_elements_inside_region_found(self):
|
def test_elements_inside_region_found(self) -> None:
|
||||||
extra = """
|
extra = """
|
||||||
(symbol (lib_id "Device:R") (at 50 50 0)
|
(symbol (lib_id "Device:R") (at 50 50 0)
|
||||||
(property "Reference" "R1" (at 0 0 0))
|
(property "Reference" "R1" (at 0 0 0))
|
||||||
@@ -340,7 +340,7 @@ class TestGetElementsInRegion:
|
|||||||
assert result["counts"]["wires"] >= 1
|
assert result["counts"]["wires"] >= 1
|
||||||
assert result["counts"]["labels"] >= 1
|
assert result["counts"]["labels"] >= 1
|
||||||
|
|
||||||
def test_elements_outside_region_excluded(self):
|
def test_elements_outside_region_excluded(self) -> None:
|
||||||
extra = """
|
extra = """
|
||||||
(symbol (lib_id "Device:R") (at 200 200 0)
|
(symbol (lib_id "Device:R") (at 200 200 0)
|
||||||
(property "Reference" "R1" (at 0 0 0))
|
(property "Reference" "R1" (at 0 0 0))
|
||||||
@@ -354,7 +354,7 @@ class TestGetElementsInRegion:
|
|||||||
class TestComputeSymbolBbox:
|
class TestComputeSymbolBbox:
|
||||||
"""Test bounding box computation."""
|
"""Test bounding box computation."""
|
||||||
|
|
||||||
def test_returns_none_for_unknown_symbol(self):
|
def test_returns_none_for_unknown_symbol(self) -> None:
|
||||||
tmp = _make_temp_schematic()
|
tmp = _make_temp_schematic()
|
||||||
from commands.pin_locator import PinLocator
|
from commands.pin_locator import PinLocator
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ class TestComputeSymbolBbox:
|
|||||||
class TestIntegrationFindWiresCrossingSymbols:
|
class TestIntegrationFindWiresCrossingSymbols:
|
||||||
"""Integration test for wire crossing symbol detection."""
|
"""Integration test for wire crossing symbol detection."""
|
||||||
|
|
||||||
def test_wire_not_touching_pins_is_collision(self):
|
def test_wire_not_touching_pins_is_collision(self) -> None:
|
||||||
"""A wire passing through a component bbox without pin contact → collision."""
|
"""A wire passing through a component bbox without pin contact → collision."""
|
||||||
# LED D1 at (100,100) → pin 1 at (96.19, 100), pin 2 at (103.81, 100)
|
# LED D1 at (100,100) → pin 1 at (96.19, 100), pin 2 at (103.81, 100)
|
||||||
# Vertical wire from (100, 95) to (100, 105) crosses through the body
|
# Vertical wire from (100, 95) to (100, 105) crosses through the body
|
||||||
@@ -387,7 +387,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
d1_collisions = [c for c in result if c["component"]["reference"] == "D1"]
|
d1_collisions = [c for c in result if c["component"]["reference"] == "D1"]
|
||||||
assert len(d1_collisions) >= 1
|
assert len(d1_collisions) >= 1
|
||||||
|
|
||||||
def test_unannotated_duplicates_not_over_reported(self):
|
def test_unannotated_duplicates_not_over_reported(self) -> None:
|
||||||
"""
|
"""
|
||||||
Regression: two components with the same unannotated reference ("R?") at
|
Regression: two components with the same unannotated reference ("R?") at
|
||||||
different positions should each produce independent bounding boxes.
|
different positions should each produce independent bounding boxes.
|
||||||
@@ -417,7 +417,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
"likely caused by reference-lookup always returning the first 'R?'"
|
"likely caused by reference-lookup always returning the first 'R?'"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_wire_starting_at_pin_passing_through_body(self):
|
def test_wire_starting_at_pin_passing_through_body(self) -> None:
|
||||||
"""A wire that starts at a pin but continues through the component body
|
"""A wire that starts at a pin but continues through the component body
|
||||||
must be flagged — this is the core bug where the old suppression logic
|
must be flagged — this is the core bug where the old suppression logic
|
||||||
treated any wire touching a pin as a valid connection."""
|
treated any wire touching a pin as a valid connection."""
|
||||||
@@ -435,7 +435,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
len(d1_crossings) >= 1
|
len(d1_crossings) >= 1
|
||||||
), "Wire starting at pin but passing through body must be detected"
|
), "Wire starting at pin but passing through body must be detected"
|
||||||
|
|
||||||
def test_wire_terminating_at_pin_from_outside(self):
|
def test_wire_terminating_at_pin_from_outside(self) -> None:
|
||||||
"""A wire that arrives at a pin from outside the component body
|
"""A wire that arrives at a pin from outside the component body
|
||||||
is a valid connection and must NOT be flagged."""
|
is a valid connection and must NOT be flagged."""
|
||||||
# LED D1 at (100,100) → pin 1 at (96.19, 100)
|
# LED D1 at (100,100) → pin 1 at (96.19, 100)
|
||||||
@@ -450,7 +450,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
d1_crossings = [c for c in result if c["component"]["reference"] == "D1"]
|
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):
|
def test_wire_shorts_component_pins_detected_as_collision(self) -> None:
|
||||||
"""Regression: a wire connecting pin1→pin2 of the same component
|
"""Regression: a wire connecting pin1→pin2 of the same component
|
||||||
must be reported even though both endpoints land on pins."""
|
must be reported even though both endpoints land on pins."""
|
||||||
r_sexp = _make_resistor_sexp("R_short", 100.0, 100.0)
|
r_sexp = _make_resistor_sexp("R_short", 100.0, 100.0)
|
||||||
@@ -472,7 +472,7 @@ class TestIntegrationFindWiresCrossingSymbols:
|
|||||||
class TestIntegrationGetElementsInRegion:
|
class TestIntegrationGetElementsInRegion:
|
||||||
"""Integration test for region query."""
|
"""Integration test for region query."""
|
||||||
|
|
||||||
def test_region_returns_pin_data(self):
|
def test_region_returns_pin_data(self) -> None:
|
||||||
"""Symbols in region should include pin position data."""
|
"""Symbols in region should include pin position data."""
|
||||||
extra = _make_resistor_sexp("R1", 100, 100)
|
extra = _make_resistor_sexp("R1", 100, 100)
|
||||||
tmp = _make_temp_schematic(extra)
|
tmp = _make_temp_schematic(extra)
|
||||||
@@ -482,7 +482,7 @@ class TestIntegrationGetElementsInRegion:
|
|||||||
assert "pins" in sym
|
assert "pins" in sym
|
||||||
assert len(sym["pins"]) == 2 # Resistor has 2 pins
|
assert len(sym["pins"]) == 2 # Resistor has 2 pins
|
||||||
|
|
||||||
def test_wire_passing_through_region_included(self):
|
def test_wire_passing_through_region_included(self) -> None:
|
||||||
"""A wire that passes through a region (no endpoints inside) should be included."""
|
"""A wire that passes through a region (no endpoints inside) should be included."""
|
||||||
extra = """
|
extra = """
|
||||||
(wire (pts (xy 0 50) (xy 100 50))
|
(wire (pts (xy 0 50) (xy 100 50))
|
||||||
@@ -493,7 +493,7 @@ class TestIntegrationGetElementsInRegion:
|
|||||||
result = get_elements_in_region(tmp, 40, 40, 60, 60)
|
result = get_elements_in_region(tmp, 40, 40, 60, 60)
|
||||||
assert result["counts"]["wires"] == 1
|
assert result["counts"]["wires"] == 1
|
||||||
|
|
||||||
def test_wire_outside_region_excluded(self):
|
def test_wire_outside_region_excluded(self) -> None:
|
||||||
"""A wire entirely outside a region should not be included."""
|
"""A wire entirely outside a region should not be included."""
|
||||||
extra = """
|
extra = """
|
||||||
(wire (pts (xy 0 0) (xy 10 0))
|
(wire (pts (xy 0 0) (xy 10 0))
|
||||||
@@ -513,48 +513,48 @@ class TestIntegrationGetElementsInRegion:
|
|||||||
class TestCheckWireOverlap:
|
class TestCheckWireOverlap:
|
||||||
"""Test wire overlap detection for horizontal, vertical, and diagonal cases."""
|
"""Test wire overlap detection for horizontal, vertical, and diagonal cases."""
|
||||||
|
|
||||||
def test_horizontal_overlap(self):
|
def test_horizontal_overlap(self) -> None:
|
||||||
w1 = {"start": (10, 50), "end": (30, 50)}
|
w1 = {"start": (10, 50), "end": (30, 50)}
|
||||||
w2 = {"start": (20, 50), "end": (40, 50)}
|
w2 = {"start": (20, 50), "end": (40, 50)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["type"] == "collinear_overlap"
|
assert result["type"] == "collinear_overlap"
|
||||||
|
|
||||||
def test_vertical_overlap(self):
|
def test_vertical_overlap(self) -> None:
|
||||||
w1 = {"start": (50, 10), "end": (50, 30)}
|
w1 = {"start": (50, 10), "end": (50, 30)}
|
||||||
w2 = {"start": (50, 20), "end": (50, 40)}
|
w2 = {"start": (50, 20), "end": (50, 40)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["type"] == "collinear_overlap"
|
assert result["type"] == "collinear_overlap"
|
||||||
|
|
||||||
def test_diagonal_overlap(self):
|
def test_diagonal_overlap(self) -> None:
|
||||||
w1 = {"start": (0, 0), "end": (20, 20)}
|
w1 = {"start": (0, 0), "end": (20, 20)}
|
||||||
w2 = {"start": (10, 10), "end": (30, 30)}
|
w2 = {"start": (10, 10), "end": (30, 30)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["type"] == "collinear_overlap"
|
assert result["type"] == "collinear_overlap"
|
||||||
|
|
||||||
def test_horizontal_no_overlap(self):
|
def test_horizontal_no_overlap(self) -> None:
|
||||||
w1 = {"start": (10, 50), "end": (20, 50)}
|
w1 = {"start": (10, 50), "end": (20, 50)}
|
||||||
w2 = {"start": (30, 50), "end": (40, 50)}
|
w2 = {"start": (30, 50), "end": (40, 50)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
def test_parallel_offset_no_overlap(self):
|
def test_parallel_offset_no_overlap(self) -> None:
|
||||||
"""Two parallel wires offset perpendicularly should not overlap."""
|
"""Two parallel wires offset perpendicularly should not overlap."""
|
||||||
w1 = {"start": (0, 0), "end": (20, 20)}
|
w1 = {"start": (0, 0), "end": (20, 20)}
|
||||||
w2 = {"start": (0, 5), "end": (20, 25)}
|
w2 = {"start": (0, 5), "end": (20, 25)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
def test_non_parallel_no_overlap(self):
|
def test_non_parallel_no_overlap(self) -> None:
|
||||||
"""Two wires at different angles should not overlap."""
|
"""Two wires at different angles should not overlap."""
|
||||||
w1 = {"start": (0, 0), "end": (10, 10)}
|
w1 = {"start": (0, 0), "end": (10, 10)}
|
||||||
w2 = {"start": (0, 0), "end": (10, 0)}
|
w2 = {"start": (0, 0), "end": (10, 0)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
def test_zero_length_segment(self):
|
def test_zero_length_segment(self) -> None:
|
||||||
w1 = {"start": (10, 10), "end": (10, 10)}
|
w1 = {"start": (10, 10), "end": (10, 10)}
|
||||||
w2 = {"start": (10, 10), "end": (20, 20)}
|
w2 = {"start": (10, 10), "end": (20, 20)}
|
||||||
result = _check_wire_overlap(w1, w2, 0.5)
|
result = _check_wire_overlap(w1, w2, 0.5)
|
||||||
@@ -565,7 +565,7 @@ class TestCheckWireOverlap:
|
|||||||
class TestIntegrationDiagonalWireOverlap:
|
class TestIntegrationDiagonalWireOverlap:
|
||||||
"""Integration tests for diagonal collinear wire overlap detection."""
|
"""Integration tests for diagonal collinear wire overlap detection."""
|
||||||
|
|
||||||
def test_diagonal_collinear_wire_overlap(self):
|
def test_diagonal_collinear_wire_overlap(self) -> None:
|
||||||
"""Two 45-degree wires that overlap should be detected."""
|
"""Two 45-degree wires that overlap should be detected."""
|
||||||
extra = """
|
extra = """
|
||||||
(wire (pts (xy 0 0) (xy 20 20))
|
(wire (pts (xy 0 0) (xy 20 20))
|
||||||
@@ -579,7 +579,7 @@ class TestIntegrationDiagonalWireOverlap:
|
|||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert len(result["overlappingWires"]) >= 1
|
assert len(result["overlappingWires"]) >= 1
|
||||||
|
|
||||||
def test_diagonal_parallel_no_overlap(self):
|
def test_diagonal_parallel_no_overlap(self) -> None:
|
||||||
"""Two parallel 45-degree wires that are offset should not overlap."""
|
"""Two parallel 45-degree wires that are offset should not overlap."""
|
||||||
extra = """
|
extra = """
|
||||||
(wire (pts (xy 0 0) (xy 20 20))
|
(wire (pts (xy 0 0) (xy 20 20))
|
||||||
@@ -593,7 +593,7 @@ class TestIntegrationDiagonalWireOverlap:
|
|||||||
result = find_overlapping_elements(tmp, tolerance=0.5)
|
result = find_overlapping_elements(tmp, tolerance=0.5)
|
||||||
assert len(result["overlappingWires"]) == 0
|
assert len(result["overlappingWires"]) == 0
|
||||||
|
|
||||||
def test_diagonal_non_collinear_no_overlap(self):
|
def test_diagonal_non_collinear_no_overlap(self) -> None:
|
||||||
"""Two wires at different angles crossing should not be flagged as collinear overlap."""
|
"""Two wires at different angles crossing should not be flagged as collinear overlap."""
|
||||||
extra = """
|
extra = """
|
||||||
(wire (pts (xy 0 0) (xy 20 20))
|
(wire (pts (xy 0 0) (xy 20 20))
|
||||||
@@ -616,7 +616,7 @@ class TestIntegrationDiagonalWireOverlap:
|
|||||||
class TestExtractLibSymbols:
|
class TestExtractLibSymbols:
|
||||||
"""Test _extract_lib_symbols helper."""
|
"""Test _extract_lib_symbols helper."""
|
||||||
|
|
||||||
def test_extracts_pins_from_lib_symbols(self):
|
def test_extracts_pins_from_lib_symbols(self) -> None:
|
||||||
sexp = sexpdata.loads("""(kicad_sch
|
sexp = sexpdata.loads("""(kicad_sch
|
||||||
(lib_symbols
|
(lib_symbols
|
||||||
(symbol "Device:R"
|
(symbol "Device:R"
|
||||||
@@ -636,19 +636,19 @@ class TestExtractLibSymbols:
|
|||||||
assert "2" in pins
|
assert "2" in pins
|
||||||
assert pins["1"]["y"] == pytest.approx(3.81)
|
assert pins["1"]["y"] == pytest.approx(3.81)
|
||||||
|
|
||||||
def test_empty_schematic_returns_empty(self):
|
def test_empty_schematic_returns_empty(self) -> None:
|
||||||
sexp = sexpdata.loads("(kicad_sch)")
|
sexp = sexpdata.loads("(kicad_sch)")
|
||||||
result = _extract_lib_symbols(sexp)
|
result = _extract_lib_symbols(sexp)
|
||||||
assert result == {}
|
assert result == {}
|
||||||
|
|
||||||
def test_no_lib_symbols_section(self):
|
def test_no_lib_symbols_section(self) -> None:
|
||||||
sexp = sexpdata.loads("""(kicad_sch
|
sexp = sexpdata.loads("""(kicad_sch
|
||||||
(wire (pts (xy 0 0) (xy 10 10)))
|
(wire (pts (xy 0 0) (xy 10 10)))
|
||||||
)""")
|
)""")
|
||||||
result = _extract_lib_symbols(sexp)
|
result = _extract_lib_symbols(sexp)
|
||||||
assert result == {}
|
assert result == {}
|
||||||
|
|
||||||
def test_extract_includes_graphics_points(self):
|
def test_extract_includes_graphics_points(self) -> None:
|
||||||
"""_extract_lib_symbols should return graphics_points from body shapes."""
|
"""_extract_lib_symbols should return graphics_points from body shapes."""
|
||||||
sexp = sexpdata.loads("""(kicad_sch
|
sexp = sexpdata.loads("""(kicad_sch
|
||||||
(lib_symbols
|
(lib_symbols
|
||||||
@@ -688,7 +688,7 @@ class TestExtractLibSymbols:
|
|||||||
class TestParseLibSymbolGraphics:
|
class TestParseLibSymbolGraphics:
|
||||||
"""Test graphics extraction from lib_symbol definitions."""
|
"""Test graphics extraction from lib_symbol definitions."""
|
||||||
|
|
||||||
def test_rectangle(self):
|
def test_rectangle(self) -> None:
|
||||||
sexp = sexpdata.loads("""(symbol "Device:R"
|
sexp = sexpdata.loads("""(symbol "Device:R"
|
||||||
(symbol "Device:R_0_1"
|
(symbol "Device:R_0_1"
|
||||||
(rectangle (start -1.016 -2.54) (end 1.016 2.54)
|
(rectangle (start -1.016 -2.54) (end 1.016 2.54)
|
||||||
@@ -699,7 +699,7 @@ class TestParseLibSymbolGraphics:
|
|||||||
assert (-1.016, -2.54) in pts
|
assert (-1.016, -2.54) in pts
|
||||||
assert (1.016, 2.54) in pts
|
assert (1.016, 2.54) in pts
|
||||||
|
|
||||||
def test_polyline(self):
|
def test_polyline(self) -> None:
|
||||||
sexp = sexpdata.loads("""(symbol "Device:C"
|
sexp = sexpdata.loads("""(symbol "Device:C"
|
||||||
(symbol "Device:C_0_1"
|
(symbol "Device:C_0_1"
|
||||||
(polyline
|
(polyline
|
||||||
@@ -710,7 +710,7 @@ class TestParseLibSymbolGraphics:
|
|||||||
assert (-2.032, -0.762) in pts
|
assert (-2.032, -0.762) in pts
|
||||||
assert (2.032, -0.762) in pts
|
assert (2.032, -0.762) in pts
|
||||||
|
|
||||||
def test_circle(self):
|
def test_circle(self) -> None:
|
||||||
sexp = sexpdata.loads("""(symbol "Test:Circle"
|
sexp = sexpdata.loads("""(symbol "Test:Circle"
|
||||||
(symbol "Test:Circle_0_1"
|
(symbol "Test:Circle_0_1"
|
||||||
(circle (center 0 0) (radius 5)
|
(circle (center 0 0) (radius 5)
|
||||||
@@ -721,7 +721,7 @@ class TestParseLibSymbolGraphics:
|
|||||||
assert (-5.0, -5.0) in pts
|
assert (-5.0, -5.0) in pts
|
||||||
assert (5.0, 5.0) in pts
|
assert (5.0, 5.0) in pts
|
||||||
|
|
||||||
def test_arc(self):
|
def test_arc(self) -> None:
|
||||||
sexp = sexpdata.loads("""(symbol "Test:Arc"
|
sexp = sexpdata.loads("""(symbol "Test:Arc"
|
||||||
(symbol "Test:Arc_0_1"
|
(symbol "Test:Arc_0_1"
|
||||||
(arc (start 1 0) (mid 0 1) (end -1 0)
|
(arc (start 1 0) (mid 0 1) (end -1 0)
|
||||||
@@ -732,7 +732,7 @@ class TestParseLibSymbolGraphics:
|
|||||||
assert (0.0, 1.0) in pts
|
assert (0.0, 1.0) in pts
|
||||||
assert (-1.0, 0.0) in pts
|
assert (-1.0, 0.0) in pts
|
||||||
|
|
||||||
def test_no_graphics(self):
|
def test_no_graphics(self) -> None:
|
||||||
sexp = sexpdata.loads("""(symbol "Test:Empty"
|
sexp = sexpdata.loads("""(symbol "Test:Empty"
|
||||||
(symbol "Test:Empty_1_1"
|
(symbol "Test:Empty_1_1"
|
||||||
(pin passive line (at 0 0 0) (length 1.27)
|
(pin passive line (at 0 0 0) (length 1.27)
|
||||||
@@ -750,24 +750,24 @@ class TestParseLibSymbolGraphics:
|
|||||||
class TestTransformLocalPoint:
|
class TestTransformLocalPoint:
|
||||||
"""Test local→absolute coordinate transform."""
|
"""Test local→absolute coordinate transform."""
|
||||||
|
|
||||||
def test_no_transform(self):
|
def test_no_transform(self) -> None:
|
||||||
# ly is negated (lib y-up → schematic y-down)
|
# ly is negated (lib y-up → schematic y-down)
|
||||||
x, y = _transform_local_point(1.0, 2.0, 100.0, 200.0, 0, False, False)
|
x, y = _transform_local_point(1.0, 2.0, 100.0, 200.0, 0, False, False)
|
||||||
assert x == pytest.approx(101.0)
|
assert x == pytest.approx(101.0)
|
||||||
assert y == pytest.approx(198.0)
|
assert y == pytest.approx(198.0)
|
||||||
|
|
||||||
def test_mirror_x(self):
|
def test_mirror_x(self) -> None:
|
||||||
# y-negate then mirror_x cancel out → net ly unchanged
|
# y-negate then mirror_x cancel out → net ly unchanged
|
||||||
x, y = _transform_local_point(1.0, 2.0, 0.0, 0.0, 0, True, False)
|
x, y = _transform_local_point(1.0, 2.0, 0.0, 0.0, 0, True, False)
|
||||||
assert x == pytest.approx(1.0)
|
assert x == pytest.approx(1.0)
|
||||||
assert y == pytest.approx(2.0)
|
assert y == pytest.approx(2.0)
|
||||||
|
|
||||||
def test_mirror_y(self):
|
def test_mirror_y(self) -> None:
|
||||||
x, y = _transform_local_point(1.0, 2.0, 0.0, 0.0, 0, False, True)
|
x, y = _transform_local_point(1.0, 2.0, 0.0, 0.0, 0, False, True)
|
||||||
assert x == pytest.approx(-1.0)
|
assert x == pytest.approx(-1.0)
|
||||||
assert y == pytest.approx(-2.0)
|
assert y == pytest.approx(-2.0)
|
||||||
|
|
||||||
def test_rotation_90(self):
|
def test_rotation_90(self) -> None:
|
||||||
# ly=0 negated is still 0, then rotate lx=1 by 90°
|
# ly=0 negated is still 0, then rotate lx=1 by 90°
|
||||||
x, y = _transform_local_point(1.0, 0.0, 0.0, 0.0, 90, False, False)
|
x, y = _transform_local_point(1.0, 0.0, 0.0, 0.0, 90, False, False)
|
||||||
assert x == pytest.approx(0.0, abs=1e-9)
|
assert x == pytest.approx(0.0, abs=1e-9)
|
||||||
@@ -782,7 +782,7 @@ class TestTransformLocalPoint:
|
|||||||
class TestComputeSymbolBboxWithGraphics:
|
class TestComputeSymbolBboxWithGraphics:
|
||||||
"""Test that bounding box computation uses graphics points when available."""
|
"""Test that bounding box computation uses graphics points when available."""
|
||||||
|
|
||||||
def test_resistor_bbox_from_graphics(self):
|
def test_resistor_bbox_from_graphics(self) -> None:
|
||||||
"""Device:R rectangle is (-1.016, -2.54) to (1.016, 2.54) in local coords.
|
"""Device:R rectangle is (-1.016, -2.54) to (1.016, 2.54) in local coords.
|
||||||
Pins at (0, ±3.81). Placed at (100, 100) with no rotation.
|
Pins at (0, ±3.81). Placed at (100, 100) with no rotation.
|
||||||
Bbox should span from pin-to-pin in Y and use rectangle width in X."""
|
Bbox should span from pin-to-pin in Y and use rectangle width in X."""
|
||||||
@@ -823,7 +823,7 @@ class TestComputeSymbolBboxWithGraphics:
|
|||||||
assert min_y == pytest.approx(100 - 3.81)
|
assert min_y == pytest.approx(100 - 3.81)
|
||||||
assert max_y == pytest.approx(100 + 3.81)
|
assert max_y == pytest.approx(100 + 3.81)
|
||||||
|
|
||||||
def test_fallback_without_graphics(self):
|
def test_fallback_without_graphics(self) -> None:
|
||||||
"""Without graphics_points, should use the old degenerate expansion."""
|
"""Without graphics_points, should use the old degenerate expansion."""
|
||||||
sym = {
|
sym = {
|
||||||
"x": 100.0,
|
"x": 100.0,
|
||||||
@@ -858,7 +858,7 @@ class TestComputeSymbolBboxWithGraphics:
|
|||||||
assert min_x == pytest.approx(100 - 1.5)
|
assert min_x == pytest.approx(100 - 1.5)
|
||||||
assert max_x == pytest.approx(100 + 1.5)
|
assert max_x == pytest.approx(100 + 1.5)
|
||||||
|
|
||||||
def test_rotated_symbol_graphics(self):
|
def test_rotated_symbol_graphics(self) -> None:
|
||||||
"""Graphics points should be rotated along with the symbol."""
|
"""Graphics points should be rotated along with the symbol."""
|
||||||
sym = {
|
sym = {
|
||||||
"x": 100.0,
|
"x": 100.0,
|
||||||
@@ -902,7 +902,7 @@ class TestComputeSymbolBboxWithGraphics:
|
|||||||
class TestIntegrationGraphicsBbox:
|
class TestIntegrationGraphicsBbox:
|
||||||
"""Integration tests verifying graphics-based bbox from real template data."""
|
"""Integration tests verifying graphics-based bbox from real template data."""
|
||||||
|
|
||||||
def test_resistor_bbox_uses_rectangle(self):
|
def test_resistor_bbox_uses_rectangle(self) -> None:
|
||||||
"""The template's Device:R has a rectangle body.
|
"""The template's Device:R has a rectangle body.
|
||||||
Verify that the bbox for a placed resistor uses the actual
|
Verify that the bbox for a placed resistor uses the actual
|
||||||
rectangle width rather than the degenerate 1.5mm expansion."""
|
rectangle width rather than the degenerate 1.5mm expansion."""
|
||||||
@@ -925,7 +925,7 @@ class TestIntegrationGraphicsBbox:
|
|||||||
# Rectangle is ±1.016 in X, NOT ±1.5 from degenerate expansion
|
# Rectangle is ±1.016 in X, NOT ±1.5 from degenerate expansion
|
||||||
assert max_x - min_x == pytest.approx(2 * 1.016, abs=0.01)
|
assert max_x - min_x == pytest.approx(2 * 1.016, abs=0.01)
|
||||||
|
|
||||||
def test_led_bbox_uses_polyline(self):
|
def test_led_bbox_uses_polyline(self) -> None:
|
||||||
"""The template's Device:LED uses polylines for its body.
|
"""The template's Device:LED uses polylines for its body.
|
||||||
Verify that the bbox uses polyline extents."""
|
Verify that the bbox uses polyline extents."""
|
||||||
extra = _make_led_sexp("D1", 100, 100)
|
extra = _make_led_sexp("D1", 100, 100)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -86,7 +87,7 @@ class TestGetSchematicComponentParsing:
|
|||||||
}
|
}
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def _parse_comp_pos(self, block_text: str):
|
def _parse_comp_pos(self, block_text: str) -> Any:
|
||||||
"""Mirrors the regex used to extract symbol position."""
|
"""Mirrors the regex used to extract symbol position."""
|
||||||
m = re.search(
|
m = re.search(
|
||||||
r'\(symbol\s+\(lib_id\s+"[^"]*"\s*\)\s+\(at\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s*\)',
|
r'\(symbol\s+\(lib_id\s+"[^"]*"\s*\)\s+\(at\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s*\)',
|
||||||
@@ -100,7 +101,7 @@ class TestGetSchematicComponentParsing:
|
|||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def test_parses_reference_field(self):
|
def test_parses_reference_field(self) -> None:
|
||||||
fields = self._parse_fields(PLACED_RESISTOR_BLOCK)
|
fields = self._parse_fields(PLACED_RESISTOR_BLOCK)
|
||||||
assert "Reference" in fields
|
assert "Reference" in fields
|
||||||
assert fields["Reference"]["value"] == "R1"
|
assert fields["Reference"]["value"] == "R1"
|
||||||
@@ -108,25 +109,25 @@ class TestGetSchematicComponentParsing:
|
|||||||
assert fields["Reference"]["y"] == pytest.approx(47.46)
|
assert fields["Reference"]["y"] == pytest.approx(47.46)
|
||||||
assert fields["Reference"]["angle"] == pytest.approx(0.0)
|
assert fields["Reference"]["angle"] == pytest.approx(0.0)
|
||||||
|
|
||||||
def test_parses_value_field(self):
|
def test_parses_value_field(self) -> None:
|
||||||
fields = self._parse_fields(PLACED_RESISTOR_BLOCK)
|
fields = self._parse_fields(PLACED_RESISTOR_BLOCK)
|
||||||
assert "Value" in fields
|
assert "Value" in fields
|
||||||
assert fields["Value"]["value"] == "10k"
|
assert fields["Value"]["value"] == "10k"
|
||||||
assert fields["Value"]["x"] == pytest.approx(51.27)
|
assert fields["Value"]["x"] == pytest.approx(51.27)
|
||||||
assert fields["Value"]["y"] == pytest.approx(52.54)
|
assert fields["Value"]["y"] == pytest.approx(52.54)
|
||||||
|
|
||||||
def test_parses_all_four_standard_fields(self):
|
def test_parses_all_four_standard_fields(self) -> None:
|
||||||
fields = self._parse_fields(PLACED_RESISTOR_BLOCK)
|
fields = self._parse_fields(PLACED_RESISTOR_BLOCK)
|
||||||
assert set(fields.keys()) >= {"Reference", "Value", "Footprint", "Datasheet"}
|
assert set(fields.keys()) >= {"Reference", "Value", "Footprint", "Datasheet"}
|
||||||
|
|
||||||
def test_parses_component_position(self):
|
def test_parses_component_position(self) -> None:
|
||||||
pos = self._parse_comp_pos(PLACED_RESISTOR_BLOCK)
|
pos = self._parse_comp_pos(PLACED_RESISTOR_BLOCK)
|
||||||
assert pos is not None
|
assert pos is not None
|
||||||
assert pos["x"] == pytest.approx(50.0)
|
assert pos["x"] == pytest.approx(50.0)
|
||||||
assert pos["y"] == pytest.approx(50.0)
|
assert pos["y"] == pytest.approx(50.0)
|
||||||
assert pos["angle"] == pytest.approx(0.0)
|
assert pos["angle"] == pytest.approx(0.0)
|
||||||
|
|
||||||
def test_field_position_regex_replaces_correctly(self):
|
def test_field_position_regex_replaces_correctly(self) -> None:
|
||||||
"""Mirrors the regex used in _handle_edit_schematic_component for fieldPositions."""
|
"""Mirrors the regex used in _handle_edit_schematic_component for fieldPositions."""
|
||||||
field_name = "Reference"
|
field_name = "Reference"
|
||||||
new_x, new_y, new_angle = 99.0, 88.0, 0
|
new_x, new_y, new_angle = 99.0, 88.0, 0
|
||||||
@@ -144,7 +145,7 @@ class TestGetSchematicComponentParsing:
|
|||||||
# Value should be unchanged
|
# Value should be unchanged
|
||||||
assert fields["Value"]["x"] == pytest.approx(51.27)
|
assert fields["Value"]["x"] == pytest.approx(51.27)
|
||||||
|
|
||||||
def test_field_position_regex_preserves_value(self):
|
def test_field_position_regex_preserves_value(self) -> None:
|
||||||
"""Replacing position must not change the field value string."""
|
"""Replacing position must not change the field value string."""
|
||||||
block = PLACED_RESISTOR_BLOCK
|
block = PLACED_RESISTOR_BLOCK
|
||||||
block = re.sub(
|
block = re.sub(
|
||||||
@@ -166,16 +167,16 @@ class TestGetSchematicComponentIntegration:
|
|||||||
"""Integration tests: write a real .kicad_sch and call the handler."""
|
"""Integration tests: write a real .kicad_sch and call the handler."""
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sch_with_r1(self, tmp_path):
|
def sch_with_r1(self, tmp_path: Any) -> Any:
|
||||||
return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)
|
return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)
|
||||||
|
|
||||||
def _get_interface(self):
|
def _get_interface(self) -> Any:
|
||||||
"""Lazily import KiCADInterface to avoid pcbnew import at collection time."""
|
"""Lazily import KiCADInterface to avoid pcbnew import at collection time."""
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
return KiCADInterface()
|
return KiCADInterface()
|
||||||
|
|
||||||
def test_get_returns_success(self, sch_with_r1):
|
def test_get_returns_success(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -186,7 +187,7 @@ class TestGetSchematicComponentIntegration:
|
|||||||
)
|
)
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
||||||
def test_get_returns_correct_reference(self, sch_with_r1):
|
def test_get_returns_correct_reference(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -197,7 +198,7 @@ class TestGetSchematicComponentIntegration:
|
|||||||
)
|
)
|
||||||
assert result["reference"] == "R1"
|
assert result["reference"] == "R1"
|
||||||
|
|
||||||
def test_get_returns_component_position(self, sch_with_r1):
|
def test_get_returns_component_position(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -210,7 +211,7 @@ class TestGetSchematicComponentIntegration:
|
|||||||
assert result["position"]["x"] == pytest.approx(50.0)
|
assert result["position"]["x"] == pytest.approx(50.0)
|
||||||
assert result["position"]["y"] == pytest.approx(50.0)
|
assert result["position"]["y"] == pytest.approx(50.0)
|
||||||
|
|
||||||
def test_get_returns_reference_field_position(self, sch_with_r1):
|
def test_get_returns_reference_field_position(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -224,7 +225,7 @@ class TestGetSchematicComponentIntegration:
|
|||||||
assert ref_field["x"] == pytest.approx(51.27)
|
assert ref_field["x"] == pytest.approx(51.27)
|
||||||
assert ref_field["y"] == pytest.approx(47.46)
|
assert ref_field["y"] == pytest.approx(47.46)
|
||||||
|
|
||||||
def test_get_returns_value_field(self, sch_with_r1):
|
def test_get_returns_value_field(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -238,7 +239,7 @@ class TestGetSchematicComponentIntegration:
|
|||||||
assert val_field["x"] == pytest.approx(51.27)
|
assert val_field["x"] == pytest.approx(51.27)
|
||||||
assert val_field["y"] == pytest.approx(52.54)
|
assert val_field["y"] == pytest.approx(52.54)
|
||||||
|
|
||||||
def test_get_unknown_reference_returns_failure(self, sch_with_r1):
|
def test_get_unknown_reference_returns_failure(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -250,7 +251,7 @@ class TestGetSchematicComponentIntegration:
|
|||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "R99" in result["message"]
|
assert "R99" in result["message"]
|
||||||
|
|
||||||
def test_get_missing_path_returns_failure(self):
|
def test_get_missing_path_returns_failure(self) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"get_schematic_component",
|
"get_schematic_component",
|
||||||
@@ -266,15 +267,15 @@ class TestEditSchematicComponentFieldPositions:
|
|||||||
"""Integration tests for the new fieldPositions parameter."""
|
"""Integration tests for the new fieldPositions parameter."""
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sch_with_r1(self, tmp_path):
|
def sch_with_r1(self, tmp_path: Any) -> Any:
|
||||||
return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)
|
return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK)
|
||||||
|
|
||||||
def _get_interface(self):
|
def _get_interface(self) -> Any:
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
return KiCADInterface()
|
return KiCADInterface()
|
||||||
|
|
||||||
def test_reposition_reference_label(self, sch_with_r1):
|
def test_reposition_reference_label(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"edit_schematic_component",
|
"edit_schematic_component",
|
||||||
@@ -297,7 +298,7 @@ class TestEditSchematicComponentFieldPositions:
|
|||||||
assert get_result["fields"]["Reference"]["x"] == pytest.approx(99.0)
|
assert get_result["fields"]["Reference"]["x"] == pytest.approx(99.0)
|
||||||
assert get_result["fields"]["Reference"]["y"] == pytest.approx(88.0)
|
assert get_result["fields"]["Reference"]["y"] == pytest.approx(88.0)
|
||||||
|
|
||||||
def test_reposition_does_not_change_value(self, sch_with_r1):
|
def test_reposition_does_not_change_value(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
iface.handle_command(
|
iface.handle_command(
|
||||||
"edit_schematic_component",
|
"edit_schematic_component",
|
||||||
@@ -318,7 +319,7 @@ class TestEditSchematicComponentFieldPositions:
|
|||||||
assert get_result["fields"]["Value"]["x"] == pytest.approx(51.27)
|
assert get_result["fields"]["Value"]["x"] == pytest.approx(51.27)
|
||||||
assert get_result["fields"]["Value"]["y"] == pytest.approx(52.54)
|
assert get_result["fields"]["Value"]["y"] == pytest.approx(52.54)
|
||||||
|
|
||||||
def test_reposition_multiple_fields(self, sch_with_r1):
|
def test_reposition_multiple_fields(self, sch_with_r1: Any) -> None:
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
"edit_schematic_component",
|
"edit_schematic_component",
|
||||||
@@ -343,7 +344,7 @@ class TestEditSchematicComponentFieldPositions:
|
|||||||
assert get_result["fields"]["Reference"]["x"] == pytest.approx(10.0)
|
assert get_result["fields"]["Reference"]["x"] == pytest.approx(10.0)
|
||||||
assert get_result["fields"]["Value"]["y"] == pytest.approx(30.0)
|
assert get_result["fields"]["Value"]["y"] == pytest.approx(30.0)
|
||||||
|
|
||||||
def test_fieldpositions_alone_is_valid(self, sch_with_r1):
|
def test_fieldpositions_alone_is_valid(self, sch_with_r1: Any) -> None:
|
||||||
"""fieldPositions without value/footprint/newReference should succeed."""
|
"""fieldPositions without value/footprint/newReference should succeed."""
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
@@ -356,7 +357,7 @@ class TestEditSchematicComponentFieldPositions:
|
|||||||
)
|
)
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
|
|
||||||
def test_no_params_still_fails(self, sch_with_r1):
|
def test_no_params_still_fails(self, sch_with_r1: Any) -> None:
|
||||||
"""Providing no update params should return an error."""
|
"""Providing no update params should return an error."""
|
||||||
iface = self._get_interface()
|
iface = self._get_interface()
|
||||||
result = iface.handle_command(
|
result = iface.handle_command(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Covers:
|
|||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -59,22 +60,22 @@ def _write_temp_sch(content: str) -> Path:
|
|||||||
class TestDeleteWireUnit:
|
class TestDeleteWireUnit:
|
||||||
"""Unit-level tests for WireManager.delete_wire."""
|
"""Unit-level tests for WireManager.delete_wire."""
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
self.WireManager = WireManager
|
self.WireManager = WireManager
|
||||||
|
|
||||||
def test_nonexistent_file_returns_false(self, tmp_path):
|
def test_nonexistent_file_returns_false(self, tmp_path: Any) -> None:
|
||||||
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
|
assert result is False
|
||||||
|
|
||||||
def test_no_matching_wire_returns_false(self, tmp_path):
|
def test_no_matching_wire_returns_false(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
shutil.copy(EMPTY_SCH, sch)
|
shutil.copy(EMPTY_SCH, sch)
|
||||||
result = self.WireManager.delete_wire(sch, [99, 99], [100, 100])
|
result = self.WireManager.delete_wire(sch, [99, 99], [100, 100])
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
def test_tolerance_argument_accepted(self, tmp_path):
|
def test_tolerance_argument_accepted(self, tmp_path: Any) -> None:
|
||||||
"""Ensure the tolerance kwarg doesn't raise a TypeError."""
|
"""Ensure the tolerance kwarg doesn't raise a TypeError."""
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
shutil.copy(EMPTY_SCH, sch)
|
shutil.copy(EMPTY_SCH, sch)
|
||||||
@@ -91,22 +92,22 @@ class TestDeleteWireUnit:
|
|||||||
class TestDeleteLabelUnit:
|
class TestDeleteLabelUnit:
|
||||||
"""Unit-level tests for WireManager.delete_label."""
|
"""Unit-level tests for WireManager.delete_label."""
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
self.WireManager = WireManager
|
self.WireManager = WireManager
|
||||||
|
|
||||||
def test_nonexistent_file_returns_false(self, tmp_path):
|
def test_nonexistent_file_returns_false(self, tmp_path: Any) -> None:
|
||||||
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
|
assert result is False
|
||||||
|
|
||||||
def test_missing_label_returns_false(self, tmp_path):
|
def test_missing_label_returns_false(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
shutil.copy(EMPTY_SCH, sch)
|
shutil.copy(EMPTY_SCH, sch)
|
||||||
result = self.WireManager.delete_label(sch, "NONEXISTENT")
|
result = self.WireManager.delete_label(sch, "NONEXISTENT")
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
def test_position_kwarg_accepted(self, tmp_path):
|
def test_position_kwarg_accepted(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
shutil.copy(EMPTY_SCH, 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)
|
||||||
@@ -122,12 +123,12 @@ class TestDeleteLabelUnit:
|
|||||||
class TestDeleteWireIntegration:
|
class TestDeleteWireIntegration:
|
||||||
"""Integration tests that read/write real .kicad_sch files."""
|
"""Integration tests that read/write real .kicad_sch files."""
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
self.WireManager = WireManager
|
self.WireManager = WireManager
|
||||||
|
|
||||||
def test_exact_match_deletes_wire(self, tmp_path):
|
def test_exact_match_deletes_wire(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ class TestDeleteWireIntegration:
|
|||||||
]
|
]
|
||||||
assert wire_items == [], "Wire should have been removed from the file"
|
assert wire_items == [], "Wire should have been removed from the file"
|
||||||
|
|
||||||
def test_reverse_direction_match_deletes_wire(self, tmp_path):
|
def test_reverse_direction_match_deletes_wire(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
|
|
||||||
@@ -151,7 +152,7 @@ class TestDeleteWireIntegration:
|
|||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
def test_within_tolerance_deletes_wire(self, tmp_path):
|
def test_within_tolerance_deletes_wire(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ class TestDeleteWireIntegration:
|
|||||||
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
|
assert result is True
|
||||||
|
|
||||||
def test_outside_tolerance_no_delete(self, tmp_path):
|
def test_outside_tolerance_no_delete(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
|
|
||||||
@@ -171,14 +172,14 @@ class TestDeleteWireIntegration:
|
|||||||
result2 = self.WireManager.delete_wire(sch2, [10.6, 20.0], [30.0, 20.0], tolerance=0.5)
|
result2 = self.WireManager.delete_wire(sch2, [10.6, 20.0], [30.0, 20.0], tolerance=0.5)
|
||||||
assert result2 is False, "Coordinate differs by 0.6 mm — outside 0.5 mm tolerance"
|
assert result2 is False, "Coordinate differs by 0.6 mm — outside 0.5 mm tolerance"
|
||||||
|
|
||||||
def test_file_is_valid_sexp_after_deletion(self, tmp_path):
|
def test_file_is_valid_sexp_after_deletion(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
self.WireManager.delete_wire(sch, [10.0, 20.0], [30.0, 20.0])
|
self.WireManager.delete_wire(sch, [10.0, 20.0], [30.0, 20.0])
|
||||||
# Must parse without exception
|
# Must parse without exception
|
||||||
sexpdata.loads(sch.read_text(encoding="utf-8"))
|
sexpdata.loads(sch.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
def test_label_preserved_after_wire_deletion(self, tmp_path):
|
def test_label_preserved_after_wire_deletion(self, tmp_path: Any) -> None:
|
||||||
"""Deleting a wire must not remove unrelated elements."""
|
"""Deleting a wire must not remove unrelated elements."""
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
@@ -199,12 +200,12 @@ class TestDeleteWireIntegration:
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
class TestDeleteLabelIntegration:
|
class TestDeleteLabelIntegration:
|
||||||
def setup_method(self):
|
def setup_method(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
self.WireManager = WireManager
|
self.WireManager = WireManager
|
||||||
|
|
||||||
def test_deletes_label_by_name(self, tmp_path):
|
def test_deletes_label_by_name(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
|
|
||||||
@@ -219,21 +220,21 @@ class TestDeleteLabelIntegration:
|
|||||||
]
|
]
|
||||||
assert labels == [], "Label should have been removed"
|
assert labels == [], "Label should have been removed"
|
||||||
|
|
||||||
def test_deletes_label_with_matching_position(self, tmp_path):
|
def test_deletes_label_with_matching_position(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
|
|
||||||
result = self.WireManager.delete_label(sch, "VCC", position=[50.0, 50.0])
|
result = self.WireManager.delete_label(sch, "VCC", position=[50.0, 50.0])
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
def test_position_mismatch_no_delete(self, tmp_path):
|
def test_position_mismatch_no_delete(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
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
|
assert result is False
|
||||||
|
|
||||||
def test_wire_preserved_after_label_deletion(self, tmp_path):
|
def test_wire_preserved_after_label_deletion(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
self.WireManager.delete_label(sch, "VCC")
|
self.WireManager.delete_label(sch, "VCC")
|
||||||
@@ -245,7 +246,7 @@ class TestDeleteLabelIntegration:
|
|||||||
]
|
]
|
||||||
assert len(wires) == 1
|
assert len(wires) == 1
|
||||||
|
|
||||||
def test_file_is_valid_sexp_after_deletion(self, tmp_path):
|
def test_file_is_valid_sexp_after_deletion(self, tmp_path: Any) -> None:
|
||||||
sch = tmp_path / "test.kicad_sch"
|
sch = tmp_path / "test.kicad_sch"
|
||||||
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
sch.write_text(_WIRE_SCH, encoding="utf-8")
|
||||||
self.WireManager.delete_label(sch, "VCC")
|
self.WireManager.delete_label(sch, "VCC")
|
||||||
@@ -260,7 +261,7 @@ class TestDeleteLabelIntegration:
|
|||||||
# Each handler is extracted as a standalone function for testing.
|
# Each handler is extracted as a standalone function for testing.
|
||||||
|
|
||||||
|
|
||||||
def _make_handler_under_test(handler_name: str):
|
def _make_handler_under_test(handler_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Return the unbound handler method from kicad_interface by importing only
|
Return the unbound handler method from kicad_interface by importing only
|
||||||
that method's source via exec, bypassing module-level side effects.
|
that method's source via exec, bypassing module-level side effects.
|
||||||
@@ -300,7 +301,7 @@ class TestHandlerParamValidation:
|
|||||||
that satisfy the dependency chain up to the first parameter-check branch.
|
that satisfy the dependency chain up to the first parameter-check branch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _make_iface_stub(self):
|
def _make_iface_stub(self) -> Any:
|
||||||
"""Return a stub that exposes only the handler methods under test."""
|
"""Return a stub that exposes only the handler methods under test."""
|
||||||
import importlib
|
import importlib
|
||||||
import types
|
import types
|
||||||
@@ -316,7 +317,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- delete_schematic_wire ---
|
# --- delete_schematic_wire ---
|
||||||
|
|
||||||
def test_delete_wire_missing_schematic_path(self):
|
def test_delete_wire_missing_schematic_path(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
with patch.object(WireManager, "delete_wire", return_value=False):
|
with patch.object(WireManager, "delete_wire", return_value=False):
|
||||||
@@ -325,7 +326,7 @@ class TestHandlerParamValidation:
|
|||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
assert schematic_path is None
|
assert schematic_path is None
|
||||||
# Handler should short-circuit before calling WireManager
|
# Handler should short-circuit before calling WireManager
|
||||||
result = (
|
result: dict[str, Any] = (
|
||||||
{"success": False, "message": "schematicPath is required"}
|
{"success": False, "message": "schematicPath is required"}
|
||||||
if not schematic_path
|
if not schematic_path
|
||||||
else {}
|
else {}
|
||||||
@@ -335,7 +336,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- delete_schematic_net_label ---
|
# --- delete_schematic_net_label ---
|
||||||
|
|
||||||
def test_delete_label_missing_net_name(self):
|
def test_delete_label_missing_net_name(self) -> None:
|
||||||
params = {"schematicPath": "/some/file.kicad_sch"}
|
params = {"schematicPath": "/some/file.kicad_sch"}
|
||||||
net_name = params.get("netName")
|
net_name = params.get("netName")
|
||||||
result = (
|
result = (
|
||||||
@@ -348,7 +349,7 @@ class TestHandlerParamValidation:
|
|||||||
)
|
)
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
|
|
||||||
def test_delete_label_missing_schematic_path(self):
|
def test_delete_label_missing_schematic_path(self) -> None:
|
||||||
params = {"netName": "VCC"}
|
params = {"netName": "VCC"}
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
result = (
|
result = (
|
||||||
@@ -363,7 +364,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- list_schematic_components ---
|
# --- list_schematic_components ---
|
||||||
|
|
||||||
def test_list_components_missing_path(self):
|
def test_list_components_missing_path(self) -> None:
|
||||||
params = {}
|
params = {}
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
result = (
|
result = (
|
||||||
@@ -373,7 +374,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- list_schematic_nets ---
|
# --- list_schematic_nets ---
|
||||||
|
|
||||||
def test_list_nets_missing_path(self):
|
def test_list_nets_missing_path(self) -> None:
|
||||||
params = {}
|
params = {}
|
||||||
result = (
|
result = (
|
||||||
{"success": False, "message": "schematicPath is required"}
|
{"success": False, "message": "schematicPath is required"}
|
||||||
@@ -384,7 +385,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- list_schematic_wires ---
|
# --- list_schematic_wires ---
|
||||||
|
|
||||||
def test_list_wires_missing_path(self):
|
def test_list_wires_missing_path(self) -> None:
|
||||||
params = {}
|
params = {}
|
||||||
result = (
|
result = (
|
||||||
{"success": False, "message": "schematicPath is required"}
|
{"success": False, "message": "schematicPath is required"}
|
||||||
@@ -395,7 +396,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- list_schematic_labels ---
|
# --- list_schematic_labels ---
|
||||||
|
|
||||||
def test_list_labels_missing_path(self):
|
def test_list_labels_missing_path(self) -> None:
|
||||||
params = {}
|
params = {}
|
||||||
result = (
|
result = (
|
||||||
{"success": False, "message": "schematicPath is required"}
|
{"success": False, "message": "schematicPath is required"}
|
||||||
@@ -406,7 +407,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- move_schematic_component ---
|
# --- move_schematic_component ---
|
||||||
|
|
||||||
def test_move_component_missing_reference(self):
|
def test_move_component_missing_reference(self) -> None:
|
||||||
params = {
|
params = {
|
||||||
"schematicPath": "/some/file.kicad_sch",
|
"schematicPath": "/some/file.kicad_sch",
|
||||||
"position": {"x": 10, "y": 20},
|
"position": {"x": 10, "y": 20},
|
||||||
@@ -421,7 +422,7 @@ class TestHandlerParamValidation:
|
|||||||
)
|
)
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
|
|
||||||
def test_move_component_missing_position(self):
|
def test_move_component_missing_position(self) -> None:
|
||||||
params = {
|
params = {
|
||||||
"schematicPath": "/some/file.kicad_sch",
|
"schematicPath": "/some/file.kicad_sch",
|
||||||
"reference": "R1",
|
"reference": "R1",
|
||||||
@@ -438,7 +439,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- rotate_schematic_component ---
|
# --- rotate_schematic_component ---
|
||||||
|
|
||||||
def test_rotate_component_missing_reference(self):
|
def test_rotate_component_missing_reference(self) -> None:
|
||||||
params = {"schematicPath": "/some/file.kicad_sch"}
|
params = {"schematicPath": "/some/file.kicad_sch"}
|
||||||
result = (
|
result = (
|
||||||
{
|
{
|
||||||
@@ -452,7 +453,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- annotate_schematic ---
|
# --- annotate_schematic ---
|
||||||
|
|
||||||
def test_annotate_missing_path(self):
|
def test_annotate_missing_path(self) -> None:
|
||||||
params = {}
|
params = {}
|
||||||
result = (
|
result = (
|
||||||
{"success": False, "message": "schematicPath is required"}
|
{"success": False, "message": "schematicPath is required"}
|
||||||
@@ -463,7 +464,7 @@ class TestHandlerParamValidation:
|
|||||||
|
|
||||||
# --- export_schematic_svg ---
|
# --- export_schematic_svg ---
|
||||||
|
|
||||||
def test_export_svg_missing_output_path(self):
|
def test_export_svg_missing_output_path(self) -> None:
|
||||||
params = {"schematicPath": "/some/file.kicad_sch"}
|
params = {"schematicPath": "/some/file.kicad_sch"}
|
||||||
result = (
|
result = (
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def _make_wire(x1: float, y1: float, x2: float, y2: float) -> MagicMock:
|
|||||||
return wire
|
return wire
|
||||||
|
|
||||||
|
|
||||||
def _make_schematic(*wires) -> MagicMock:
|
def _make_schematic(*wires: Any) -> MagicMock:
|
||||||
sch = MagicMock()
|
sch = MagicMock()
|
||||||
sch.wire = list(wires)
|
sch.wire = list(wires)
|
||||||
# No net labels, no symbols by default
|
# No net labels, no symbols by default
|
||||||
@@ -66,12 +66,12 @@ def _make_schematic(*wires) -> MagicMock:
|
|||||||
class TestSchema:
|
class TestSchema:
|
||||||
"""Verify the get_wire_connections tool schema is present and well-formed."""
|
"""Verify the get_wire_connections tool schema is present and well-formed."""
|
||||||
|
|
||||||
def test_schema_registered(self):
|
def test_schema_registered(self) -> None:
|
||||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||||
|
|
||||||
assert "get_wire_connections" in TOOL_SCHEMAS
|
assert "get_wire_connections" in TOOL_SCHEMAS
|
||||||
|
|
||||||
def test_schema_required_fields(self):
|
def test_schema_required_fields(self) -> None:
|
||||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||||
|
|
||||||
schema = TOOL_SCHEMAS["get_wire_connections"]
|
schema = TOOL_SCHEMAS["get_wire_connections"]
|
||||||
@@ -80,7 +80,7 @@ class TestSchema:
|
|||||||
assert "x" in required
|
assert "x" in required
|
||||||
assert "y" in required
|
assert "y" in required
|
||||||
|
|
||||||
def test_schema_has_title_and_description(self):
|
def test_schema_has_title_and_description(self) -> None:
|
||||||
from schemas.tool_schemas import TOOL_SCHEMAS
|
from schemas.tool_schemas import TOOL_SCHEMAS
|
||||||
|
|
||||||
schema = TOOL_SCHEMAS["get_wire_connections"]
|
schema = TOOL_SCHEMAS["get_wire_connections"]
|
||||||
@@ -97,7 +97,7 @@ class TestSchema:
|
|||||||
class TestHandlerDispatch:
|
class TestHandlerDispatch:
|
||||||
"""Verify the handler is wired into KiCadInterface.command_routes."""
|
"""Verify the handler is wired into KiCadInterface.command_routes."""
|
||||||
|
|
||||||
def test_get_wire_connections_in_routes(self):
|
def test_get_wire_connections_in_routes(self) -> None:
|
||||||
# Import lazily to avoid heavy side-effects at collection time
|
# Import lazily to avoid heavy side-effects at collection time
|
||||||
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
@@ -116,7 +116,7 @@ class TestHandlerDispatch:
|
|||||||
|
|
||||||
# Build routes only (avoid full __init__ side-effects)
|
# Build routes only (avoid full __init__ side-effects)
|
||||||
# The routes dict is built in __init__; we call it directly.
|
# The routes dict is built in __init__; we call it directly.
|
||||||
iface.__init__()
|
KiCADInterface.__init__(iface)
|
||||||
|
|
||||||
assert "get_wire_connections" in iface.command_routes
|
assert "get_wire_connections" in iface.command_routes
|
||||||
assert callable(iface.command_routes["get_wire_connections"])
|
assert callable(iface.command_routes["get_wire_connections"])
|
||||||
@@ -131,7 +131,7 @@ class TestHandlerDispatch:
|
|||||||
class TestHandlerParamValidation:
|
class TestHandlerParamValidation:
|
||||||
"""Handler returns error responses for bad or missing parameters."""
|
"""Handler returns error responses for bad or missing parameters."""
|
||||||
|
|
||||||
def _make_handler(self):
|
def _make_handler(self) -> Any:
|
||||||
"""Return a bound _handle_get_wire_connections without full init."""
|
"""Return a bound _handle_get_wire_connections without full init."""
|
||||||
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
with patch("kicad_interface.USE_IPC_BACKEND", False):
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
@@ -139,29 +139,29 @@ class TestHandlerParamValidation:
|
|||||||
iface = KiCADInterface.__new__(KiCADInterface)
|
iface = KiCADInterface.__new__(KiCADInterface)
|
||||||
return iface._handle_get_wire_connections
|
return iface._handle_get_wire_connections
|
||||||
|
|
||||||
def test_missing_schematic_path(self):
|
def test_missing_schematic_path(self) -> None:
|
||||||
handler = self._make_handler()
|
handler = self._make_handler()
|
||||||
result = handler({"x": 1.0, "y": 2.0})
|
result = handler({"x": 1.0, "y": 2.0})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "schematicPath" in result["message"] or "Missing" in result["message"]
|
assert "schematicPath" in result["message"] or "Missing" in result["message"]
|
||||||
|
|
||||||
def test_missing_x(self):
|
def test_missing_x(self) -> None:
|
||||||
handler = self._make_handler()
|
handler = self._make_handler()
|
||||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "y": 2.0})
|
result = handler({"schematicPath": "/tmp/test.kicad_sch", "y": 2.0})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
|
|
||||||
def test_missing_y(self):
|
def test_missing_y(self) -> None:
|
||||||
handler = self._make_handler()
|
handler = self._make_handler()
|
||||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0})
|
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
|
|
||||||
def test_non_numeric_x(self):
|
def test_non_numeric_x(self) -> None:
|
||||||
handler = self._make_handler()
|
handler = self._make_handler()
|
||||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": "bad", "y": 2.0})
|
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": "bad", "y": 2.0})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "numeric" in result["message"].lower() or "x" in result["message"]
|
assert "numeric" in result["message"].lower() or "x" in result["message"]
|
||||||
|
|
||||||
def test_non_numeric_y(self):
|
def test_non_numeric_y(self) -> None:
|
||||||
handler = self._make_handler()
|
handler = self._make_handler()
|
||||||
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0, "y": "bad"})
|
result = handler({"schematicPath": "/tmp/test.kicad_sch", "x": 1.0, "y": "bad"})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
@@ -180,39 +180,39 @@ class TestCoreLogic:
|
|||||||
|
|
||||||
# --- _to_iu ---
|
# --- _to_iu ---
|
||||||
|
|
||||||
def test_to_iu_integer_mm(self):
|
def test_to_iu_integer_mm(self) -> None:
|
||||||
assert _to_iu(1.0, 2.0) == (10_000, 20_000)
|
assert _to_iu(1.0, 2.0) == (10_000, 20_000)
|
||||||
|
|
||||||
def test_to_iu_fractional_mm(self):
|
def test_to_iu_fractional_mm(self) -> None:
|
||||||
assert _to_iu(0.5, 0.25) == (5_000, 2_500)
|
assert _to_iu(0.5, 0.25) == (5_000, 2_500)
|
||||||
|
|
||||||
def test_to_iu_zero(self):
|
def test_to_iu_zero(self) -> None:
|
||||||
assert _to_iu(0.0, 0.0) == (0, 0)
|
assert _to_iu(0.0, 0.0) == (0, 0)
|
||||||
|
|
||||||
def test_to_iu_negative(self):
|
def test_to_iu_negative(self) -> None:
|
||||||
assert _to_iu(-1.0, -2.0) == (-10_000, -20_000)
|
assert _to_iu(-1.0, -2.0) == (-10_000, -20_000)
|
||||||
|
|
||||||
# --- _parse_wires ---
|
# --- _parse_wires ---
|
||||||
|
|
||||||
def test_parse_wires_single_wire(self):
|
def test_parse_wires_single_wire(self) -> None:
|
||||||
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
|
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
|
||||||
result = _parse_wires(sch)
|
result = _parse_wires(sch)
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0] == [(0, 0), (10_000, 0)]
|
assert result[0] == [(0, 0), (10_000, 0)]
|
||||||
|
|
||||||
def test_parse_wires_empty_schematic(self):
|
def test_parse_wires_empty_schematic(self) -> None:
|
||||||
sch = MagicMock()
|
sch = MagicMock()
|
||||||
sch.wire = []
|
sch.wire = []
|
||||||
assert _parse_wires(sch) == []
|
assert _parse_wires(sch) == []
|
||||||
|
|
||||||
def test_parse_wires_multiple_wires(self):
|
def test_parse_wires_multiple_wires(self) -> None:
|
||||||
sch = _make_schematic(
|
sch = _make_schematic(
|
||||||
_make_wire(0.0, 0.0, 1.0, 0.0),
|
_make_wire(0.0, 0.0, 1.0, 0.0),
|
||||||
_make_wire(1.0, 0.0, 2.0, 0.0),
|
_make_wire(1.0, 0.0, 2.0, 0.0),
|
||||||
)
|
)
|
||||||
assert len(_parse_wires(sch)) == 2
|
assert len(_parse_wires(sch)) == 2
|
||||||
|
|
||||||
def test_parse_wires_skips_wire_without_pts(self):
|
def test_parse_wires_skips_wire_without_pts(self) -> None:
|
||||||
bad_wire = MagicMock(spec=[]) # no `pts` attribute
|
bad_wire = MagicMock(spec=[]) # no `pts` attribute
|
||||||
sch = MagicMock()
|
sch = MagicMock()
|
||||||
sch.wire = [bad_wire]
|
sch.wire = [bad_wire]
|
||||||
@@ -220,7 +220,7 @@ class TestCoreLogic:
|
|||||||
|
|
||||||
# --- _build_adjacency ---
|
# --- _build_adjacency ---
|
||||||
|
|
||||||
def test_build_adjacency_two_connected_wires(self):
|
def test_build_adjacency_two_connected_wires(self) -> None:
|
||||||
# wire0: (0,0)-(1,0), wire1: (1,0)-(2,0) — share endpoint (1,0)
|
# wire0: (0,0)-(1,0), wire1: (1,0)-(2,0) — share endpoint (1,0)
|
||||||
wires = [
|
wires = [
|
||||||
[(0, 0), (10_000, 0)],
|
[(0, 0), (10_000, 0)],
|
||||||
@@ -230,7 +230,7 @@ class TestCoreLogic:
|
|||||||
assert 1 in adjacency[0]
|
assert 1 in adjacency[0]
|
||||||
assert 0 in adjacency[1]
|
assert 0 in adjacency[1]
|
||||||
|
|
||||||
def test_build_adjacency_two_disconnected_wires(self):
|
def test_build_adjacency_two_disconnected_wires(self) -> None:
|
||||||
wires = [
|
wires = [
|
||||||
[(0, 0), (10_000, 0)],
|
[(0, 0), (10_000, 0)],
|
||||||
[(20_000, 0), (30_000, 0)],
|
[(20_000, 0), (30_000, 0)],
|
||||||
@@ -239,7 +239,7 @@ class TestCoreLogic:
|
|||||||
assert adjacency[0] == set()
|
assert adjacency[0] == set()
|
||||||
assert adjacency[1] == set()
|
assert adjacency[1] == set()
|
||||||
|
|
||||||
def test_build_adjacency_iu_to_wires_maps_correctly(self):
|
def test_build_adjacency_iu_to_wires_maps_correctly(self) -> None:
|
||||||
wires = [
|
wires = [
|
||||||
[(0, 0), (10_000, 0)],
|
[(0, 0), (10_000, 0)],
|
||||||
[(10_000, 0), (20_000, 0)],
|
[(10_000, 0), (20_000, 0)],
|
||||||
@@ -248,7 +248,7 @@ class TestCoreLogic:
|
|||||||
assert iu_to_wires[(10_000, 0)] == {0, 1}
|
assert iu_to_wires[(10_000, 0)] == {0, 1}
|
||||||
assert iu_to_wires[(0, 0)] == {0}
|
assert iu_to_wires[(0, 0)] == {0}
|
||||||
|
|
||||||
def test_build_adjacency_three_wires_at_junction(self):
|
def test_build_adjacency_three_wires_at_junction(self) -> None:
|
||||||
# All three wires meet at (10,000, 0)
|
# All three wires meet at (10,000, 0)
|
||||||
wires = [
|
wires = [
|
||||||
[(0, 0), (10_000, 0)],
|
[(0, 0), (10_000, 0)],
|
||||||
@@ -262,14 +262,14 @@ class TestCoreLogic:
|
|||||||
|
|
||||||
# --- _find_connected_wires ---
|
# --- _find_connected_wires ---
|
||||||
|
|
||||||
def test_find_connected_wires_no_wire_at_point(self):
|
def test_find_connected_wires_no_wire_at_point(self) -> None:
|
||||||
wires = [[(0, 0), (10_000, 0)]]
|
wires = [[(0, 0), (10_000, 0)]]
|
||||||
adjacency, iu_to_wires = _build_adjacency(wires)
|
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 visited is None
|
||||||
assert net_points is None
|
assert net_points is None
|
||||||
|
|
||||||
def test_find_connected_wires_single_wire(self):
|
def test_find_connected_wires_single_wire(self) -> None:
|
||||||
wires = [[(0, 0), (10_000, 0)]]
|
wires = [[(0, 0), (10_000, 0)]]
|
||||||
adjacency, iu_to_wires = _build_adjacency(wires)
|
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)
|
||||||
@@ -277,7 +277,7 @@ class TestCoreLogic:
|
|||||||
assert (0, 0) in net_points
|
assert (0, 0) in net_points
|
||||||
assert (10_000, 0) in net_points
|
assert (10_000, 0) in net_points
|
||||||
|
|
||||||
def test_find_connected_wires_flood_fills_chain(self):
|
def test_find_connected_wires_flood_fills_chain(self) -> None:
|
||||||
# Three wires in a chain: A-B-C-D
|
# Three wires in a chain: A-B-C-D
|
||||||
wires = [
|
wires = [
|
||||||
[(0, 0), (10_000, 0)],
|
[(0, 0), (10_000, 0)],
|
||||||
@@ -288,7 +288,7 @@ class TestCoreLogic:
|
|||||||
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}
|
assert visited == {0, 1, 2}
|
||||||
|
|
||||||
def test_find_connected_wires_does_not_cross_gap(self):
|
def test_find_connected_wires_does_not_cross_gap(self) -> None:
|
||||||
# Two disconnected segments; query on segment 0 should not reach segment 1
|
# Two disconnected segments; query on segment 0 should not reach segment 1
|
||||||
wires = [
|
wires = [
|
||||||
[(0, 0), (10_000, 0)],
|
[(0, 0), (10_000, 0)],
|
||||||
@@ -300,18 +300,18 @@ class TestCoreLogic:
|
|||||||
|
|
||||||
# --- get_wire_connections (integration of internal functions) ---
|
# --- get_wire_connections (integration of internal functions) ---
|
||||||
|
|
||||||
def test_get_wire_connections_no_wires(self):
|
def test_get_wire_connections_no_wires(self) -> None:
|
||||||
sch = MagicMock()
|
sch = MagicMock()
|
||||||
sch.wire = []
|
sch.wire = []
|
||||||
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
|
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
|
||||||
assert result == {"pins": [], "wires": []}
|
assert result == {"pins": [], "wires": []}
|
||||||
|
|
||||||
def test_get_wire_connections_no_wire_at_point_returns_none(self):
|
def test_get_wire_connections_no_wire_at_point_returns_none(self) -> None:
|
||||||
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
|
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
|
||||||
result = get_wire_connections(sch, "/fake/path.kicad_sch", 5.0, 0.0)
|
result = get_wire_connections(sch, "/fake/path.kicad_sch", 5.0, 0.0)
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
def test_get_wire_connections_returns_wire_data(self):
|
def test_get_wire_connections_returns_wire_data(self) -> None:
|
||||||
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
|
sch = _make_schematic(_make_wire(0.0, 0.0, 1.0, 0.0))
|
||||||
# Prevent _find_pins_on_net from iterating symbols
|
# Prevent _find_pins_on_net from iterating symbols
|
||||||
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
|
result = get_wire_connections(sch, "/fake/path.kicad_sch", 0.0, 0.0)
|
||||||
@@ -322,7 +322,7 @@ class TestCoreLogic:
|
|||||||
assert wire["start"] == {"x": 0.0, "y": 0.0}
|
assert wire["start"] == {"x": 0.0, "y": 0.0}
|
||||||
assert wire["end"] == {"x": 1.0, "y": 0.0}
|
assert wire["end"] == {"x": 1.0, "y": 0.0}
|
||||||
|
|
||||||
def test_get_wire_connections_chain_returns_all_wires(self):
|
def test_get_wire_connections_chain_returns_all_wires(self) -> None:
|
||||||
sch = _make_schematic(
|
sch = _make_schematic(
|
||||||
_make_wire(0.0, 0.0, 1.0, 0.0),
|
_make_wire(0.0, 0.0, 1.0, 0.0),
|
||||||
_make_wire(1.0, 0.0, 2.0, 0.0),
|
_make_wire(1.0, 0.0, 2.0, 0.0),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -29,20 +30,20 @@ EMPTY_SCH = TEMPLATES_DIR / "empty.kicad_sch"
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _make_temp_sch():
|
def _make_temp_sch() -> Any:
|
||||||
"""Copy the empty schematic template to a temp file and return the Path."""
|
"""Copy the empty schematic template to a temp file and return the Path."""
|
||||||
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
tmp = Path(tempfile.mkdtemp()) / "test.kicad_sch"
|
||||||
shutil.copy(EMPTY_SCH, tmp)
|
shutil.copy(EMPTY_SCH, tmp)
|
||||||
return tmp
|
return tmp
|
||||||
|
|
||||||
|
|
||||||
def _parse_sch(path: Path):
|
def _parse_sch(path: Path) -> Any:
|
||||||
"""Parse a .kicad_sch file and return the S-expression list."""
|
"""Parse a .kicad_sch file and return the S-expression list."""
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
return sexpdata.loads(f.read())
|
return sexpdata.loads(f.read())
|
||||||
|
|
||||||
|
|
||||||
def _find_elements(sch_data, tag: str):
|
def _find_elements(sch_data: Any, tag: str) -> Any:
|
||||||
"""Return all top-level S-expression elements with the given tag Symbol."""
|
"""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)]
|
||||||
|
|
||||||
@@ -56,22 +57,22 @@ class TestSchemas:
|
|||||||
"""Verify tool_schemas.py reflects the new API."""
|
"""Verify tool_schemas.py reflects the new API."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def load_schemas(self):
|
def load_schemas(self) -> Any:
|
||||||
from schemas.tool_schemas import SCHEMATIC_TOOLS
|
from schemas.tool_schemas import SCHEMATIC_TOOLS
|
||||||
|
|
||||||
self.tools = {t["name"]: t for t in SCHEMATIC_TOOLS}
|
self.tools = {t["name"]: t for t in SCHEMATIC_TOOLS}
|
||||||
|
|
||||||
def test_add_schematic_wire_has_waypoints(self):
|
def test_add_schematic_wire_has_waypoints(self) -> None:
|
||||||
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
||||||
assert "waypoints" in schema["properties"], "waypoints must be a property"
|
assert "waypoints" in schema["properties"], "waypoints must be a property"
|
||||||
assert "waypoints" in schema["required"]
|
assert "waypoints" in schema["required"]
|
||||||
|
|
||||||
def test_add_schematic_wire_has_schematic_path(self):
|
def test_add_schematic_wire_has_schematic_path(self) -> None:
|
||||||
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
||||||
assert "schematicPath" in schema["properties"]
|
assert "schematicPath" in schema["properties"]
|
||||||
assert "schematicPath" in schema["required"]
|
assert "schematicPath" in schema["required"]
|
||||||
|
|
||||||
def test_add_schematic_wire_has_snap_params(self):
|
def test_add_schematic_wire_has_snap_params(self) -> None:
|
||||||
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
||||||
props = schema["properties"]
|
props = schema["properties"]
|
||||||
assert "snapToPins" in props
|
assert "snapToPins" in props
|
||||||
@@ -79,28 +80,28 @@ class TestSchemas:
|
|||||||
assert "snapTolerance" in props
|
assert "snapTolerance" in props
|
||||||
assert props["snapTolerance"]["type"] == "number"
|
assert props["snapTolerance"]["type"] == "number"
|
||||||
|
|
||||||
def test_add_schematic_wire_no_old_point_params(self):
|
def test_add_schematic_wire_no_old_point_params(self) -> None:
|
||||||
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
schema = self.tools["add_schematic_wire"]["inputSchema"]
|
||||||
props = schema["properties"]
|
props = schema["properties"]
|
||||||
assert "startPoint" not in props, "startPoint should be removed"
|
assert "startPoint" not in props, "startPoint should be removed"
|
||||||
assert "endPoint" not in props, "endPoint should be removed"
|
assert "endPoint" not in props, "endPoint should be removed"
|
||||||
|
|
||||||
def test_add_schematic_connection_removed(self):
|
def test_add_schematic_connection_removed(self) -> None:
|
||||||
assert (
|
assert (
|
||||||
"add_schematic_connection" not in self.tools
|
"add_schematic_connection" not in self.tools
|
||||||
), "add_schematic_connection must not appear in SCHEMATIC_TOOLS"
|
), "add_schematic_connection must not appear in SCHEMATIC_TOOLS"
|
||||||
|
|
||||||
def test_add_schematic_junction_present(self):
|
def test_add_schematic_junction_present(self) -> None:
|
||||||
assert "add_schematic_junction" in self.tools
|
assert "add_schematic_junction" in self.tools
|
||||||
|
|
||||||
def test_add_schematic_junction_schema(self):
|
def test_add_schematic_junction_schema(self) -> None:
|
||||||
schema = self.tools["add_schematic_junction"]["inputSchema"]
|
schema = self.tools["add_schematic_junction"]["inputSchema"]
|
||||||
props = schema["properties"]
|
props = schema["properties"]
|
||||||
assert "schematicPath" in props
|
assert "schematicPath" in props
|
||||||
assert "position" in props
|
assert "position" in props
|
||||||
assert set(schema["required"]) >= {"schematicPath", "position"}
|
assert set(schema["required"]) >= {"schematicPath", "position"}
|
||||||
|
|
||||||
def test_add_schematic_junction_position_is_array(self):
|
def test_add_schematic_junction_position_is_array(self) -> None:
|
||||||
schema = self.tools["add_schematic_junction"]["inputSchema"]
|
schema = self.tools["add_schematic_junction"]["inputSchema"]
|
||||||
pos = schema["properties"]["position"]
|
pos = schema["properties"]["position"]
|
||||||
assert pos["type"] == "array"
|
assert pos["type"] == "array"
|
||||||
@@ -117,7 +118,7 @@ class TestHandlerDispatch:
|
|||||||
"""Verify KiCADInterface registers the right tool handlers."""
|
"""Verify KiCADInterface registers the right tool handlers."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def load_handler_map(self):
|
def load_handler_map(self) -> Any:
|
||||||
# Import only the dispatch table without initialising KiCAD connections
|
# Import only the dispatch table without initialising KiCAD connections
|
||||||
import importlib
|
import importlib
|
||||||
import types
|
import types
|
||||||
@@ -141,18 +142,18 @@ class TestHandlerDispatch:
|
|||||||
}
|
}
|
||||||
self.handlers = obj._tool_handlers
|
self.handlers = obj._tool_handlers
|
||||||
|
|
||||||
def test_add_schematic_wire_registered(self):
|
def test_add_schematic_wire_registered(self) -> None:
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
# Just verify the class has the handler method
|
# Just verify the class has the handler method
|
||||||
assert hasattr(KiCADInterface, "_handle_add_schematic_wire")
|
assert hasattr(KiCADInterface, "_handle_add_schematic_wire")
|
||||||
|
|
||||||
def test_add_schematic_junction_registered(self):
|
def test_add_schematic_junction_registered(self) -> None:
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
assert hasattr(KiCADInterface, "_handle_add_schematic_junction")
|
assert hasattr(KiCADInterface, "_handle_add_schematic_junction")
|
||||||
|
|
||||||
def test_add_schematic_connection_not_present(self):
|
def test_add_schematic_connection_not_present(self) -> None:
|
||||||
from kicad_interface import KiCADInterface
|
from kicad_interface import KiCADInterface
|
||||||
|
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
@@ -169,7 +170,7 @@ class TestHandleAddSchematicWireValidation:
|
|||||||
"""Unit tests for _handle_add_schematic_wire validation paths (no disk I/O)."""
|
"""Unit tests for _handle_add_schematic_wire validation paths (no disk I/O)."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def handler(self):
|
def handler(self) -> Any:
|
||||||
import types
|
import types
|
||||||
|
|
||||||
for mod in ["pcbnew", "skip"]:
|
for mod in ["pcbnew", "skip"]:
|
||||||
@@ -179,17 +180,17 @@ class TestHandleAddSchematicWireValidation:
|
|||||||
with patch.object(KiCADInterface, "__init__", lambda self, *a, **kw: None):
|
with patch.object(KiCADInterface, "__init__", lambda self, *a, **kw: None):
|
||||||
self.iface = KiCADInterface.__new__(KiCADInterface)
|
self.iface = KiCADInterface.__new__(KiCADInterface)
|
||||||
|
|
||||||
def test_missing_schematic_path(self):
|
def test_missing_schematic_path(self) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire({"waypoints": [[0, 0], [10, 0]]})
|
result = self.iface._handle_add_schematic_wire({"waypoints": [[0, 0], [10, 0]]})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Schematic path" in result["message"]
|
assert "Schematic path" in result["message"]
|
||||||
|
|
||||||
def test_missing_waypoints(self):
|
def test_missing_waypoints(self) -> None:
|
||||||
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 result["success"] is False
|
||||||
assert "waypoint" in result["message"].lower()
|
assert "waypoint" in result["message"].lower()
|
||||||
|
|
||||||
def test_single_waypoint_rejected(self):
|
def test_single_waypoint_rejected(self) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
"schematicPath": "/tmp/x.kicad_sch",
|
"schematicPath": "/tmp/x.kicad_sch",
|
||||||
@@ -209,7 +210,7 @@ class TestHandleAddSchematicWireRouting:
|
|||||||
"""Unit tests verifying add_wire vs add_polyline_wire dispatch, no pin snapping."""
|
"""Unit tests verifying add_wire vs add_polyline_wire dispatch, no pin snapping."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self):
|
def setup(self) -> Any:
|
||||||
import types
|
import types
|
||||||
|
|
||||||
for mod in ["pcbnew", "skip"]:
|
for mod in ["pcbnew", "skip"]:
|
||||||
@@ -224,7 +225,7 @@ class TestHandleAddSchematicWireRouting:
|
|||||||
shutil.rmtree(self.sch_path.parent, ignore_errors=True)
|
shutil.rmtree(self.sch_path.parent, ignore_errors=True)
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
|
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
|
||||||
def test_two_waypoints_calls_add_wire(self, mock_add_wire):
|
def test_two_waypoints_calls_add_wire(self, mock_add_wire: Any) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
"schematicPath": str(self.sch_path),
|
"schematicPath": str(self.sch_path),
|
||||||
@@ -239,7 +240,7 @@ class TestHandleAddSchematicWireRouting:
|
|||||||
assert args[2] == [30.0, 20.0]
|
assert args[2] == [30.0, 20.0]
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_polyline_wire", return_value=True)
|
@patch("commands.wire_manager.WireManager.add_polyline_wire", return_value=True)
|
||||||
def test_four_waypoints_calls_add_polyline_wire(self, mock_poly):
|
def test_four_waypoints_calls_add_polyline_wire(self, mock_poly: Any) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
"schematicPath": str(self.sch_path),
|
"schematicPath": str(self.sch_path),
|
||||||
@@ -250,7 +251,7 @@ class TestHandleAddSchematicWireRouting:
|
|||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
mock_poly.assert_called_once()
|
mock_poly.assert_called_once()
|
||||||
|
|
||||||
def test_points_key_without_waypoints_is_rejected(self):
|
def test_points_key_without_waypoints_is_rejected(self) -> None:
|
||||||
"""'points' key alone (without 'waypoints') is rejected — no fallback."""
|
"""'points' key alone (without 'waypoints') is rejected — no fallback."""
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
@@ -263,7 +264,7 @@ class TestHandleAddSchematicWireRouting:
|
|||||||
assert "waypoint" in result["message"].lower()
|
assert "waypoint" in result["message"].lower()
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_wire", return_value=False)
|
@patch("commands.wire_manager.WireManager.add_wire", return_value=False)
|
||||||
def test_failure_response(self, _):
|
def test_failure_response(self, _: Any) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
"schematicPath": str(self.sch_path),
|
"schematicPath": str(self.sch_path),
|
||||||
@@ -283,18 +284,18 @@ class TestPinSnapping:
|
|||||||
"""Verify pin snapping logic snaps endpoints correctly."""
|
"""Verify pin snapping logic snaps endpoints correctly."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self):
|
def setup(self) -> Any:
|
||||||
import types
|
import types
|
||||||
|
|
||||||
# Provide a minimal skip.Schematic stub so the handler can import it
|
# Provide a minimal skip.Schematic stub so the handler can import it
|
||||||
skip_mod = types.ModuleType("skip")
|
skip_mod = types.ModuleType("skip")
|
||||||
|
|
||||||
class FakeSchematic:
|
class FakeSchematic:
|
||||||
def __init__(self, path):
|
def __init__(self, path: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def symbol(self):
|
def symbol(self) -> list[Any]:
|
||||||
return [] # no symbols → no pins in snapping loop
|
return [] # no symbols → no pins in snapping loop
|
||||||
|
|
||||||
skip_mod.Schematic = FakeSchematic
|
skip_mod.Schematic = FakeSchematic
|
||||||
@@ -312,7 +313,7 @@ class TestPinSnapping:
|
|||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
|
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
|
||||||
@patch("commands.pin_locator.PinLocator.get_all_symbol_pins")
|
@patch("commands.pin_locator.PinLocator.get_all_symbol_pins")
|
||||||
def test_start_point_snapped_within_tolerance(self, mock_pins, mock_wire):
|
def test_start_point_snapped_within_tolerance(self, mock_pins: Any, mock_wire: Any) -> None:
|
||||||
"""First waypoint within tolerance of a pin should be snapped to pin coords."""
|
"""First waypoint within tolerance of a pin should be snapped to pin coords."""
|
||||||
# get_all_symbol_pins won't be called because symbol list is empty in fixture.
|
# get_all_symbol_pins won't be called because symbol list is empty in fixture.
|
||||||
# Instead we patch find_nearest_pin indirectly by providing all_pins via the
|
# Instead we patch find_nearest_pin indirectly by providing all_pins via the
|
||||||
@@ -326,7 +327,7 @@ class TestPinSnapping:
|
|||||||
class Reference:
|
class Reference:
|
||||||
value = "R1"
|
value = "R1"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
skip_mod.Schematic = lambda path: type("FakeSch", (), {"symbol": [FakeSymbol()]})()
|
skip_mod.Schematic = lambda path: type("FakeSch", (), {"symbol": [FakeSymbol()]})()
|
||||||
@@ -361,7 +362,7 @@ class TestPinSnapping:
|
|||||||
], f"Start should snap to [10.0, 20.0], got {called_start}"
|
], f"Start should snap to [10.0, 20.0], got {called_start}"
|
||||||
# If it failed due to stub issues, just verify no exception
|
# If it failed due to stub issues, just verify no exception
|
||||||
|
|
||||||
def test_snap_disabled_passes_original_coords(self):
|
def test_snap_disabled_passes_original_coords(self) -> None:
|
||||||
"""With snapToPins=False the handler should not load PinLocator at all."""
|
"""With snapToPins=False the handler should not load PinLocator at all."""
|
||||||
with (
|
with (
|
||||||
patch("commands.wire_manager.WireManager.add_wire", return_value=True) as mw,
|
patch("commands.wire_manager.WireManager.add_wire", return_value=True) as mw,
|
||||||
@@ -380,7 +381,7 @@ class TestPinSnapping:
|
|||||||
assert called_start == [1.0, 2.0]
|
assert called_start == [1.0, 2.0]
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
|
@patch("commands.wire_manager.WireManager.add_wire", return_value=True)
|
||||||
def test_snap_miss_leaves_coords_unchanged(self, mock_wire):
|
def test_snap_miss_leaves_coords_unchanged(self, mock_wire: Any) -> None:
|
||||||
"""Point beyond tolerance should not be snapped."""
|
"""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(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
@@ -397,7 +398,7 @@ class TestPinSnapping:
|
|||||||
assert "snapped" not in result.get("message", "")
|
assert "snapped" not in result.get("message", "")
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_polyline_wire", return_value=True)
|
@patch("commands.wire_manager.WireManager.add_polyline_wire", return_value=True)
|
||||||
def test_intermediate_waypoints_not_snapped(self, mock_poly):
|
def test_intermediate_waypoints_not_snapped(self, mock_poly: Any) -> None:
|
||||||
"""Middle waypoints must remain unchanged even with snapToPins=True."""
|
"""Middle waypoints must remain unchanged even with snapToPins=True."""
|
||||||
mid = [50.0, 50.0]
|
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:
|
||||||
@@ -424,7 +425,7 @@ class TestPinSnapping:
|
|||||||
class TestHandleAddSchematicJunction:
|
class TestHandleAddSchematicJunction:
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self):
|
def setup(self) -> Any:
|
||||||
import types
|
import types
|
||||||
|
|
||||||
for mod in ["pcbnew", "skip"]:
|
for mod in ["pcbnew", "skip"]:
|
||||||
@@ -434,18 +435,18 @@ class TestHandleAddSchematicJunction:
|
|||||||
with patch.object(KiCADInterface, "__init__", lambda self, *a, **kw: None):
|
with patch.object(KiCADInterface, "__init__", lambda self, *a, **kw: None):
|
||||||
self.iface = KiCADInterface.__new__(KiCADInterface)
|
self.iface = KiCADInterface.__new__(KiCADInterface)
|
||||||
|
|
||||||
def test_missing_schematic_path(self):
|
def test_missing_schematic_path(self) -> None:
|
||||||
result = self.iface._handle_add_schematic_junction({"position": [10.0, 20.0]})
|
result = self.iface._handle_add_schematic_junction({"position": [10.0, 20.0]})
|
||||||
assert result["success"] is False
|
assert result["success"] is False
|
||||||
assert "Schematic path" in result["message"]
|
assert "Schematic path" in result["message"]
|
||||||
|
|
||||||
def test_missing_position(self):
|
def test_missing_position(self) -> None:
|
||||||
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 result["success"] is False
|
||||||
assert "Position" in result["message"]
|
assert "Position" in result["message"]
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_junction", return_value=True)
|
@patch("commands.wire_manager.WireManager.add_junction", return_value=True)
|
||||||
def test_success(self, mock_jct):
|
def test_success(self, mock_jct: Any) -> None:
|
||||||
sch = _make_temp_sch()
|
sch = _make_temp_sch()
|
||||||
try:
|
try:
|
||||||
result = self.iface._handle_add_schematic_junction(
|
result = self.iface._handle_add_schematic_junction(
|
||||||
@@ -461,7 +462,7 @@ class TestHandleAddSchematicJunction:
|
|||||||
shutil.rmtree(sch.parent, ignore_errors=True)
|
shutil.rmtree(sch.parent, ignore_errors=True)
|
||||||
|
|
||||||
@patch("commands.wire_manager.WireManager.add_junction", return_value=False)
|
@patch("commands.wire_manager.WireManager.add_junction", return_value=False)
|
||||||
def test_failure(self, _):
|
def test_failure(self, _: Any) -> None:
|
||||||
sch = _make_temp_sch()
|
sch = _make_temp_sch()
|
||||||
try:
|
try:
|
||||||
result = self.iface._handle_add_schematic_junction(
|
result = self.iface._handle_add_schematic_junction(
|
||||||
@@ -483,21 +484,21 @@ class TestHandleAddSchematicJunction:
|
|||||||
|
|
||||||
class TestConnectionManagerOrphanedMethodsRemoved:
|
class TestConnectionManagerOrphanedMethodsRemoved:
|
||||||
|
|
||||||
def test_add_wire_removed(self):
|
def test_add_wire_removed(self) -> None:
|
||||||
from commands.connection_schematic import ConnectionManager
|
from commands.connection_schematic import ConnectionManager
|
||||||
|
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
ConnectionManager, "add_wire"
|
ConnectionManager, "add_wire"
|
||||||
), "ConnectionManager.add_wire should have been removed"
|
), "ConnectionManager.add_wire should have been removed"
|
||||||
|
|
||||||
def test_add_connection_removed(self):
|
def test_add_connection_removed(self) -> None:
|
||||||
from commands.connection_schematic import ConnectionManager
|
from commands.connection_schematic import ConnectionManager
|
||||||
|
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
ConnectionManager, "add_connection"
|
ConnectionManager, "add_connection"
|
||||||
), "ConnectionManager.add_connection should have been removed"
|
), "ConnectionManager.add_connection should have been removed"
|
||||||
|
|
||||||
def test_get_pin_location_removed(self):
|
def test_get_pin_location_removed(self) -> None:
|
||||||
from commands.connection_schematic import ConnectionManager
|
from commands.connection_schematic import ConnectionManager
|
||||||
|
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
@@ -515,12 +516,12 @@ class TestIntegrationWireManager:
|
|||||||
"""Integration tests using real schematic files and WireManager."""
|
"""Integration tests using real schematic files and WireManager."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def sch(self):
|
def sch(self) -> Any:
|
||||||
path = _make_temp_sch()
|
path = _make_temp_sch()
|
||||||
yield path
|
yield path
|
||||||
shutil.rmtree(path.parent, ignore_errors=True)
|
shutil.rmtree(path.parent, ignore_errors=True)
|
||||||
|
|
||||||
def test_add_wire_writes_wire_element(self, sch):
|
def test_add_wire_writes_wire_element(self, sch: Any) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
ok = WireManager.add_wire(sch, [10.0, 10.0], [30.0, 10.0])
|
ok = WireManager.add_wire(sch, [10.0, 10.0], [30.0, 10.0])
|
||||||
@@ -529,7 +530,7 @@ class TestIntegrationWireManager:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 1
|
assert len(wires) == 1
|
||||||
|
|
||||||
def test_add_polyline_wire_creates_segments(self, sch):
|
def test_add_polyline_wire_creates_segments(self, sch: Any) -> None:
|
||||||
"""N waypoints should produce N-1 individual 2-point wire segments."""
|
"""N waypoints should produce N-1 individual 2-point wire segments."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -540,7 +541,7 @@ class TestIntegrationWireManager:
|
|||||||
wires = _find_elements(data, "wire")
|
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):
|
def test_add_junction_writes_junction_element(self, sch: Any) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
ok = WireManager.add_junction(sch, [25.4, 25.4])
|
ok = WireManager.add_junction(sch, [25.4, 25.4])
|
||||||
@@ -553,7 +554,7 @@ class TestIntegrationWireManager:
|
|||||||
assert at[1] == 25.4
|
assert at[1] == 25.4
|
||||||
assert at[2] == 25.4
|
assert at[2] == 25.4
|
||||||
|
|
||||||
def test_wire_endpoint_coordinates_match(self, sch):
|
def test_wire_endpoint_coordinates_match(self, sch: Any) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
WireManager.add_wire(sch, [5.0, 7.5], [15.0, 7.5])
|
WireManager.add_wire(sch, [5.0, 7.5], [15.0, 7.5])
|
||||||
@@ -576,7 +577,7 @@ class TestIntegrationHandlerEndToEnd:
|
|||||||
"""Integration tests for KiCADInterface handlers writing to real schematic files."""
|
"""Integration tests for KiCADInterface handlers writing to real schematic files."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def setup(self):
|
def setup(self) -> Any:
|
||||||
import types
|
import types
|
||||||
|
|
||||||
for mod in ["pcbnew", "skip"]:
|
for mod in ["pcbnew", "skip"]:
|
||||||
@@ -589,7 +590,7 @@ class TestIntegrationHandlerEndToEnd:
|
|||||||
yield
|
yield
|
||||||
shutil.rmtree(self.sch.parent, ignore_errors=True)
|
shutil.rmtree(self.sch.parent, ignore_errors=True)
|
||||||
|
|
||||||
def test_junction_handler_writes_junction(self):
|
def test_junction_handler_writes_junction(self) -> None:
|
||||||
result = self.iface._handle_add_schematic_junction(
|
result = self.iface._handle_add_schematic_junction(
|
||||||
{
|
{
|
||||||
"schematicPath": str(self.sch),
|
"schematicPath": str(self.sch),
|
||||||
@@ -601,7 +602,7 @@ class TestIntegrationHandlerEndToEnd:
|
|||||||
junctions = _find_elements(data, "junction")
|
junctions = _find_elements(data, "junction")
|
||||||
assert len(junctions) == 1
|
assert len(junctions) == 1
|
||||||
|
|
||||||
def test_wire_handler_two_points_writes_wire(self):
|
def test_wire_handler_two_points_writes_wire(self) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
"schematicPath": str(self.sch),
|
"schematicPath": str(self.sch),
|
||||||
@@ -614,7 +615,7 @@ class TestIntegrationHandlerEndToEnd:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 1
|
assert len(wires) == 1
|
||||||
|
|
||||||
def test_wire_handler_four_points_creates_three_segments(self):
|
def test_wire_handler_four_points_creates_three_segments(self) -> None:
|
||||||
result = self.iface._handle_add_schematic_wire(
|
result = self.iface._handle_add_schematic_wire(
|
||||||
{
|
{
|
||||||
"schematicPath": str(self.sch),
|
"schematicPath": str(self.sch),
|
||||||
@@ -638,64 +639,64 @@ class TestPointStrictlyOnWire:
|
|||||||
"""Unit tests for WireManager._point_strictly_on_wire geometry helper."""
|
"""Unit tests for WireManager._point_strictly_on_wire geometry helper."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fn(px, py, x1, y1, x2, y2, eps=1e-6):
|
def _fn(px: Any, py: Any, x1: Any, y1: Any, x2: Any, y2: Any, eps: Any = 1e-6) -> Any:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
return WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2, eps)
|
return WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2, eps)
|
||||||
|
|
||||||
def test_horizontal_midpoint(self):
|
def test_horizontal_midpoint(self) -> None:
|
||||||
assert self._fn(5, 0, 0, 0, 10, 0) is True
|
assert self._fn(5, 0, 0, 0, 10, 0) is True
|
||||||
|
|
||||||
def test_vertical_midpoint(self):
|
def test_vertical_midpoint(self) -> None:
|
||||||
assert self._fn(0, 5, 0, 0, 0, 10) is True
|
assert self._fn(0, 5, 0, 0, 0, 10) is True
|
||||||
|
|
||||||
def test_horizontal_at_start_endpoint(self):
|
def test_horizontal_at_start_endpoint(self) -> None:
|
||||||
"""Point at wire start should NOT be strictly on wire."""
|
"""Point at wire start should NOT be strictly on wire."""
|
||||||
assert self._fn(0, 0, 0, 0, 10, 0) is False
|
assert self._fn(0, 0, 0, 0, 10, 0) is False
|
||||||
|
|
||||||
def test_horizontal_at_end_endpoint(self):
|
def test_horizontal_at_end_endpoint(self) -> None:
|
||||||
"""Point at wire end should NOT be strictly on wire."""
|
"""Point at wire end should NOT be strictly on wire."""
|
||||||
assert self._fn(10, 0, 0, 0, 10, 0) is False
|
assert self._fn(10, 0, 0, 0, 10, 0) is False
|
||||||
|
|
||||||
def test_vertical_at_start_endpoint(self):
|
def test_vertical_at_start_endpoint(self) -> None:
|
||||||
assert self._fn(0, 0, 0, 0, 0, 10) is False
|
assert self._fn(0, 0, 0, 0, 0, 10) is False
|
||||||
|
|
||||||
def test_vertical_at_end_endpoint(self):
|
def test_vertical_at_end_endpoint(self) -> None:
|
||||||
assert self._fn(0, 10, 0, 0, 0, 10) is False
|
assert self._fn(0, 10, 0, 0, 0, 10) is False
|
||||||
|
|
||||||
def test_point_off_horizontal_wire(self):
|
def test_point_off_horizontal_wire(self) -> None:
|
||||||
"""Point above a horizontal wire."""
|
"""Point above a horizontal wire."""
|
||||||
assert self._fn(5, 1, 0, 0, 10, 0) is False
|
assert self._fn(5, 1, 0, 0, 10, 0) is False
|
||||||
|
|
||||||
def test_point_off_vertical_wire(self):
|
def test_point_off_vertical_wire(self) -> None:
|
||||||
"""Point to the right of a vertical wire."""
|
"""Point to the right of a vertical wire."""
|
||||||
assert self._fn(1, 5, 0, 0, 0, 10) is False
|
assert self._fn(1, 5, 0, 0, 0, 10) is False
|
||||||
|
|
||||||
def test_point_beyond_horizontal_wire(self):
|
def test_point_beyond_horizontal_wire(self) -> None:
|
||||||
"""Point collinear but past the end of a horizontal wire."""
|
"""Point collinear but past the end of a horizontal wire."""
|
||||||
assert self._fn(15, 0, 0, 0, 10, 0) is False
|
assert self._fn(15, 0, 0, 0, 10, 0) is False
|
||||||
|
|
||||||
def test_point_beyond_vertical_wire(self):
|
def test_point_beyond_vertical_wire(self) -> None:
|
||||||
"""Point collinear but past the end of a vertical wire."""
|
"""Point collinear but past the end of a vertical wire."""
|
||||||
assert self._fn(0, 15, 0, 0, 0, 10) is False
|
assert self._fn(0, 15, 0, 0, 0, 10) is False
|
||||||
|
|
||||||
def test_diagonal_wire_always_false(self):
|
def test_diagonal_wire_always_false(self) -> None:
|
||||||
"""Only horizontal/vertical wires are handled; diagonal → False."""
|
"""Only horizontal/vertical wires are handled; diagonal → False."""
|
||||||
assert self._fn(5, 5, 0, 0, 10, 10) is False
|
assert self._fn(5, 5, 0, 0, 10, 10) is False
|
||||||
|
|
||||||
def test_reversed_horizontal_endpoints(self):
|
def test_reversed_horizontal_endpoints(self) -> None:
|
||||||
"""Wire endpoints reversed (x2 < x1) should still work."""
|
"""Wire endpoints reversed (x2 < x1) should still work."""
|
||||||
assert self._fn(5, 0, 10, 0, 0, 0) is True
|
assert self._fn(5, 0, 10, 0, 0, 0) is True
|
||||||
|
|
||||||
def test_reversed_vertical_endpoints(self):
|
def test_reversed_vertical_endpoints(self) -> None:
|
||||||
"""Wire endpoints reversed (y2 < y1) should still work."""
|
"""Wire endpoints reversed (y2 < y1) should still work."""
|
||||||
assert self._fn(0, 5, 0, 10, 0, 0) is True
|
assert self._fn(0, 5, 0, 10, 0, 0) is True
|
||||||
|
|
||||||
def test_near_endpoint_within_epsilon(self):
|
def test_near_endpoint_within_epsilon(self) -> None:
|
||||||
"""Point within epsilon of endpoint should NOT be considered strictly on wire."""
|
"""Point within epsilon of endpoint should NOT be considered strictly on wire."""
|
||||||
assert self._fn(1e-7, 0, 0, 0, 10, 0) is False
|
assert self._fn(1e-7, 0, 0, 0, 10, 0) is False
|
||||||
|
|
||||||
def test_zero_length_wire(self):
|
def test_zero_length_wire(self) -> None:
|
||||||
"""Degenerate wire with same start/end — nothing is strictly between."""
|
"""Degenerate wire with same start/end — nothing is strictly between."""
|
||||||
assert self._fn(5, 5, 5, 5, 5, 5) is False
|
assert self._fn(5, 5, 5, 5, 5, 5) is False
|
||||||
|
|
||||||
@@ -710,12 +711,12 @@ class TestParseWire:
|
|||||||
"""Unit tests for WireManager._parse_wire S-expression parser."""
|
"""Unit tests for WireManager._parse_wire S-expression parser."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fn(item):
|
def _fn(item: Any) -> Any:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
return WireManager._parse_wire(item)
|
return WireManager._parse_wire(item)
|
||||||
|
|
||||||
def test_valid_wire(self):
|
def test_valid_wire(self) -> None:
|
||||||
wire = [
|
wire = [
|
||||||
Symbol("wire"),
|
Symbol("wire"),
|
||||||
[Symbol("pts"), [Symbol("xy"), 10.0, 20.0], [Symbol("xy"), 30.0, 20.0]],
|
[Symbol("pts"), [Symbol("xy"), 10.0, 20.0], [Symbol("xy"), 30.0, 20.0]],
|
||||||
@@ -734,28 +735,28 @@ class TestParseWire:
|
|||||||
assert width == 0
|
assert width == 0
|
||||||
assert stype == "default"
|
assert stype == "default"
|
||||||
|
|
||||||
def test_non_wire_element_returns_none(self):
|
def test_non_wire_element_returns_none(self) -> None:
|
||||||
junction = [Symbol("junction"), [Symbol("at"), 10, 20]]
|
junction = [Symbol("junction"), [Symbol("at"), 10, 20]]
|
||||||
assert TestParseWire._fn(junction) is None
|
assert TestParseWire._fn(junction) is None
|
||||||
|
|
||||||
def test_non_list_returns_none(self):
|
def test_non_list_returns_none(self) -> None:
|
||||||
assert TestParseWire._fn("not a list") is None
|
assert TestParseWire._fn("not a list") is None
|
||||||
|
|
||||||
def test_empty_list_returns_none(self):
|
def test_empty_list_returns_none(self) -> None:
|
||||||
assert TestParseWire._fn([]) is None
|
assert TestParseWire._fn([]) is None
|
||||||
|
|
||||||
def test_wire_with_no_pts_returns_none(self):
|
def test_wire_with_no_pts_returns_none(self) -> None:
|
||||||
wire = [Symbol("wire"), [Symbol("stroke"), [Symbol("width"), 0]]]
|
wire = [Symbol("wire"), [Symbol("stroke"), [Symbol("width"), 0]]]
|
||||||
assert TestParseWire._fn(wire) is None
|
assert TestParseWire._fn(wire) is None
|
||||||
|
|
||||||
def test_wire_with_only_one_xy_returns_none(self):
|
def test_wire_with_only_one_xy_returns_none(self) -> None:
|
||||||
wire = [
|
wire = [
|
||||||
Symbol("wire"),
|
Symbol("wire"),
|
||||||
[Symbol("pts"), [Symbol("xy"), 10.0, 20.0]],
|
[Symbol("pts"), [Symbol("xy"), 10.0, 20.0]],
|
||||||
]
|
]
|
||||||
assert TestParseWire._fn(wire) is None
|
assert TestParseWire._fn(wire) is None
|
||||||
|
|
||||||
def test_wire_without_stroke_uses_defaults(self):
|
def test_wire_without_stroke_uses_defaults(self) -> None:
|
||||||
wire = [
|
wire = [
|
||||||
Symbol("wire"),
|
Symbol("wire"),
|
||||||
[Symbol("pts"), [Symbol("xy"), 0, 0], [Symbol("xy"), 10, 0]],
|
[Symbol("pts"), [Symbol("xy"), 0, 0], [Symbol("xy"), 10, 0]],
|
||||||
@@ -776,7 +777,7 @@ class TestParseWire:
|
|||||||
class TestMakeWireSexp:
|
class TestMakeWireSexp:
|
||||||
"""Unit tests for WireManager._make_wire_sexp builder."""
|
"""Unit tests for WireManager._make_wire_sexp builder."""
|
||||||
|
|
||||||
def test_produces_valid_parseable_wire(self):
|
def test_produces_valid_parseable_wire(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
sexp = WireManager._make_wire_sexp([10, 20], [30, 20])
|
sexp = WireManager._make_wire_sexp([10, 20], [30, 20])
|
||||||
@@ -788,7 +789,7 @@ class TestMakeWireSexp:
|
|||||||
assert width == 0
|
assert width == 0
|
||||||
assert stype == "default"
|
assert stype == "default"
|
||||||
|
|
||||||
def test_custom_stroke(self):
|
def test_custom_stroke(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
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")
|
||||||
@@ -798,7 +799,7 @@ class TestMakeWireSexp:
|
|||||||
assert width == 0.5
|
assert width == 0.5
|
||||||
assert stype == "dash"
|
assert stype == "dash"
|
||||||
|
|
||||||
def test_has_uuid(self):
|
def test_has_uuid(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
sexp = WireManager._make_wire_sexp([0, 0], [10, 0])
|
sexp = WireManager._make_wire_sexp([0, 0], [10, 0])
|
||||||
@@ -807,7 +808,7 @@ class TestMakeWireSexp:
|
|||||||
assert uuid_entry[0] == Symbol("uuid")
|
assert uuid_entry[0] == Symbol("uuid")
|
||||||
assert isinstance(uuid_entry[1], str) and len(uuid_entry[1]) > 0
|
assert isinstance(uuid_entry[1], str) and len(uuid_entry[1]) > 0
|
||||||
|
|
||||||
def test_two_calls_produce_different_uuids(self):
|
def test_two_calls_produce_different_uuids(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
sexp1 = WireManager._make_wire_sexp([0, 0], [10, 0])
|
sexp1 = WireManager._make_wire_sexp([0, 0], [10, 0])
|
||||||
@@ -825,7 +826,7 @@ class TestBreakWiresAtPoint:
|
|||||||
"""Unit tests for WireManager._break_wires_at_point T-junction logic."""
|
"""Unit tests for WireManager._break_wires_at_point T-junction logic."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_sch_data_with_wires(wire_coords):
|
def _make_sch_data_with_wires(wire_coords: Any) -> list[Any]:
|
||||||
"""Build a minimal sch_data list with wire elements and a sheet_instances marker."""
|
"""Build a minimal sch_data list with wire elements and a sheet_instances marker."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -835,7 +836,7 @@ class TestBreakWiresAtPoint:
|
|||||||
data.append([Symbol("sheet_instances")])
|
data.append([Symbol("sheet_instances")])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def test_split_horizontal_wire_at_midpoint(self):
|
def test_split_horizontal_wire_at_midpoint(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
data = self._make_sch_data_with_wires([([0, 0], [20, 0])])
|
data = self._make_sch_data_with_wires([([0, 0], [20, 0])])
|
||||||
@@ -851,7 +852,7 @@ class TestBreakWiresAtPoint:
|
|||||||
endpoints = {c for pair in coords for c in pair}
|
endpoints = {c for pair in coords for c in pair}
|
||||||
assert (10.0, 0.0) in endpoints
|
assert (10.0, 0.0) in endpoints
|
||||||
|
|
||||||
def test_split_vertical_wire_at_midpoint(self):
|
def test_split_vertical_wire_at_midpoint(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
data = self._make_sch_data_with_wires([([5, 0], [5, 30])])
|
data = self._make_sch_data_with_wires([([5, 0], [5, 30])])
|
||||||
@@ -860,7 +861,7 @@ class TestBreakWiresAtPoint:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 2
|
assert len(wires) == 2
|
||||||
|
|
||||||
def test_no_split_at_wire_endpoint(self):
|
def test_no_split_at_wire_endpoint(self) -> None:
|
||||||
"""Point at existing endpoint should not trigger a split."""
|
"""Point at existing endpoint should not trigger a split."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -870,7 +871,7 @@ class TestBreakWiresAtPoint:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 1
|
assert len(wires) == 1
|
||||||
|
|
||||||
def test_no_split_point_not_on_wire(self):
|
def test_no_split_point_not_on_wire(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
data = self._make_sch_data_with_wires([([0, 0], [20, 0])])
|
data = self._make_sch_data_with_wires([([0, 0], [20, 0])])
|
||||||
@@ -879,7 +880,7 @@ class TestBreakWiresAtPoint:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 1
|
assert len(wires) == 1
|
||||||
|
|
||||||
def test_split_multiple_wires_at_same_point(self):
|
def test_split_multiple_wires_at_same_point(self) -> None:
|
||||||
"""Two crossing wires at (10, 10) — both should be split."""
|
"""Two crossing wires at (10, 10) — both should be split."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -894,7 +895,7 @@ class TestBreakWiresAtPoint:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 4 # each wire split into 2
|
assert len(wires) == 4 # each wire split into 2
|
||||||
|
|
||||||
def test_split_preserves_stroke_properties(self):
|
def test_split_preserves_stroke_properties(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
data = [Symbol("kicad_sch")]
|
data = [Symbol("kicad_sch")]
|
||||||
@@ -910,7 +911,7 @@ class TestBreakWiresAtPoint:
|
|||||||
assert parsed[2] == 0.5, "stroke_width should be preserved"
|
assert parsed[2] == 0.5, "stroke_width should be preserved"
|
||||||
assert parsed[3] == "dash", "stroke_type should be preserved"
|
assert parsed[3] == "dash", "stroke_type should be preserved"
|
||||||
|
|
||||||
def test_no_split_on_diagonal_wire(self):
|
def test_no_split_on_diagonal_wire(self) -> None:
|
||||||
"""Diagonal wires are not handled by _point_strictly_on_wire → no split."""
|
"""Diagonal wires are not handled by _point_strictly_on_wire → no split."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -918,7 +919,7 @@ class TestBreakWiresAtPoint:
|
|||||||
splits = WireManager._break_wires_at_point(data, [5, 5])
|
splits = WireManager._break_wires_at_point(data, [5, 5])
|
||||||
assert splits == 0
|
assert splits == 0
|
||||||
|
|
||||||
def test_empty_sch_data(self):
|
def test_empty_sch_data(self) -> None:
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
data = [Symbol("kicad_sch"), [Symbol("sheet_instances")]]
|
data = [Symbol("kicad_sch"), [Symbol("sheet_instances")]]
|
||||||
@@ -936,12 +937,12 @@ class TestIntegrationTJunction:
|
|||||||
"""Integration tests for T-junction wire breaking during add_wire/add_junction."""
|
"""Integration tests for T-junction wire breaking during add_wire/add_junction."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def sch(self):
|
def sch(self) -> Any:
|
||||||
path = _make_temp_sch()
|
path = _make_temp_sch()
|
||||||
yield path
|
yield path
|
||||||
shutil.rmtree(path.parent, ignore_errors=True)
|
shutil.rmtree(path.parent, ignore_errors=True)
|
||||||
|
|
||||||
def test_add_wire_breaks_existing_horizontal_wire(self, sch):
|
def test_add_wire_breaks_existing_horizontal_wire(self, sch: Any) -> None:
|
||||||
"""Adding a vertical wire whose endpoint is mid-horizontal-wire should split it."""
|
"""Adding a vertical wire whose endpoint is mid-horizontal-wire should split it."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -954,7 +955,7 @@ class TestIntegrationTJunction:
|
|||||||
# Original horizontal wire should be split into 2, plus the new vertical = 3 total
|
# Original horizontal wire should be split into 2, plus the new vertical = 3 total
|
||||||
assert len(wires) == 3, f"Expected 3 wires (split + new), got {len(wires)}"
|
assert len(wires) == 3, f"Expected 3 wires (split + new), got {len(wires)}"
|
||||||
|
|
||||||
def test_add_wire_does_not_break_at_shared_endpoint(self, sch):
|
def test_add_wire_does_not_break_at_shared_endpoint(self, sch: Any) -> None:
|
||||||
"""Wire connecting at an existing endpoint should not trigger a split."""
|
"""Wire connecting at an existing endpoint should not trigger a split."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -965,7 +966,7 @@ class TestIntegrationTJunction:
|
|||||||
wires = _find_elements(data, "wire")
|
wires = _find_elements(data, "wire")
|
||||||
assert len(wires) == 2, f"Expected 2 wires (no split), got {len(wires)}"
|
assert len(wires) == 2, f"Expected 2 wires (no split), got {len(wires)}"
|
||||||
|
|
||||||
def test_add_junction_breaks_wire(self, sch):
|
def test_add_junction_breaks_wire(self, sch: Any) -> None:
|
||||||
"""Adding a junction mid-wire should split that wire."""
|
"""Adding a junction mid-wire should split that wire."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -977,7 +978,7 @@ class TestIntegrationTJunction:
|
|||||||
junctions = _find_elements(data, "junction")
|
junctions = _find_elements(data, "junction")
|
||||||
assert len(junctions) == 1
|
assert len(junctions) == 1
|
||||||
|
|
||||||
def test_add_junction_at_wire_endpoint_no_split(self, sch):
|
def test_add_junction_at_wire_endpoint_no_split(self, sch: Any) -> None:
|
||||||
"""Junction at wire endpoint should not split it."""
|
"""Junction at wire endpoint should not split it."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -987,7 +988,7 @@ class TestIntegrationTJunction:
|
|||||||
wires = _find_elements(data, "wire")
|
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):
|
def test_polyline_breaks_existing_wire(self, sch: Any) -> None:
|
||||||
"""Polyline whose start/end hits mid-wire should break it."""
|
"""Polyline whose start/end hits mid-wire should break it."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
@@ -999,7 +1000,7 @@ class TestIntegrationTJunction:
|
|||||||
# 2 from split + 2 polyline segments = 4
|
# 2 from split + 2 polyline segments = 4
|
||||||
assert len(wires) == 4, f"Expected 4 wires, got {len(wires)}"
|
assert len(wires) == 4, f"Expected 4 wires, got {len(wires)}"
|
||||||
|
|
||||||
def test_polyline_two_points_same_as_add_wire(self, sch):
|
def test_polyline_two_points_same_as_add_wire(self, sch: Any) -> None:
|
||||||
"""Polyline with exactly 2 points should produce 1 wire segment."""
|
"""Polyline with exactly 2 points should produce 1 wire segment."""
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
|
|||||||
@@ -157,9 +157,9 @@ class KiCADProcessManager:
|
|||||||
timeout=5 if system == "Windows" else None,
|
timeout=5 if system == "Windows" else None,
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
path = result.stdout.strip().split("\n")[0]
|
exe_path = result.stdout.strip().split("\n")[0]
|
||||||
logger.info(f"Found KiCAD executable: {path}")
|
logger.info(f"Found KiCAD executable: {exe_path}")
|
||||||
return Path(path)
|
return Path(exe_path)
|
||||||
|
|
||||||
# Platform-specific default paths
|
# Platform-specific default paths
|
||||||
if system == "Linux":
|
if system == "Linux":
|
||||||
|
|||||||
Reference in New Issue
Block a user