kicad-sch-api 0.3.5__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +2 -2
  2. kicad_sch_api/collections/base.py +5 -7
  3. kicad_sch_api/collections/components.py +24 -12
  4. kicad_sch_api/collections/junctions.py +31 -43
  5. kicad_sch_api/collections/labels.py +19 -27
  6. kicad_sch_api/collections/wires.py +17 -18
  7. kicad_sch_api/core/components.py +5 -0
  8. kicad_sch_api/core/formatter.py +3 -1
  9. kicad_sch_api/core/labels.py +2 -2
  10. kicad_sch_api/core/managers/__init__.py +26 -0
  11. kicad_sch_api/core/managers/file_io.py +243 -0
  12. kicad_sch_api/core/managers/format_sync.py +501 -0
  13. kicad_sch_api/core/managers/graphics.py +579 -0
  14. kicad_sch_api/core/managers/metadata.py +268 -0
  15. kicad_sch_api/core/managers/sheet.py +454 -0
  16. kicad_sch_api/core/managers/text_elements.py +536 -0
  17. kicad_sch_api/core/managers/validation.py +474 -0
  18. kicad_sch_api/core/managers/wire.py +346 -0
  19. kicad_sch_api/core/nets.py +1 -1
  20. kicad_sch_api/core/no_connects.py +5 -3
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +779 -1083
  23. kicad_sch_api/core/texts.py +1 -1
  24. kicad_sch_api/core/types.py +1 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +40 -21
  27. kicad_sch_api/interfaces/__init__.py +1 -1
  28. kicad_sch_api/interfaces/parser.py +1 -1
  29. kicad_sch_api/interfaces/repository.py +1 -1
  30. kicad_sch_api/interfaces/resolver.py +1 -1
  31. kicad_sch_api/parsers/__init__.py +2 -2
  32. kicad_sch_api/parsers/base.py +7 -10
  33. kicad_sch_api/parsers/label_parser.py +7 -7
  34. kicad_sch_api/parsers/registry.py +4 -2
  35. kicad_sch_api/parsers/symbol_parser.py +5 -10
  36. kicad_sch_api/parsers/wire_parser.py +2 -2
  37. kicad_sch_api/symbols/__init__.py +1 -1
  38. kicad_sch_api/symbols/cache.py +9 -12
  39. kicad_sch_api/symbols/resolver.py +20 -26
  40. kicad_sch_api/symbols/validators.py +188 -137
  41. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
  42. kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
  43. kicad_sch_api-0.3.5.dist-info/RECORD +0 -58
  44. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.5.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -340,4 +340,4 @@ class TextCollection:
340
340
 
341
341
  def __bool__(self) -> bool:
342
342
  """Return True if collection has texts."""
343
- return len(self._texts) > 0
343
+ return len(self._texts) > 0
@@ -366,10 +366,7 @@ class SchematicRectangle:
366
366
  @property
367
367
  def center(self) -> Point:
368
368
  """Rectangle center point."""
369
- return Point(
370
- (self.start.x + self.end.x) / 2,
371
- (self.start.y + self.end.y) / 2
372
- )
369
+ return Point((self.start.x + self.end.x) / 2, (self.start.y + self.end.y) / 2)
373
370
 
374
371
 
375
372
  @dataclass
@@ -17,4 +17,6 @@ DEFAULT_PIN_NUMBER_SIZE = 1.27 # 50 mils
17
17
  # Text width ratio for proportional font rendering
18
18
  # KiCad uses proportional fonts where average character width is ~0.65x height
19
19
  # This prevents label text from extending beyond calculated bounding boxes
20
- DEFAULT_PIN_TEXT_WIDTH_RATIO = 0.65 # Width to height ratio for pin text (proportional font average)
20
+ DEFAULT_PIN_TEXT_WIDTH_RATIO = (
21
+ 0.65 # Width to height ratio for pin text (proportional font average)
22
+ )
@@ -120,7 +120,9 @@ class SymbolBoundingBoxCalculator:
120
120
  if min_x == float("inf") or max_x == float("-inf"):
121
121
  raise ValueError(f"No valid geometry found in symbol data")
122
122
 
123
- logger.debug(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
123
+ logger.debug(
124
+ f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
125
+ )
124
126
  logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
125
127
 
126
128
  # Add small margin for text that might extend beyond shapes
@@ -252,7 +254,9 @@ class SymbolBoundingBoxCalculator:
252
254
  if min_x == float("inf") or max_x == float("-inf"):
253
255
  raise ValueError(f"No valid geometry found in symbol data")
254
256
 
255
- logger.debug(f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
257
+ logger.debug(
258
+ f"After geometry processing: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
259
+ )
256
260
  logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
257
261
 
258
262
  # Add small margin for visual spacing
@@ -265,12 +269,16 @@ class SymbolBoundingBoxCalculator:
265
269
  # Add minimal space for component properties (Reference above, Value below)
266
270
  # Use adaptive spacing based on component height for better visual hierarchy
267
271
  component_height = max_y - min_y
268
- property_spacing = max(3.0, component_height * 0.15) # Adaptive: minimum 3mm or 15% of height
272
+ property_spacing = max(
273
+ 3.0, component_height * 0.15
274
+ ) # Adaptive: minimum 3mm or 15% of height
269
275
  property_height = 1.27 # Reduced from 2.54mm
270
276
  min_y -= property_spacing + property_height # Reference above
271
277
  max_y += property_spacing + property_height # Value below
272
278
 
273
- logger.debug(f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})")
279
+ logger.debug(
280
+ f"FINAL PLACEMENT BBOX: ({min_x:.2f}, {min_y:.2f}) to ({max_x:.2f}, {max_y:.2f})"
281
+ )
274
282
  logger.debug(f" Width: {max_x - min_x:.2f}, Height: {max_y - min_y:.2f}")
275
283
  logger.debug("=" * 50)
276
284
 
@@ -359,7 +367,10 @@ class SymbolBoundingBoxCalculator:
359
367
 
360
368
  @classmethod
361
369
  def get_symbol_dimensions(
362
- cls, symbol_data: Dict[str, Any], include_properties: bool = True, pin_net_map: Optional[Dict[str, str]] = None
370
+ cls,
371
+ symbol_data: Dict[str, Any],
372
+ include_properties: bool = True,
373
+ pin_net_map: Optional[Dict[str, str]] = None,
363
374
  ) -> Tuple[float, float]:
364
375
  """
365
376
  Get the width and height of a symbol.
@@ -488,24 +499,28 @@ class SymbolBoundingBoxCalculator:
488
499
  # If no net name match, use minimal fallback to avoid oversized bounding boxes
489
500
  if pin_net_map and pin_number in pin_net_map:
490
501
  label_text = pin_net_map[pin_number]
491
- logger.debug(f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}")
502
+ logger.debug(
503
+ f" PIN {pin_number}: ✅ USING NET '{label_text}' (len={len(label_text)}), at=({x:.2f}, {y:.2f}), angle={angle}"
504
+ )
492
505
  else:
493
506
  # No net match - use minimal size (3 chars) instead of potentially long pin name
494
507
  label_text = "XXX" # 3-character placeholder for unmatched pins
495
- logger.debug(f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})")
508
+ logger.debug(
509
+ f" PIN {pin_number}: ⚠️ NO MATCH, using minimal fallback (pin name was '{pin_name}'), at=({x:.2f}, {y:.2f})"
510
+ )
496
511
 
497
512
  if label_text and label_text != "~": # ~ means no name
498
513
  # Calculate text dimensions
499
514
  # For horizontal text: width = char_count * char_width
500
515
  name_width = (
501
- len(label_text)
502
- * cls.DEFAULT_TEXT_HEIGHT
503
- * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
516
+ len(label_text) * cls.DEFAULT_TEXT_HEIGHT * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
504
517
  )
505
518
  # For vertical text: height = char_count * char_height (characters stack vertically)
506
519
  name_height = len(label_text) * cls.DEFAULT_TEXT_HEIGHT
507
520
 
508
- logger.debug(f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})")
521
+ logger.debug(
522
+ f" label_width={name_width:.2f}, label_height={name_height:.2f} (len={len(label_text)})"
523
+ )
509
524
 
510
525
  # Adjust bounds based on pin orientation
511
526
  # Labels are placed at PIN ENDPOINT with offset, extending AWAY from the component
@@ -515,32 +530,36 @@ class SymbolBoundingBoxCalculator:
515
530
 
516
531
  if angle == 0: # Pin points right - label extends LEFT from endpoint
517
532
  label_x = end_x - offset - name_width
518
- logger.debug(f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})")
533
+ logger.debug(
534
+ f" Angle 0 (Right pin): min_x {min_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
535
+ )
519
536
  min_x = min(min_x, label_x)
520
537
  elif angle == 180: # Pin points left - label extends RIGHT from endpoint
521
538
  label_x = end_x + offset + name_width
522
- logger.debug(f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})")
539
+ logger.debug(
540
+ f" Angle 180 (Left pin): max_x {max_x:.2f} -> {label_x:.2f} (offset={offset:.3f})"
541
+ )
523
542
  max_x = max(max_x, label_x)
524
543
  elif angle == 90: # Pin points up - label extends DOWN from endpoint
525
544
  label_y = end_y - offset - name_height
526
- logger.debug(f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})")
545
+ logger.debug(
546
+ f" Angle 90 (Up pin): min_y {min_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
547
+ )
527
548
  min_y = min(min_y, label_y)
528
549
  elif angle == 270: # Pin points down - label extends UP from endpoint
529
550
  label_y = end_y + offset + name_height
530
- logger.debug(f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})")
551
+ logger.debug(
552
+ f" Angle 270 (Down pin): max_y {max_y:.2f} -> {label_y:.2f} (offset={offset:.3f})"
553
+ )
531
554
  max_y = max(max_y, label_y)
532
555
 
533
556
  # Pin numbers are typically placed near the component body
534
557
  if pin_number:
535
558
  num_width = (
536
- len(pin_number)
537
- * cls.DEFAULT_PIN_NUMBER_SIZE
538
- * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
559
+ len(pin_number) * cls.DEFAULT_PIN_NUMBER_SIZE * cls.DEFAULT_PIN_TEXT_WIDTH_RATIO
539
560
  )
540
561
  # Add some space for the pin number
541
- margin = (
542
- cls.DEFAULT_PIN_NUMBER_SIZE * 1.5
543
- ) # Increase margin for better spacing
562
+ margin = cls.DEFAULT_PIN_NUMBER_SIZE * 1.5 # Increase margin for better spacing
544
563
  min_x -= margin
545
564
  min_y -= margin
546
565
  max_x += margin
@@ -14,4 +14,4 @@ __all__ = [
14
14
  "ISchematicParser",
15
15
  "ISchematicRepository",
16
16
  "ISymbolResolver",
17
- ]
17
+ ]
@@ -73,4 +73,4 @@ class ISchematicParser(Protocol):
73
73
  Raises:
74
74
  ParseError: If content format is invalid
75
75
  """
76
- ...
76
+ ...
@@ -67,4 +67,4 @@ class ISchematicRepository(Protocol):
67
67
  Raises:
68
68
  FileNotFoundError: If file doesn't exist
69
69
  """
70
- ...
70
+ ...
@@ -114,4 +114,4 @@ class ISymbolCache(Protocol):
114
114
  Returns:
115
115
  Dictionary with cache statistics (hits, misses, size, etc.)
116
116
  """
117
- ...
117
+ ...
@@ -5,10 +5,10 @@ This package provides specialized parsers for different types of KiCAD
5
5
  S-expression elements, organized by responsibility and testable in isolation.
6
6
  """
7
7
 
8
- from .registry import ElementParserRegistry
9
8
  from .base import BaseElementParser
9
+ from .registry import ElementParserRegistry
10
10
 
11
11
  __all__ = [
12
12
  "ElementParserRegistry",
13
13
  "BaseElementParser",
14
- ]
14
+ ]
@@ -92,10 +92,7 @@ class BaseElementParser(IElementParser):
92
92
  return None
93
93
 
94
94
  try:
95
- result = {
96
- "x": float(element[1]),
97
- "y": float(element[2])
98
- }
95
+ result = {"x": float(element[1]), "y": float(element[2])}
99
96
 
100
97
  # Optional angle parameter
101
98
  if len(element) > 3:
@@ -105,7 +102,9 @@ class BaseElementParser(IElementParser):
105
102
  except (ValueError, IndexError):
106
103
  return None
107
104
 
108
- def _extract_property_list(self, elements: List[Any], property_name: str) -> List[Dict[str, Any]]:
105
+ def _extract_property_list(
106
+ self, elements: List[Any], property_name: str
107
+ ) -> List[Dict[str, Any]]:
109
108
  """
110
109
  Extract all instances of a property from a list of elements.
111
110
 
@@ -118,9 +117,7 @@ class BaseElementParser(IElementParser):
118
117
  """
119
118
  properties = []
120
119
  for element in elements:
121
- if (isinstance(element, list) and
122
- len(element) > 0 and
123
- element[0] == property_name):
120
+ if isinstance(element, list) and len(element) > 0 and element[0] == property_name:
124
121
  prop = self._parse_property_element(element)
125
122
  if prop:
126
123
  properties.append(prop)
@@ -144,5 +141,5 @@ class BaseElementParser(IElementParser):
144
141
  return {
145
142
  "type": element[0],
146
143
  "value": element[1] if len(element) > 1 else None,
147
- "raw": element
148
- }
144
+ "raw": element,
145
+ }
@@ -46,9 +46,9 @@ class LabelParser(BaseElementParser):
46
46
  "bold": False,
47
47
  "italic": False,
48
48
  "hide": False,
49
- "justify": []
49
+ "justify": [],
50
50
  },
51
- "uuid": None
51
+ "uuid": None,
52
52
  }
53
53
 
54
54
  for elem in element[2:]:
@@ -99,7 +99,7 @@ class LabelParser(BaseElementParser):
99
99
  "bold": False,
100
100
  "italic": False,
101
101
  "hide": False,
102
- "justify": []
102
+ "justify": [],
103
103
  }
104
104
 
105
105
  for elem in effects_element[1:]:
@@ -176,9 +176,9 @@ class HierarchicalLabelParser(BaseElementParser):
176
176
  "bold": False,
177
177
  "italic": False,
178
178
  "hide": False,
179
- "justify": []
179
+ "justify": [],
180
180
  },
181
- "uuid": None
181
+ "uuid": None,
182
182
  }
183
183
 
184
184
  for elem in element[2:]:
@@ -217,7 +217,7 @@ class HierarchicalLabelParser(BaseElementParser):
217
217
  "bold": False,
218
218
  "italic": False,
219
219
  "hide": False,
220
- "justify": []
220
+ "justify": [],
221
221
  }
222
222
 
223
223
  for elem in effects_element[1:]:
@@ -251,4 +251,4 @@ class HierarchicalLabelParser(BaseElementParser):
251
251
  elif elem_type == "bold":
252
252
  effects["bold"] = True
253
253
  elif elem_type == "italic":
254
- effects["italic"] = True
254
+ effects["italic"] = True
@@ -99,7 +99,9 @@ class ElementParserRegistry:
99
99
 
100
100
  # Try fallback parser
101
101
  if self._fallback_parser:
102
- self._logger.debug(f"Using fallback parser for unknown element type: {element_type_str}")
102
+ self._logger.debug(
103
+ f"Using fallback parser for unknown element type: {element_type_str}"
104
+ )
103
105
  return self._fallback_parser.parse(element)
104
106
 
105
107
  # No parser available
@@ -150,4 +152,4 @@ class ElementParserRegistry:
150
152
  """Clear all registered parsers."""
151
153
  self._parsers.clear()
152
154
  self._fallback_parser = None
153
- self._logger.debug("Cleared all registered parsers")
155
+ self._logger.debug("Cleared all registered parsers")
@@ -44,7 +44,7 @@ class SymbolParser(BaseElementParser):
44
44
  "fields_autoplaced": False,
45
45
  "uuid": None,
46
46
  "properties": [],
47
- "instances": []
47
+ "instances": [],
48
48
  }
49
49
 
50
50
  for elem in element[1:]:
@@ -112,7 +112,7 @@ class SymbolParser(BaseElementParser):
112
112
  "name": str(prop_element[1]),
113
113
  "value": str(prop_element[2]),
114
114
  "position": {"x": 0, "y": 0, "angle": 0},
115
- "effects": {}
115
+ "effects": {},
116
116
  }
117
117
 
118
118
  # Parse additional property elements
@@ -145,7 +145,7 @@ class SymbolParser(BaseElementParser):
145
145
  "bold": False,
146
146
  "italic": False,
147
147
  "hide": False,
148
- "justify": []
148
+ "justify": [],
149
149
  }
150
150
 
151
151
  for elem in effects_element[1:]:
@@ -193,12 +193,7 @@ class SymbolParser(BaseElementParser):
193
193
 
194
194
  def _parse_instance(self, instance_element: List[Any]) -> Optional[Dict[str, Any]]:
195
195
  """Parse a single instance element."""
196
- instance = {
197
- "project": "",
198
- "path": "",
199
- "reference": "",
200
- "unit": 1
201
- }
196
+ instance = {"project": "", "path": "", "reference": "", "unit": 1}
202
197
 
203
198
  for elem in instance_element[1:]:
204
199
  if isinstance(elem, list) and len(elem) >= 2:
@@ -224,4 +219,4 @@ class SymbolParser(BaseElementParser):
224
219
  elif isinstance(value, sexpdata.Symbol):
225
220
  return str(value).lower() in ("yes", "true", "1")
226
221
  else:
227
- return bool(value)
222
+ return bool(value)
@@ -39,7 +39,7 @@ class WireParser(BaseElementParser):
39
39
  "stroke_width": 0.0,
40
40
  "stroke_type": "default",
41
41
  "uuid": None,
42
- "wire_type": "wire" # Default to wire (vs bus)
42
+ "wire_type": "wire", # Default to wire (vs bus)
43
43
  }
44
44
 
45
45
  for elem in element[1:]:
@@ -96,4 +96,4 @@ class WireParser(BaseElementParser):
96
96
  elif stroke_type == "type":
97
97
  wire_data["stroke_type"] = str(stroke_elem[1])
98
98
  except (ValueError, IndexError) as e:
99
- self._logger.warning(f"Invalid stroke property: {stroke_elem}, error: {e}")
99
+ self._logger.warning(f"Invalid stroke property: {stroke_elem}, error: {e}")
@@ -15,4 +15,4 @@ __all__ = [
15
15
  "SymbolCache",
16
16
  "SymbolResolver",
17
17
  "SymbolValidator",
18
- ]
18
+ ]
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
17
17
 
18
18
  import sexpdata
19
19
 
20
- from ..library.cache import SymbolDefinition, LibraryStats
20
+ from ..library.cache import LibraryStats, SymbolDefinition
21
21
  from ..utils.validation import ValidationError
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -199,7 +199,7 @@ class SymbolCache(ISymbolCache):
199
199
  library_path=library_path,
200
200
  file_size=stat.st_size,
201
201
  last_modified=stat.st_mtime,
202
- symbol_count=0 # Will be updated when library is loaded
202
+ symbol_count=0, # Will be updated when library is loaded
203
203
  )
204
204
 
205
205
  logger.info(f"Added library: {library_name} ({library_path})")
@@ -257,10 +257,10 @@ class SymbolCache(ISymbolCache):
257
257
  name: {
258
258
  "file_size": stats.file_size,
259
259
  "symbols_count": stats.symbols_count,
260
- "last_loaded": stats.last_loaded
260
+ "last_loaded": stats.last_loaded,
261
261
  }
262
262
  for name, stats in self._lib_stats.items()
263
- }
263
+ },
264
264
  }
265
265
 
266
266
  # Private methods for implementation details
@@ -316,10 +316,7 @@ class SymbolCache(ISymbolCache):
316
316
  return None
317
317
 
318
318
  def _create_symbol_definition(
319
- self,
320
- symbol_data: List,
321
- lib_id: str,
322
- library_name: str
319
+ self, symbol_data: List, lib_id: str, library_name: str
323
320
  ) -> SymbolDefinition:
324
321
  """Create SymbolDefinition from parsed symbol data."""
325
322
  symbol_name = str(symbol_data[1]).strip('"')
@@ -345,7 +342,7 @@ class SymbolCache(ISymbolCache):
345
342
  power_symbol=properties.get("power_symbol", False),
346
343
  graphic_elements=graphic_elements,
347
344
  raw_kicad_data=symbol_data,
348
- extends=extends # Store extends information for resolver
345
+ extends=extends, # Store extends information for resolver
349
346
  )
350
347
 
351
348
  def _extract_symbol_properties(self, symbol_data: List) -> Dict[str, Any]:
@@ -356,7 +353,7 @@ class SymbolCache(ISymbolCache):
356
353
  "keywords": "",
357
354
  "datasheet": "",
358
355
  "units": 1,
359
- "power_symbol": False
356
+ "power_symbol": False,
360
357
  }
361
358
 
362
359
  for item in symbol_data[1:]:
@@ -446,7 +443,7 @@ class SymbolCache(ISymbolCache):
446
443
  index_data = {
447
444
  "symbol_index": self._symbol_index,
448
445
  "library_paths": [str(path) for path in self._library_paths],
449
- "created": time.time()
446
+ "created": time.time(),
450
447
  }
451
448
 
452
449
  with open(self._index_file, "w") as f:
@@ -467,4 +464,4 @@ class SymbolCache(ISymbolCache):
467
464
  if str(item[0]) == "symbol" and str(item[1]) == symbol_name:
468
465
  return item
469
466
 
470
- return None
467
+ return None
@@ -86,7 +86,7 @@ class SymbolResolver:
86
86
  max_chain_length = 0
87
87
 
88
88
  for symbol in self._inheritance_cache.values():
89
- if hasattr(symbol, '_inheritance_depth'):
89
+ if hasattr(symbol, "_inheritance_depth"):
90
90
  inheritance_chains += 1
91
91
  max_chain_length = max(max_chain_length, symbol._inheritance_depth)
92
92
 
@@ -94,7 +94,7 @@ class SymbolResolver:
94
94
  "resolved_symbols": len(self._inheritance_cache),
95
95
  "inheritance_chains": inheritance_chains,
96
96
  "max_chain_length": max_chain_length,
97
- "cache_size": len(self._inheritance_cache)
97
+ "cache_size": len(self._inheritance_cache),
98
98
  }
99
99
 
100
100
  def _resolve_with_inheritance(self, symbol: SymbolDefinition) -> Optional[SymbolDefinition]:
@@ -112,7 +112,9 @@ class SymbolResolver:
112
112
 
113
113
  # Check for circular inheritance
114
114
  if symbol.lib_id in self._resolution_stack:
115
- logger.error(f"Circular inheritance detected: {' -> '.join(self._resolution_stack + [symbol.lib_id])}")
115
+ logger.error(
116
+ f"Circular inheritance detected: {' -> '.join(self._resolution_stack + [symbol.lib_id])}"
117
+ )
116
118
  return None
117
119
 
118
120
  self._resolution_stack.append(symbol.lib_id)
@@ -136,7 +138,7 @@ class SymbolResolver:
136
138
  resolved_symbol = self._merge_parent_into_child(symbol, resolved_parent)
137
139
 
138
140
  # Track inheritance depth for statistics
139
- parent_depth = getattr(resolved_parent, '_inheritance_depth', 0)
141
+ parent_depth = getattr(resolved_parent, "_inheritance_depth", 0)
140
142
  resolved_symbol._inheritance_depth = parent_depth + 1
141
143
 
142
144
  logger.debug(f"Resolved inheritance: {symbol.lib_id} extends {parent_lib_id}")
@@ -168,9 +170,7 @@ class SymbolResolver:
168
170
  return f"{current_library}:{parent_name}"
169
171
 
170
172
  def _merge_parent_into_child(
171
- self,
172
- child: SymbolDefinition,
173
- parent: SymbolDefinition
173
+ self, child: SymbolDefinition, parent: SymbolDefinition
174
174
  ) -> SymbolDefinition:
175
175
  """
176
176
  Merge parent symbol into child symbol.
@@ -188,10 +188,7 @@ class SymbolResolver:
188
188
  # Merge raw KiCAD data for exact format preservation
189
189
  if child.raw_kicad_data and parent.raw_kicad_data:
190
190
  merged.raw_kicad_data = self._merge_kicad_data(
191
- child.raw_kicad_data,
192
- parent.raw_kicad_data,
193
- child.name,
194
- parent.name
191
+ child.raw_kicad_data, parent.raw_kicad_data, child.name, parent.name
195
192
  )
196
193
 
197
194
  # Merge other properties
@@ -204,11 +201,7 @@ class SymbolResolver:
204
201
  return merged
205
202
 
206
203
  def _merge_kicad_data(
207
- self,
208
- child_data: List,
209
- parent_data: List,
210
- child_name: str,
211
- parent_name: str
204
+ self, child_data: List, parent_data: List, child_name: str, parent_name: str
212
205
  ) -> List:
213
206
  """
214
207
  Merge parent KiCAD data into child KiCAD data.
@@ -227,11 +220,10 @@ class SymbolResolver:
227
220
 
228
221
  # Remove extends directive from child
229
222
  merged = [
230
- item for item in merged
223
+ item
224
+ for item in merged
231
225
  if not (
232
- isinstance(item, list) and
233
- len(item) >= 2 and
234
- item[0] == sexpdata.Symbol("extends")
226
+ isinstance(item, list) and len(item) >= 2 and item[0] == sexpdata.Symbol("extends")
235
227
  )
236
228
  ]
237
229
 
@@ -255,9 +247,7 @@ class SymbolResolver:
255
247
  return merged
256
248
 
257
249
  def _merge_symbol_properties(
258
- self,
259
- child: SymbolDefinition,
260
- parent: SymbolDefinition
250
+ self, child: SymbolDefinition, parent: SymbolDefinition
261
251
  ) -> SymbolDefinition:
262
252
  """
263
253
  Merge symbol properties, with child properties taking precedence.
@@ -315,7 +305,9 @@ class SymbolResolver:
315
305
 
316
306
  def check_symbol(current_lib_id: str) -> None:
317
307
  if current_lib_id in visited:
318
- issues.append(f"Circular inheritance detected: {' -> '.join(chain + [current_lib_id])}")
308
+ issues.append(
309
+ f"Circular inheritance detected: {' -> '.join(chain + [current_lib_id])}"
310
+ )
319
311
  return
320
312
 
321
313
  visited.add(current_lib_id)
@@ -329,7 +321,9 @@ class SymbolResolver:
329
321
  if symbol.extends:
330
322
  parent_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
331
323
  if not self._cache.has_symbol(parent_lib_id):
332
- issues.append(f"Parent symbol not found: {parent_lib_id} (extended by {current_lib_id})")
324
+ issues.append(
325
+ f"Parent symbol not found: {parent_lib_id} (extended by {current_lib_id})"
326
+ )
333
327
  else:
334
328
  check_symbol(parent_lib_id)
335
329
 
@@ -364,4 +358,4 @@ class SymbolResolver:
364
358
 
365
359
  current_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
366
360
 
367
- return chain
361
+ return chain