kicad-sch-api 0.3.2__py3-none-any.whl → 0.3.5__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 (39) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/collections/__init__.py +21 -0
  3. kicad_sch_api/collections/base.py +296 -0
  4. kicad_sch_api/collections/components.py +422 -0
  5. kicad_sch_api/collections/junctions.py +378 -0
  6. kicad_sch_api/collections/labels.py +412 -0
  7. kicad_sch_api/collections/wires.py +407 -0
  8. kicad_sch_api/core/formatter.py +31 -0
  9. kicad_sch_api/core/labels.py +348 -0
  10. kicad_sch_api/core/nets.py +310 -0
  11. kicad_sch_api/core/no_connects.py +274 -0
  12. kicad_sch_api/core/parser.py +72 -0
  13. kicad_sch_api/core/schematic.py +185 -9
  14. kicad_sch_api/core/texts.py +343 -0
  15. kicad_sch_api/core/types.py +26 -0
  16. kicad_sch_api/geometry/__init__.py +26 -0
  17. kicad_sch_api/geometry/font_metrics.py +20 -0
  18. kicad_sch_api/geometry/symbol_bbox.py +589 -0
  19. kicad_sch_api/interfaces/__init__.py +17 -0
  20. kicad_sch_api/interfaces/parser.py +76 -0
  21. kicad_sch_api/interfaces/repository.py +70 -0
  22. kicad_sch_api/interfaces/resolver.py +117 -0
  23. kicad_sch_api/parsers/__init__.py +14 -0
  24. kicad_sch_api/parsers/base.py +148 -0
  25. kicad_sch_api/parsers/label_parser.py +254 -0
  26. kicad_sch_api/parsers/registry.py +153 -0
  27. kicad_sch_api/parsers/symbol_parser.py +227 -0
  28. kicad_sch_api/parsers/wire_parser.py +99 -0
  29. kicad_sch_api/symbols/__init__.py +18 -0
  30. kicad_sch_api/symbols/cache.py +470 -0
  31. kicad_sch_api/symbols/resolver.py +367 -0
  32. kicad_sch_api/symbols/validators.py +453 -0
  33. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  34. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  35. kicad_sch_api-0.3.2.dist-info/RECORD +0 -31
  36. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  37. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  38. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  39. {kicad_sch_api-0.3.2.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,367 @@
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(f"Circular inheritance detected: {' -> '.join(self._resolution_stack + [symbol.lib_id])}")
116
+ return None
117
+
118
+ self._resolution_stack.append(symbol.lib_id)
119
+
120
+ try:
121
+ # Get parent symbol
122
+ parent_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
123
+ parent_symbol = self._cache.get_symbol(parent_lib_id)
124
+
125
+ if not parent_symbol:
126
+ logger.warning(f"Parent symbol {parent_lib_id} not found for {symbol.lib_id}")
127
+ return None
128
+
129
+ # Recursively resolve parent inheritance
130
+ resolved_parent = self._resolve_with_inheritance(parent_symbol)
131
+ if not resolved_parent:
132
+ logger.error(f"Failed to resolve parent {parent_lib_id} for {symbol.lib_id}")
133
+ return None
134
+
135
+ # Merge parent into child
136
+ resolved_symbol = self._merge_parent_into_child(symbol, resolved_parent)
137
+
138
+ # Track inheritance depth for statistics
139
+ parent_depth = getattr(resolved_parent, '_inheritance_depth', 0)
140
+ resolved_symbol._inheritance_depth = parent_depth + 1
141
+
142
+ logger.debug(f"Resolved inheritance: {symbol.lib_id} extends {parent_lib_id}")
143
+ return resolved_symbol
144
+
145
+ except Exception as e:
146
+ logger.error(f"Error resolving inheritance for {symbol.lib_id}: {e}")
147
+ return None
148
+
149
+ finally:
150
+ self._resolution_stack.pop()
151
+
152
+ def _resolve_parent_lib_id(self, parent_name: str, current_library: str) -> str:
153
+ """
154
+ Resolve parent symbol lib_id from extends name.
155
+
156
+ Args:
157
+ parent_name: Name from extends directive
158
+ current_library: Current symbol's library
159
+
160
+ Returns:
161
+ Full lib_id for parent symbol
162
+ """
163
+ # If parent_name contains library (e.g., "Device:R"), use as-is
164
+ if ":" in parent_name:
165
+ return parent_name
166
+
167
+ # Otherwise, assume same library
168
+ return f"{current_library}:{parent_name}"
169
+
170
+ def _merge_parent_into_child(
171
+ self,
172
+ child: SymbolDefinition,
173
+ 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,
192
+ parent.raw_kicad_data,
193
+ child.name,
194
+ parent.name
195
+ )
196
+
197
+ # Merge other properties
198
+ merged = self._merge_symbol_properties(merged, parent)
199
+
200
+ # Clear extends since we've resolved it
201
+ merged.extends = None
202
+
203
+ logger.debug(f"Merged {parent.lib_id} into {child.lib_id}")
204
+ return merged
205
+
206
+ def _merge_kicad_data(
207
+ self,
208
+ child_data: List,
209
+ parent_data: List,
210
+ child_name: str,
211
+ parent_name: str
212
+ ) -> List:
213
+ """
214
+ Merge parent KiCAD data into child KiCAD data.
215
+
216
+ Args:
217
+ child_data: Child symbol S-expression data
218
+ parent_data: Parent symbol S-expression data
219
+ child_name: Child symbol name for unit renaming
220
+ parent_name: Parent symbol name for unit renaming
221
+
222
+ Returns:
223
+ Merged S-expression data
224
+ """
225
+ # Start with child data structure
226
+ merged = copy.deepcopy(child_data)
227
+
228
+ # Remove extends directive from child
229
+ merged = [
230
+ item for item in merged
231
+ if not (
232
+ isinstance(item, list) and
233
+ len(item) >= 2 and
234
+ item[0] == sexpdata.Symbol("extends")
235
+ )
236
+ ]
237
+
238
+ # Copy symbol units and graphics from parent
239
+ for item in parent_data[1:]:
240
+ if isinstance(item, list) and len(item) > 0:
241
+ if item[0] == sexpdata.Symbol("symbol"):
242
+ # Copy symbol unit with name adjustment
243
+ unit_item = copy.deepcopy(item)
244
+ if len(unit_item) > 1:
245
+ old_unit_name = str(unit_item[1]).strip('"')
246
+ new_unit_name = old_unit_name.replace(parent_name, child_name)
247
+ unit_item[1] = new_unit_name
248
+ logger.debug(f"Renamed unit {old_unit_name} -> {new_unit_name}")
249
+ merged.append(unit_item)
250
+
251
+ elif item[0] not in [sexpdata.Symbol("property")]:
252
+ # Copy other non-property elements (child properties take precedence)
253
+ merged.append(copy.deepcopy(item))
254
+
255
+ return merged
256
+
257
+ def _merge_symbol_properties(
258
+ self,
259
+ child: SymbolDefinition,
260
+ parent: SymbolDefinition
261
+ ) -> SymbolDefinition:
262
+ """
263
+ Merge symbol properties, with child properties taking precedence.
264
+
265
+ Args:
266
+ child: Child symbol (will be modified)
267
+ parent: Parent symbol (source of inherited properties)
268
+
269
+ Returns:
270
+ Child symbol with inherited properties
271
+ """
272
+ # Inherit parent properties where child doesn't have them
273
+ if not child.description and parent.description:
274
+ child.description = parent.description
275
+
276
+ if not child.keywords and parent.keywords:
277
+ child.keywords = parent.keywords
278
+
279
+ if not child.datasheet and parent.datasheet:
280
+ child.datasheet = parent.datasheet
281
+
282
+ # Merge pins from parent (child pins take precedence)
283
+ parent_pin_numbers = {pin.number for pin in child.pins}
284
+ for parent_pin in parent.pins:
285
+ if parent_pin.number not in parent_pin_numbers:
286
+ child.pins.append(copy.deepcopy(parent_pin))
287
+
288
+ # Merge graphic elements from parent
289
+ child.graphic_elements.extend(copy.deepcopy(parent.graphic_elements))
290
+
291
+ # Inherit unit information
292
+ if parent.units > child.units:
293
+ child.units = parent.units
294
+
295
+ # Merge unit names
296
+ for unit_num, unit_name in parent.unit_names.items():
297
+ if unit_num not in child.unit_names:
298
+ child.unit_names[unit_num] = unit_name
299
+
300
+ return child
301
+
302
+ def validate_inheritance_chain(self, lib_id: str) -> List[str]:
303
+ """
304
+ Validate inheritance chain for cycles and missing parents.
305
+
306
+ Args:
307
+ lib_id: Symbol to validate
308
+
309
+ Returns:
310
+ List of issues found (empty if valid)
311
+ """
312
+ issues = []
313
+ visited = set()
314
+ chain = []
315
+
316
+ def check_symbol(current_lib_id: str) -> None:
317
+ if current_lib_id in visited:
318
+ issues.append(f"Circular inheritance detected: {' -> '.join(chain + [current_lib_id])}")
319
+ return
320
+
321
+ visited.add(current_lib_id)
322
+ chain.append(current_lib_id)
323
+
324
+ symbol = self._cache.get_symbol(current_lib_id)
325
+ if not symbol:
326
+ issues.append(f"Symbol not found: {current_lib_id}")
327
+ return
328
+
329
+ if symbol.extends:
330
+ parent_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
331
+ 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})")
333
+ else:
334
+ check_symbol(parent_lib_id)
335
+
336
+ chain.pop()
337
+
338
+ check_symbol(lib_id)
339
+ return issues
340
+
341
+ def get_inheritance_chain(self, lib_id: str) -> List[str]:
342
+ """
343
+ Get the complete inheritance chain for a symbol.
344
+
345
+ Args:
346
+ lib_id: Symbol to get chain for
347
+
348
+ Returns:
349
+ List of lib_ids in inheritance order (child to parent)
350
+ """
351
+ chain = []
352
+ current_lib_id = lib_id
353
+
354
+ while current_lib_id:
355
+ if current_lib_id in chain:
356
+ # Circular inheritance
357
+ break
358
+
359
+ chain.append(current_lib_id)
360
+ symbol = self._cache.get_symbol(current_lib_id)
361
+
362
+ if not symbol or not symbol.extends:
363
+ break
364
+
365
+ current_lib_id = self._resolve_parent_lib_id(symbol.extends, symbol.library)
366
+
367
+ return chain