diff --git a/python/commands/dynamic_symbol_loader.py b/python/commands/dynamic_symbol_loader.py index 14eb344..c0d8ae5 100644 --- a/python/commands/dynamic_symbol_loader.py +++ b/python/commands/dynamic_symbol_loader.py @@ -38,6 +38,7 @@ class DynamicSymbolLoader: possible_paths = [ Path("/usr/share/kicad/symbols"), Path("/usr/local/share/kicad/symbols"), + Path("C:/Program Files/KiCad/10.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"), Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"), @@ -477,16 +478,18 @@ class DynamicSymbolLoader: # Iterate over every top-level (property ...) block in the symbol search_pos = 0 while True: - m = re.search(r'\(property\s+"([^"]+)"\s+"[^"]*"\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)', - sym_block[search_pos:]) + m = re.search( + r'\(property\s+"([^"]+)"\s+"[^"]*"\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)', + sym_block[search_pos:], + ) if not m: break abs_start = search_pos + m.start() prop_block = self._extract_paren_block(sym_block, abs_start) - name = m.group(1) - dx = float(m.group(2)) - dy = float(m.group(3)) + name = m.group(1) + dx = float(m.group(2)) + dy = float(m.group(3)) angle = float(m.group(4)) # Extract (effects ...) block from within this property @@ -495,7 +498,7 @@ class DynamicSymbolLoader: effects_str = self._extract_paren_block(prop_block, eff_pos) # Strip (hide ...) sub-expressions — visibility will be set # separately by the caller - effects_str = re.sub(r'\s*\(hide\s+[^)]+\)', '', effects_str) + effects_str = re.sub(r"\s*\(hide\s+[^)]+\)", "", effects_str) effects_str = effects_str.strip() else: effects_str = "(effects (font (size 1.27 1.27)))" @@ -556,14 +559,13 @@ class DynamicSymbolLoader: new_uuid = str(uuid.uuid4()) # --- read property offsets from the already-injected lib_symbols block ----- - lib_props = self._extract_lib_property_positions( - schematic_path, library_name, symbol_name - ) + lib_props = self._extract_lib_property_positions(schematic_path, library_name, symbol_name) _DEFAULT_EFFECTS = "(effects (font (size 1.27 1.27)))" - def _prop_at(name: str, fallback_dx: float, fallback_dy: float, - fallback_angle: float = 0) -> tuple: + def _prop_at( + name: str, fallback_dx: float, fallback_dy: float, fallback_angle: float = 0 + ) -> tuple: """Return (abs_x, abs_y, text_angle, effects_str) for a property.""" if name in lib_props: dx, dy, text_ang, eff = lib_props[name] @@ -572,10 +574,10 @@ class DynamicSymbolLoader: rdx, rdy = self._rotate_offset(dx, dy, angle) return round(x + rdx, 3), round(y + rdy, 3), text_ang, eff - ref_x, ref_y, ref_a, ref_eff = _prop_at("Reference", 2.032, 0, 0) - val_x, val_y, val_a, val_eff = _prop_at("Value", 0, 2.54, 0) - fp_x, fp_y, _, _ = _prop_at("Footprint", 0, 0, 0) - ds_x, ds_y, _, _ = _prop_at("Datasheet", 0, 0, 0) + ref_x, ref_y, ref_a, ref_eff = _prop_at("Reference", 2.032, 0, 0) + val_x, val_y, val_a, val_eff = _prop_at("Value", 0, 2.54, 0) + fp_x, fp_y, _, _ = _prop_at("Footprint", 0, 0, 0) + ds_x, ds_y, _, _ = _prop_at("Datasheet", 0, 0, 0) mirror_str = " (mirror y)" if mirror_y else "" instance_block = f""" (symbol (lib_id "{full_lib_id}") (at {x} {y} {angle}){mirror_str} (unit {unit}) diff --git a/python/commands/library.py b/python/commands/library.py index dd2ee51..f29a3a3 100644 --- a/python/commands/library.py +++ b/python/commands/library.py @@ -182,12 +182,15 @@ class LibraryManager: possible_paths = [ "/usr/share/kicad/footprints", "/usr/local/share/kicad/footprints", + "C:/Program Files/KiCad/10.0/share/kicad/footprints", "C:/Program Files/KiCad/9.0/share/kicad/footprints", "C:/Program Files/KiCad/8.0/share/kicad/footprints", "/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints", ] # Also check environment variable + if "KICAD10_FOOTPRINT_DIR" in os.environ: + possible_paths.insert(0, os.environ["KICAD10_FOOTPRINT_DIR"]) if "KICAD9_FOOTPRINT_DIR" in os.environ: possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"]) if "KICAD8_FOOTPRINT_DIR" in os.environ: diff --git a/python/commands/library_symbol.py b/python/commands/library_symbol.py index 44acc05..f833b55 100644 --- a/python/commands/library_symbol.py +++ b/python/commands/library_symbol.py @@ -83,9 +83,7 @@ class SymbolLibraryManager: logger.debug("Skipping unparseable library: %s", nickname) logger.info("Symbol cache warm-up complete (%d libraries)", len(self.libraries)) - threading.Thread( - target=_warm, name="symbol-cache-warmup", daemon=True - ).start() + threading.Thread(target=_warm, name="symbol-cache-warmup", daemon=True).start() def _load_libraries(self) -> None: """Load libraries from sym-lib-table files""" @@ -232,12 +230,15 @@ class SymbolLibraryManager: possible_paths = [ "/usr/share/kicad/symbols", "/usr/local/share/kicad/symbols", + "C:/Program Files/KiCad/10.0/share/kicad/symbols", "C:/Program Files/KiCad/9.0/share/kicad/symbols", "C:/Program Files/KiCad/8.0/share/kicad/symbols", "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols", ] # Check environment variable + if "KICAD10_SYMBOL_DIR" in os.environ: + possible_paths.insert(0, os.environ["KICAD10_SYMBOL_DIR"]) if "KICAD9_SYMBOL_DIR" in os.environ: possible_paths.insert(0, os.environ["KICAD9_SYMBOL_DIR"]) if "KICAD8_SYMBOL_DIR" in os.environ: @@ -610,8 +611,10 @@ class SymbolLibraryCommands: # Patch (SER2RJ45): keep cache when project_path matches AND libraries were # actually loaded. Original early-return skipped rebuild even when the cache # was empty (e.g. first call after create_project, before sym-lib-table existed). - if (self.library_manager.project_path == project_path - and len(self.library_manager.libraries) > 0): + if ( + self.library_manager.project_path == project_path + and len(self.library_manager.libraries) > 0 + ): return logger.info(f"Rebuilding SymbolLibraryManager for project: {project_path}") self.library_manager = SymbolLibraryManager(project_path=project_path) diff --git a/tests/test_kicad10_paths.py b/tests/test_kicad10_paths.py new file mode 100644 index 0000000..8eda02a --- /dev/null +++ b/tests/test_kicad10_paths.py @@ -0,0 +1,46 @@ +"""Regression tests for issue #245: KiCad 10 Windows install paths must be in +the auto-discovery candidate lists (footprints and symbols), with env-var +overrides for KiCad 10. +""" + +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +sys.path.insert(0, str(Path(__file__).parent.parent / "python")) + +from commands.library import LibraryManager # noqa: E402 +from commands.library_symbol import SymbolLibraryManager # noqa: E402 + +K10_FOOTPRINTS = "C:/Program Files/KiCad/10.0/share/kicad/footprints" +K10_SYMBOLS = "C:/Program Files/KiCad/10.0/share/kicad/symbols" + + +@pytest.mark.unit +class TestKicad10FootprintDir: + def test_k10_windows_path_is_discovered(self): + lm = LibraryManager.__new__(LibraryManager) + with patch("commands.library.os.path.isdir", side_effect=lambda p: p == K10_FOOTPRINTS): + assert lm._find_kicad_footprint_dir() == K10_FOOTPRINTS + + def test_k10_env_override_wins(self, monkeypatch): + lm = LibraryManager.__new__(LibraryManager) + monkeypatch.setenv("KICAD10_FOOTPRINT_DIR", "/custom/k10/footprints") + with patch("commands.library.os.path.isdir", return_value=True): + assert lm._find_kicad_footprint_dir() == "/custom/k10/footprints" + + +@pytest.mark.unit +class TestKicad10SymbolDir: + def test_k10_windows_path_is_discovered(self): + sm = SymbolLibraryManager.__new__(SymbolLibraryManager) + with patch("commands.library_symbol.os.path.isdir", side_effect=lambda p: p == K10_SYMBOLS): + assert sm._find_kicad_symbol_dir() == K10_SYMBOLS + + def test_k10_env_override_wins(self, monkeypatch): + sm = SymbolLibraryManager.__new__(SymbolLibraryManager) + monkeypatch.setenv("KICAD10_SYMBOL_DIR", "/custom/k10/symbols") + with patch("commands.library_symbol.os.path.isdir", return_value=True): + assert sm._find_kicad_symbol_dir() == "/custom/k10/symbols"