kicad-sch-api 0.3.4__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 +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  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 +348 -0
  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 +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.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.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,361 @@
1
+ """
2
+ Symbol resolution with inheritance support for KiCAD symbols.
3
+
4
+ Provides authoritative symbol inheritance and resolution, separating
5
+ this concern from caching for better testability and maintainability.
6
+ """
7
+
8
+ import copy
9
+ import logging
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import sexpdata
13
+
14
+ from ..library.cache import SymbolDefinition
15
+ from .cache import ISymbolCache
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class SymbolResolver:
21
+ """
22
+ Authoritative symbol inheritance and resolution.
23
+
24
+ Handles the complex logic of resolving symbol inheritance chains
25
+ while maintaining clean separation from caching concerns.
26
+ """
27
+
28
+ def __init__(self, cache: ISymbolCache):
29
+ """
30
+ Initialize symbol resolver.
31
+
32
+ Args:
33
+ cache: Symbol cache implementation
34
+ """
35
+ self._cache = cache
36
+ self._inheritance_cache: Dict[str, SymbolDefinition] = {}
37
+ self._resolution_stack: List[str] = [] # Track resolution to detect cycles
38
+
39
+ def resolve_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
40
+ """
41
+ Resolve symbol with full inheritance chain.
42
+
43
+ Args:
44
+ lib_id: Library identifier (e.g., "Device:R")
45
+
46
+ Returns:
47
+ Fully resolved symbol with inheritance applied, or None if not found
48
+ """
49
+ # Check inheritance cache first
50
+ if lib_id in self._inheritance_cache:
51
+ symbol = self._inheritance_cache[lib_id]
52
+ symbol.access_count += 1
53
+ return symbol
54
+
55
+ # Get raw symbol from cache
56
+ symbol = self._cache.get_symbol(lib_id)
57
+ if not symbol:
58
+ return None
59
+
60
+ # If symbol has no inheritance, cache and return as-is
61
+ if not symbol.extends:
62
+ resolved_symbol = copy.deepcopy(symbol)
63
+ self._inheritance_cache[lib_id] = resolved_symbol
64
+ return resolved_symbol
65
+
66
+ # Resolve inheritance chain
67
+ resolved_symbol = self._resolve_with_inheritance(symbol)
68
+ if resolved_symbol:
69
+ self._inheritance_cache[lib_id] = resolved_symbol
70
+
71
+ return resolved_symbol
72
+
73
+ def clear_inheritance_cache(self) -> None:
74
+ """Clear the inheritance resolution cache."""
75
+ self._inheritance_cache.clear()
76
+ logger.debug("Cleared inheritance cache")
77
+
78
+ def get_inheritance_statistics(self) -> Dict[str, Any]:
79
+ """
80
+ Get inheritance resolution statistics.
81
+
82
+ Returns:
83
+ Dictionary with inheritance statistics
84
+ """
85
+ inheritance_chains = 0
86
+ max_chain_length = 0
87
+
88
+ for symbol in self._inheritance_cache.values():
89
+ if hasattr(symbol, "_inheritance_depth"):
90
+ inheritance_chains += 1
91
+ max_chain_length = max(max_chain_length, symbol._inheritance_depth)
92
+
93
+ return {
94
+ "resolved_symbols": len(self._inheritance_cache),
95
+ "inheritance_chains": inheritance_chains,
96
+ "max_chain_length": max_chain_length,
97
+ "cache_size": len(self._inheritance_cache),
98
+ }
99
+
100
+ def _resolve_with_inheritance(self, symbol: SymbolDefinition) -> Optional[SymbolDefinition]:
101
+ """
102
+ Private implementation of inheritance resolution.
103
+
104
+ Args:
105
+ symbol: Symbol to resolve
106
+
107
+ Returns:
108
+ Resolved symbol or None if resolution failed
109
+ """
110
+ if not symbol.extends:
111
+ return copy.deepcopy(symbol)
112
+
113
+ # Check for circular inheritance
114
+ if symbol.lib_id in self._resolution_stack:
115
+ logger.error(
116
+ f"Circular inheritance detected: {' -> '.join(self._resolution_stack + [symbol.lib_id])}"
117
+ )
118
+ return None
119
+
120
+ self._resolution_stack.append(symbol.lib_id)
121
+
122
+ try:
123
+ # Get parent symbol
124
+ parent_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
125
+ parent_symbol = self._cache.get_symbol(parent_lib_id)
126
+
127
+ if not parent_symbol:
128
+ logger.warning(f"Parent symbol {parent_lib_id} not found for {symbol.lib_id}")
129
+ return None
130
+
131
+ # Recursively resolve parent inheritance
132
+ resolved_parent = self._resolve_with_inheritance(parent_symbol)
133
+ if not resolved_parent:
134
+ logger.error(f"Failed to resolve parent {parent_lib_id} for {symbol.lib_id}")
135
+ return None
136
+
137
+ # Merge parent into child
138
+ resolved_symbol = self._merge_parent_into_child(symbol, resolved_parent)
139
+
140
+ # Track inheritance depth for statistics
141
+ parent_depth = getattr(resolved_parent, "_inheritance_depth", 0)
142
+ resolved_symbol._inheritance_depth = parent_depth + 1
143
+
144
+ logger.debug(f"Resolved inheritance: {symbol.lib_id} extends {parent_lib_id}")
145
+ return resolved_symbol
146
+
147
+ except Exception as e:
148
+ logger.error(f"Error resolving inheritance for {symbol.lib_id}: {e}")
149
+ return None
150
+
151
+ finally:
152
+ self._resolution_stack.pop()
153
+
154
+ def _resolve_parent_lib_id(self, parent_name: str, current_library: str) -> str:
155
+ """
156
+ Resolve parent symbol lib_id from extends name.
157
+
158
+ Args:
159
+ parent_name: Name from extends directive
160
+ current_library: Current symbol's library
161
+
162
+ Returns:
163
+ Full lib_id for parent symbol
164
+ """
165
+ # If parent_name contains library (e.g., "Device:R"), use as-is
166
+ if ":" in parent_name:
167
+ return parent_name
168
+
169
+ # Otherwise, assume same library
170
+ return f"{current_library}:{parent_name}"
171
+
172
+ def _merge_parent_into_child(
173
+ self, child: SymbolDefinition, parent: SymbolDefinition
174
+ ) -> SymbolDefinition:
175
+ """
176
+ Merge parent symbol into child symbol.
177
+
178
+ Args:
179
+ child: Child symbol definition
180
+ parent: Resolved parent symbol definition
181
+
182
+ Returns:
183
+ New symbol definition with inheritance applied
184
+ """
185
+ # Start with deep copy of child
186
+ merged = copy.deepcopy(child)
187
+
188
+ # Merge raw KiCAD data for exact format preservation
189
+ if child.raw_kicad_data and parent.raw_kicad_data:
190
+ merged.raw_kicad_data = self._merge_kicad_data(
191
+ child.raw_kicad_data, parent.raw_kicad_data, child.name, parent.name
192
+ )
193
+
194
+ # Merge other properties
195
+ merged = self._merge_symbol_properties(merged, parent)
196
+
197
+ # Clear extends since we've resolved it
198
+ merged.extends = None
199
+
200
+ logger.debug(f"Merged {parent.lib_id} into {child.lib_id}")
201
+ return merged
202
+
203
+ def _merge_kicad_data(
204
+ self, child_data: List, parent_data: List, child_name: str, parent_name: str
205
+ ) -> List:
206
+ """
207
+ Merge parent KiCAD data into child KiCAD data.
208
+
209
+ Args:
210
+ child_data: Child symbol S-expression data
211
+ parent_data: Parent symbol S-expression data
212
+ child_name: Child symbol name for unit renaming
213
+ parent_name: Parent symbol name for unit renaming
214
+
215
+ Returns:
216
+ Merged S-expression data
217
+ """
218
+ # Start with child data structure
219
+ merged = copy.deepcopy(child_data)
220
+
221
+ # Remove extends directive from child
222
+ merged = [
223
+ item
224
+ for item in merged
225
+ if not (
226
+ isinstance(item, list) and len(item) >= 2 and item[0] == sexpdata.Symbol("extends")
227
+ )
228
+ ]
229
+
230
+ # Copy symbol units and graphics from parent
231
+ for item in parent_data[1:]:
232
+ if isinstance(item, list) and len(item) > 0:
233
+ if item[0] == sexpdata.Symbol("symbol"):
234
+ # Copy symbol unit with name adjustment
235
+ unit_item = copy.deepcopy(item)
236
+ if len(unit_item) > 1:
237
+ old_unit_name = str(unit_item[1]).strip('"')
238
+ new_unit_name = old_unit_name.replace(parent_name, child_name)
239
+ unit_item[1] = new_unit_name
240
+ logger.debug(f"Renamed unit {old_unit_name} -> {new_unit_name}")
241
+ merged.append(unit_item)
242
+
243
+ elif item[0] not in [sexpdata.Symbol("property")]:
244
+ # Copy other non-property elements (child properties take precedence)
245
+ merged.append(copy.deepcopy(item))
246
+
247
+ return merged
248
+
249
+ def _merge_symbol_properties(
250
+ self, child: SymbolDefinition, parent: SymbolDefinition
251
+ ) -> SymbolDefinition:
252
+ """
253
+ Merge symbol properties, with child properties taking precedence.
254
+
255
+ Args:
256
+ child: Child symbol (will be modified)
257
+ parent: Parent symbol (source of inherited properties)
258
+
259
+ Returns:
260
+ Child symbol with inherited properties
261
+ """
262
+ # Inherit parent properties where child doesn't have them
263
+ if not child.description and parent.description:
264
+ child.description = parent.description
265
+
266
+ if not child.keywords and parent.keywords:
267
+ child.keywords = parent.keywords
268
+
269
+ if not child.datasheet and parent.datasheet:
270
+ child.datasheet = parent.datasheet
271
+
272
+ # Merge pins from parent (child pins take precedence)
273
+ parent_pin_numbers = {pin.number for pin in child.pins}
274
+ for parent_pin in parent.pins:
275
+ if parent_pin.number not in parent_pin_numbers:
276
+ child.pins.append(copy.deepcopy(parent_pin))
277
+
278
+ # Merge graphic elements from parent
279
+ child.graphic_elements.extend(copy.deepcopy(parent.graphic_elements))
280
+
281
+ # Inherit unit information
282
+ if parent.units > child.units:
283
+ child.units = parent.units
284
+
285
+ # Merge unit names
286
+ for unit_num, unit_name in parent.unit_names.items():
287
+ if unit_num not in child.unit_names:
288
+ child.unit_names[unit_num] = unit_name
289
+
290
+ return child
291
+
292
+ def validate_inheritance_chain(self, lib_id: str) -> List[str]:
293
+ """
294
+ Validate inheritance chain for cycles and missing parents.
295
+
296
+ Args:
297
+ lib_id: Symbol to validate
298
+
299
+ Returns:
300
+ List of issues found (empty if valid)
301
+ """
302
+ issues = []
303
+ visited = set()
304
+ chain = []
305
+
306
+ def check_symbol(current_lib_id: str) -> None:
307
+ if current_lib_id in visited:
308
+ issues.append(
309
+ f"Circular inheritance detected: {' -> '.join(chain + [current_lib_id])}"
310
+ )
311
+ return
312
+
313
+ visited.add(current_lib_id)
314
+ chain.append(current_lib_id)
315
+
316
+ symbol = self._cache.get_symbol(current_lib_id)
317
+ if not symbol:
318
+ issues.append(f"Symbol not found: {current_lib_id}")
319
+ return
320
+
321
+ if symbol.extends:
322
+ parent_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
323
+ if not self._cache.has_symbol(parent_lib_id):
324
+ issues.append(
325
+ f"Parent symbol not found: {parent_lib_id} (extended by {current_lib_id})"
326
+ )
327
+ else:
328
+ check_symbol(parent_lib_id)
329
+
330
+ chain.pop()
331
+
332
+ check_symbol(lib_id)
333
+ return issues
334
+
335
+ def get_inheritance_chain(self, lib_id: str) -> List[str]:
336
+ """
337
+ Get the complete inheritance chain for a symbol.
338
+
339
+ Args:
340
+ lib_id: Symbol to get chain for
341
+
342
+ Returns:
343
+ List of lib_ids in inheritance order (child to parent)
344
+ """
345
+ chain = []
346
+ current_lib_id = lib_id
347
+
348
+ while current_lib_id:
349
+ if current_lib_id in chain:
350
+ # Circular inheritance
351
+ break
352
+
353
+ chain.append(current_lib_id)
354
+ symbol = self._cache.get_symbol(current_lib_id)
355
+
356
+ if not symbol or not symbol.extends:
357
+ break
358
+
359
+ current_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
360
+
361
+ return chain