diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 7d1f02a..9970a45 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -524,11 +524,24 @@ class KiCADInterface: # Board-mutating commands that trigger auto-save on SWIG path _BOARD_MUTATING_COMMANDS = { - "place_component", "move_component", "rotate_component", "delete_component", - "route_trace", "route_pad_to_pad", "add_via", "delete_trace", "add_net", - "add_board_outline", "add_mounting_hole", "add_text", "add_board_text", - "add_copper_pour", "refill_zones", "import_svg_logo", - "sync_schematic_to_board", "connect_passthrough", + "place_component", + "move_component", + "rotate_component", + "delete_component", + "route_trace", + "route_pad_to_pad", + "add_via", + "delete_trace", + "add_net", + "add_board_outline", + "add_mounting_hole", + "add_text", + "add_board_text", + "add_copper_pour", + "refill_zones", + "import_svg_logo", + "sync_schematic_to_board", + "connect_passthrough", } def _auto_save_board(self): @@ -833,7 +846,9 @@ class KiCADInterface: new_footprint = params.get("footprint") new_value = params.get("value") new_reference = params.get("newReference") - field_positions = params.get("fieldPositions") # dict: {"Reference": {"x": 1, "y": 2, "angle": 0}} + field_positions = params.get( + "fieldPositions" + ) # dict: {"Reference": {"x": 1, "y": 2, "angle": 0}} if not schematic_path: return {"success": False, "message": "schematicPath is required"} @@ -939,8 +954,10 @@ class KiCADInterface: y = pos.get("y", 0) angle = pos.get("angle", 0) block_text = re.sub( - r'(\(property\s+"' + re.escape(field_name) + r'"\s+"[^"]*"\s+)\(at\s+[\d\.\-]+\s+[\d\.\-]+\s+[\d\.\-]+\s*\)', - rf'\1(at {x} {y} {angle})', + r'(\(property\s+"' + + re.escape(field_name) + + r'"\s+"[^"]*"\s+)\(at\s+[\d\.\-]+\s+[\d\.\-]+\s+[\d\.\-]+\s*\)', + rf"\1(at {x} {y} {angle})", block_text, ) @@ -987,7 +1004,10 @@ class KiCADInterface: sch_file = Path(schematic_path) if not sch_file.exists(): - return {"success": False, "message": f"Schematic not found: {schematic_path}"} + return { + "success": False, + "message": f"Schematic not found: {schematic_path}", + } with open(sch_file, "r", encoding="utf-8") as f: content = f.read() @@ -1007,7 +1027,9 @@ class KiCADInterface: # Skip lib_symbols section lib_sym_pos = content.find("(lib_symbols") - lib_sym_end = find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1 + lib_sym_end = ( + find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1 + ) # Find the placed symbol block for this reference block_start = block_end = None @@ -1025,21 +1047,34 @@ class KiCADInterface: if end < 0: search_start = pos + 1 continue - block_text = content[pos: end + 1] - if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text): + block_text = content[pos : end + 1] + if re.search( + r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', + block_text, + ): block_start, block_end = pos, end break search_start = end + 1 if block_start is None: - return {"success": False, "message": f"Component '{reference}' not found in schematic"} + return { + "success": False, + "message": f"Component '{reference}' not found in schematic", + } - block_text = content[block_start: block_end + 1] + block_text = content[block_start : block_end + 1] # Extract component position: first (at x y angle) in the symbol header line - comp_at = re.search(r'\(symbol\s+\(lib_id\s+"[^"]*"\s*\)\s+\(at\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s*\)', block_text) + comp_at = re.search( + r'\(symbol\s+\(lib_id\s+"[^"]*"\s*\)\s+\(at\s+([\d\.\-]+)\s+([\d\.\-]+)\s+([\d\.\-]+)\s*\)', + block_text, + ) if comp_at: - comp_pos = {"x": float(comp_at.group(1)), "y": float(comp_at.group(2)), "angle": float(comp_at.group(3))} + comp_pos = { + "x": float(comp_at.group(1)), + "y": float(comp_at.group(2)), + "angle": float(comp_at.group(3)), + } else: comp_pos = None @@ -1049,7 +1084,13 @@ class KiCADInterface: ) fields = {} for m in prop_pattern.finditer(block_text): - name, value, x, y, angle = m.group(1), m.group(2), m.group(3), m.group(4), m.group(5) + name, value, x, y, angle = ( + m.group(1), + m.group(2), + m.group(3), + m.group(4), + m.group(5), + ) fields[name] = { "value": value, "x": float(x), @@ -1067,6 +1108,7 @@ class KiCADInterface: except Exception as e: logger.error(f"Error getting schematic component: {e}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} @@ -1457,7 +1499,10 @@ class KiCADInterface: pin_offset = int(params.get("pinOffset", 0)) if not all([schematic_path, source_ref, target_ref]): - return {"success": False, "message": "Missing required parameters: schematicPath, sourceRef, targetRef"} + return { + "success": False, + "message": "Missing required parameters: schematicPath, sourceRef, targetRef", + } result = ConnectionManager.connect_passthrough( Path(schematic_path), source_ref, target_ref, net_prefix, pin_offset @@ -1474,6 +1519,7 @@ class KiCADInterface: except Exception as e: logger.error(f"Error in connect_passthrough: {str(e)}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} @@ -1488,26 +1534,39 @@ class KiCADInterface: reference = params.get("reference") if not all([schematic_path, reference]): - return {"success": False, "message": "Missing required parameters: schematicPath, reference"} + return { + "success": False, + "message": "Missing required parameters: schematicPath, reference", + } locator = PinLocator() all_pins = locator.get_all_symbol_pins(Path(schematic_path), reference) if not all_pins: - return {"success": False, "message": f"No pins found for {reference} — check reference and schematic path"} + return { + "success": False, + "message": f"No pins found for {reference} — check reference and schematic path", + } # Enrich with pin names and angles from the symbol definition - pins_def = locator.get_symbol_pins( - Path(schematic_path), - locator._get_lib_id(Path(schematic_path), reference), - ) if hasattr(locator, "_get_lib_id") else {} + pins_def = ( + locator.get_symbol_pins( + Path(schematic_path), + locator._get_lib_id(Path(schematic_path), reference), + ) + if hasattr(locator, "_get_lib_id") + else {} + ) result = {} for pin_num, coords in all_pins.items(): entry = {"x": coords[0], "y": coords[1]} if pin_num in pins_def: entry["name"] = pins_def[pin_num].get("name", pin_num) - entry["angle"] = locator.get_pin_angle(Path(schematic_path), reference, pin_num) or 0 + entry["angle"] = ( + locator.get_pin_angle(Path(schematic_path), reference, pin_num) + or 0 + ) result[pin_num] = entry return {"success": True, "reference": reference, "pins": result} @@ -1515,6 +1574,7 @@ class KiCADInterface: except Exception as e: logger.error(f"Error getting pin locations: {e}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} @@ -1562,14 +1622,27 @@ class KiCADInterface: "errorDetails": "Install KiCAD 8.0+ or add kicad-cli to PATH.", } - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as tmp: json_output = tmp.name try: - cmd = [kicad_cli, "sch", "erc", "--format", "json", "--output", json_output, schematic_path] + cmd = [ + kicad_cli, + "sch", + "erc", + "--format", + "json", + "--output", + json_output, + schematic_path, + ] logger.info(f"Running ERC command: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=120 + ) if result.returncode != 0: logger.error(f"ERC command failed: {result.stderr}") @@ -1590,13 +1663,18 @@ class KiCADInterface: items = v.get("items", []) loc = {} if items and "pos" in items[0]: - loc = {"x": items[0]["pos"].get("x", 0), "y": items[0]["pos"].get("y", 0)} - violations.append({ - "type": v.get("type", "unknown"), - "severity": vseverity, - "message": v.get("description", ""), - "location": loc, - }) + loc = { + "x": items[0]["pos"].get("x", 0), + "y": items[0]["pos"].get("y", 0), + } + violations.append( + { + "type": v.get("type", "unknown"), + "severity": vseverity, + "message": v.get("description", ""), + "location": loc, + } + ) if vseverity in severity_counts: severity_counts[vseverity] += 1 @@ -1643,10 +1721,12 @@ class KiCADInterface: def _handle_sync_schematic_to_board(self, params): """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. + """ logger.info("Syncing schematic to board") try: from pathlib import Path + schematic_path = params.get("schematicPath") board_path = params.get("boardPath") @@ -1658,7 +1738,10 @@ class KiCADInterface: board = self.board board_path = board.GetFileName() if not board_path else board_path else: - return {"success": False, "message": "No board loaded. Use open_project first or provide boardPath."} + return { + "success": False, + "message": "No board loaded. Use open_project first or provide boardPath.", + } if not board_path: board_path = board.GetFileName() @@ -1675,14 +1758,19 @@ class KiCADInterface: schematic_path = str(sch_files[0]) if not schematic_path or not Path(schematic_path).exists(): - return {"success": False, "message": f"Schematic not found. Provide schematicPath. Tried: {schematic_path}"} + return { + "success": False, + "message": f"Schematic not found. Provide schematicPath. Tried: {schematic_path}", + } # Generate netlist from schematic schematic = SchematicManager.load_schematic(schematic_path) if not schematic: return {"success": False, "message": "Failed to load schematic"} - netlist = ConnectionManager.generate_netlist(schematic, schematic_path=schematic_path) + netlist = ConnectionManager.generate_netlist( + schematic, schematic_path=schematic_path + ) # Build (reference, pad_number) -> net_name map pad_net_map = {} # {(ref, pin_str): net_name} @@ -1733,7 +1821,9 @@ class KiCADInterface: self.board = board self._update_command_handlers() - logger.info(f"sync_schematic_to_board: {len(added_nets)} nets added, {assigned_pads} pads assigned") + logger.info( + f"sync_schematic_to_board: {len(added_nets)} nets added, {assigned_pads} pads assigned" + ) return { "success": True, "message": f"PCB nets synced from schematic: {len(added_nets)} nets added, {assigned_pads} pads assigned", @@ -1746,6 +1836,7 @@ class KiCADInterface: except Exception as e: logger.error(f"Error in sync_schematic_to_board: {e}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} @@ -1765,9 +1856,14 @@ class KiCADInterface: filled = bool(params.get("filled", True)) if not pcb_path or not svg_path: - return {"success": False, "message": "Missing required parameters: pcbPath, svgPath"} + return { + "success": False, + "message": "Missing required parameters: pcbPath, svgPath", + } - result = import_svg_to_pcb(pcb_path, svg_path, x, y, width, layer, stroke_width, filled) + result = import_svg_to_pcb( + pcb_path, svg_path, x, y, width, layer, stroke_width, filled + ) # import_svg_to_pcb writes gr_poly entries directly to the .kicad_pcb file, # bypassing the pcbnew in-memory board object. Any subsequent board.Save() @@ -1780,13 +1876,16 @@ class KiCADInterface: self._update_command_handlers() logger.info("Reloaded board into pcbnew after SVG logo import") except Exception as reload_err: - logger.warning(f"Board reload after SVG import failed (non-fatal): {reload_err}") + logger.warning( + f"Board reload after SVG import failed (non-fatal): {reload_err}" + ) return result except Exception as e: logger.error(f"Error importing SVG logo: {str(e)}") import traceback + logger.error(traceback.format_exc()) return {"success": False, "message": str(e)} @@ -1795,9 +1894,10 @@ class KiCADInterface: import shutil from datetime import datetime from pathlib import Path + try: - step = params.get("step", "") - label = params.get("label", "") + step = params.get("step", "") + label = params.get("label", "") prompt_text = params.get("prompt", "") # Determine project directory from loaded board or explicit path project_dir = None @@ -1808,7 +1908,10 @@ class KiCADInterface: if not project_dir: project_dir = params.get("projectPath") if not project_dir or not os.path.isdir(project_dir): - return {"success": False, "message": "Could not determine project directory for snapshot"} + return { + "success": False, + "message": "Could not determine project directory for snapshot", + } ts = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -1818,16 +1921,21 @@ class KiCADInterface: prompt_file = None if prompt_text: - prompt_filename = f"PROMPT_step{step}_{ts}.md" if step else f"PROMPT_{ts}.md" + prompt_filename = ( + f"PROMPT_step{step}_{ts}.md" if step else f"PROMPT_{ts}.md" + ) prompt_file = logs_dir / prompt_filename prompt_file.write_text(prompt_text, encoding="utf-8") logger.info(f"Prompt saved: {prompt_file}") # Copy current MCP session log into logs/ before snapshotting import platform + system = platform.system() if system == "Windows": - mcp_log_dir = os.path.join(os.environ.get("APPDATA", ""), "Claude", "logs") + mcp_log_dir = os.path.join( + os.environ.get("APPDATA", ""), "Claude", "logs" + ) elif system == "Darwin": mcp_log_dir = os.path.expanduser("~/Library/Logs/Claude") else: @@ -1842,11 +1950,15 @@ class KiCADInterface: if "Initializing server" in line: session_start = i session_lines = all_lines[session_start:] - log_filename = f"mcp_log_step{step}_{ts}.txt" if step else f"mcp_log_{ts}.txt" + log_filename = ( + f"mcp_log_step{step}_{ts}.txt" if step else f"mcp_log_{ts}.txt" + ) mcp_log_dest = logs_dir / log_filename with open(mcp_log_dest, "w", encoding="utf-8") as f: f.writelines(session_lines) - logger.info(f"MCP session log saved: {mcp_log_dest} ({len(session_lines)} lines)") + logger.info( + f"MCP session log saved: {mcp_log_dest} ({len(session_lines)} lines)" + ) base_name = Path(project_dir).name suffix_parts = [p for p in [f"step{step}" if step else "", label, ts] if p] @@ -1855,7 +1967,9 @@ class KiCADInterface: snapshots_base.mkdir(exist_ok=True) snapshot_dir = str(snapshots_base / snapshot_name) - shutil.copytree(project_dir, snapshot_dir, ignore=shutil.ignore_patterns("snapshots")) + shutil.copytree( + project_dir, snapshot_dir, ignore=shutil.ignore_patterns("snapshots") + ) logger.info(f"Project snapshot saved: {snapshot_dir}") return { "success": True, @@ -1928,13 +2042,19 @@ class KiCADInterface: # First save the board so the subprocess can load it fresh board_path = self.board.GetFileName() if not board_path: - return {"success": False, "message": "Board has no file path — save first"} + return { + "success": False, + "message": "Board has no file path — save first", + } self.board.Save(board_path) - zone_count = self.board.GetAreaCount() if hasattr(self.board, "GetAreaCount") else 0 + zone_count = ( + self.board.GetAreaCount() if hasattr(self.board, "GetAreaCount") else 0 + ) # Run pcbnew zone fill in an isolated subprocess to prevent crashes import subprocess, sys, textwrap + script = textwrap.dedent(f""" import pcbnew, sys board = pcbnew.LoadBoard({repr(board_path)}) @@ -1946,7 +2066,9 @@ print("ok") try: result = subprocess.run( [sys.executable, "-c", script], - capture_output=True, text=True, timeout=60 + capture_output=True, + text=True, + timeout=60, ) if result.returncode == 0 and "ok" in result.stdout: # Reload board after subprocess modified it @@ -1959,12 +2081,18 @@ print("ok") "zoneCount": zone_count, } else: - logger.warning(f"Zone fill subprocess failed: rc={result.returncode} stderr={result.stderr[:200]}") + logger.warning( + f"Zone fill subprocess failed: rc={result.returncode} stderr={result.stderr[:200]}" + ) return { "success": False, "message": "Zone fill failed in subprocess — zones are defined and will fill when opened in KiCAD (press B). Continuing is safe.", "zoneCount": zone_count, - "details": result.stderr[:300] if result.stderr else result.stdout[:300], + "details": ( + result.stderr[:300] + if result.stderr + else result.stdout[:300] + ), } except subprocess.TimeoutExpired: logger.warning("Zone fill subprocess timed out after 60s") diff --git a/python/tests/test_schematic_component_fields.py b/python/tests/test_schematic_component_fields.py index 0a3da02..2173b10 100644 --- a/python/tests/test_schematic_component_fields.py +++ b/python/tests/test_schematic_component_fields.py @@ -1,6 +1,7 @@ """ Tests for get_schematic_component and edit_schematic_component fieldPositions support. """ + import re import sys import shutil @@ -21,7 +22,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "python")) TEMPLATE_SCH = Path(__file__).parent.parent / "templates" / "empty.kicad_sch" # Minimal placed-symbol block we can embed into a schematic for testing -PLACED_RESISTOR_BLOCK = '''\ +PLACED_RESISTOR_BLOCK = """\ (symbol (lib_id "Device:R") (at 50 50 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") @@ -38,7 +39,7 @@ PLACED_RESISTOR_BLOCK = '''\ (effects (font (size 1.27 1.27)) hide) ) ) -''' +""" def _make_test_schematic(tmp_dir: Path, extra_block: str = "") -> Path: @@ -58,6 +59,7 @@ def _make_test_schematic(tmp_dir: Path, extra_block: str = "") -> Path: # Unit tests – regex / parsing logic only (no file I/O, no KiCAD imports) # --------------------------------------------------------------------------- + @pytest.mark.unit class TestGetSchematicComponentParsing: """Unit tests for the regex logic used by _handle_get_schematic_component.""" @@ -69,8 +71,19 @@ class TestGetSchematicComponentParsing: ) fields = {} for m in prop_pattern.finditer(block_text): - name, value, x, y, angle = m.group(1), m.group(2), m.group(3), m.group(4), m.group(5) - fields[name] = {"value": value, "x": float(x), "y": float(y), "angle": float(angle)} + name, value, x, y, angle = ( + m.group(1), + m.group(2), + m.group(3), + m.group(4), + m.group(5), + ) + fields[name] = { + "value": value, + "x": float(x), + "y": float(y), + "angle": float(angle), + } return fields def _parse_comp_pos(self, block_text: str): @@ -80,7 +93,11 @@ class TestGetSchematicComponentParsing: block_text, ) if m: - return {"x": float(m.group(1)), "y": float(m.group(2)), "angle": float(m.group(3))} + return { + "x": float(m.group(1)), + "y": float(m.group(2)), + "angle": float(m.group(3)), + } return None def test_parses_reference_field(self): @@ -115,8 +132,10 @@ class TestGetSchematicComponentParsing: new_x, new_y, new_angle = 99.0, 88.0, 0 block = PLACED_RESISTOR_BLOCK block = re.sub( - r'(\(property\s+"' + re.escape(field_name) + r'"\s+"[^"]*"\s+)\(at\s+[\d\.\-]+\s+[\d\.\-]+\s+[\d\.\-]+\s*\)', - rf'\1(at {new_x} {new_y} {new_angle})', + r'(\(property\s+"' + + re.escape(field_name) + + r'"\s+"[^"]*"\s+)\(at\s+[\d\.\-]+\s+[\d\.\-]+\s+[\d\.\-]+\s*\)', + rf"\1(at {new_x} {new_y} {new_angle})", block, ) fields = self._parse_fields(block) @@ -130,7 +149,7 @@ class TestGetSchematicComponentParsing: block = PLACED_RESISTOR_BLOCK block = re.sub( r'(\(property\s+"Value"\s+"[^"]*"\s+)\(at\s+[\d\.\-]+\s+[\d\.\-]+\s+[\d\.\-]+\s*\)', - r'\1(at 0.0 0.0 0)', + r"\1(at 0.0 0.0 0)", block, ) fields = self._parse_fields(block) @@ -141,6 +160,7 @@ class TestGetSchematicComponentParsing: # Integration tests – real file I/O using the empty.kicad_sch template # --------------------------------------------------------------------------- + @pytest.mark.integration class TestGetSchematicComponentIntegration: """Integration tests: write a real .kicad_sch and call the handler.""" @@ -152,40 +172,53 @@ class TestGetSchematicComponentIntegration: def _get_interface(self): """Lazily import KiCADInterface to avoid pcbnew import at collection time.""" from kicad_interface import KiCADInterface + return KiCADInterface() def test_get_returns_success(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) assert result["success"] is True def test_get_returns_correct_reference(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) assert result["reference"] == "R1" def test_get_returns_component_position(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) assert result["position"] is not None assert result["position"]["x"] == pytest.approx(50.0) assert result["position"]["y"] == pytest.approx(50.0) def test_get_returns_reference_field_position(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) ref_field = result["fields"]["Reference"] assert ref_field["value"] == "R1" assert ref_field["x"] == pytest.approx(51.27) @@ -193,10 +226,13 @@ class TestGetSchematicComponentIntegration: def test_get_returns_value_field(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) val_field = result["fields"]["Value"] assert val_field["value"] == "10k" assert val_field["x"] == pytest.approx(51.27) @@ -204,18 +240,24 @@ class TestGetSchematicComponentIntegration: def test_get_unknown_reference_returns_failure(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R99", - }) + result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R99", + }, + ) assert result["success"] is False assert "R99" in result["message"] def test_get_missing_path_returns_failure(self): iface = self._get_interface() - result = iface.handle_command("get_schematic_component", { - "reference": "R1", - }) + result = iface.handle_command( + "get_schematic_component", + { + "reference": "R1", + }, + ) assert result["success"] is False @@ -229,74 +271,99 @@ class TestEditSchematicComponentFieldPositions: def _get_interface(self): from kicad_interface import KiCADInterface + return KiCADInterface() def test_reposition_reference_label(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("edit_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - "fieldPositions": {"Reference": {"x": 99.0, "y": 88.0, "angle": 0}}, - }) + result = iface.handle_command( + "edit_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + "fieldPositions": {"Reference": {"x": 99.0, "y": 88.0, "angle": 0}}, + }, + ) assert result["success"] is True # Verify the position was actually written - get_result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + get_result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) assert get_result["fields"]["Reference"]["x"] == pytest.approx(99.0) assert get_result["fields"]["Reference"]["y"] == pytest.approx(88.0) def test_reposition_does_not_change_value(self, sch_with_r1): iface = self._get_interface() - iface.handle_command("edit_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - "fieldPositions": {"Reference": {"x": 99.0, "y": 88.0}}, - }) - get_result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + iface.handle_command( + "edit_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + "fieldPositions": {"Reference": {"x": 99.0, "y": 88.0}}, + }, + ) + get_result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) # Value field position must be unchanged assert get_result["fields"]["Value"]["x"] == pytest.approx(51.27) assert get_result["fields"]["Value"]["y"] == pytest.approx(52.54) def test_reposition_multiple_fields(self, sch_with_r1): iface = self._get_interface() - result = iface.handle_command("edit_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - "fieldPositions": { - "Reference": {"x": 10.0, "y": 20.0, "angle": 0}, - "Value": {"x": 10.0, "y": 30.0, "angle": 0}, + result = iface.handle_command( + "edit_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + "fieldPositions": { + "Reference": {"x": 10.0, "y": 20.0, "angle": 0}, + "Value": {"x": 10.0, "y": 30.0, "angle": 0}, + }, }, - }) + ) assert result["success"] is True - get_result = iface.handle_command("get_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + get_result = iface.handle_command( + "get_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) assert get_result["fields"]["Reference"]["x"] == pytest.approx(10.0) assert get_result["fields"]["Value"]["y"] == pytest.approx(30.0) def test_fieldpositions_alone_is_valid(self, sch_with_r1): """fieldPositions without value/footprint/newReference should succeed.""" iface = self._get_interface() - result = iface.handle_command("edit_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - "fieldPositions": {"Value": {"x": 55.0, "y": 60.0}}, - }) + result = iface.handle_command( + "edit_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + "fieldPositions": {"Value": {"x": 55.0, "y": 60.0}}, + }, + ) assert result["success"] is True def test_no_params_still_fails(self, sch_with_r1): """Providing no update params should return an error.""" iface = self._get_interface() - result = iface.handle_command("edit_schematic_component", { - "schematicPath": str(sch_with_r1), - "reference": "R1", - }) + result = iface.handle_command( + "edit_schematic_component", + { + "schematicPath": str(sch_with_r1), + "reference": "R1", + }, + ) assert result["success"] is False