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.
- kicad_sch_api/collections/__init__.py +21 -0
- kicad_sch_api/collections/base.py +294 -0
- kicad_sch_api/collections/components.py +434 -0
- kicad_sch_api/collections/junctions.py +366 -0
- kicad_sch_api/collections/labels.py +404 -0
- kicad_sch_api/collections/wires.py +406 -0
- kicad_sch_api/core/components.py +5 -0
- kicad_sch_api/core/formatter.py +3 -1
- kicad_sch_api/core/labels.py +348 -0
- kicad_sch_api/core/managers/__init__.py +26 -0
- kicad_sch_api/core/managers/file_io.py +243 -0
- kicad_sch_api/core/managers/format_sync.py +501 -0
- kicad_sch_api/core/managers/graphics.py +579 -0
- kicad_sch_api/core/managers/metadata.py +268 -0
- kicad_sch_api/core/managers/sheet.py +454 -0
- kicad_sch_api/core/managers/text_elements.py +536 -0
- kicad_sch_api/core/managers/validation.py +474 -0
- kicad_sch_api/core/managers/wire.py +346 -0
- kicad_sch_api/core/nets.py +310 -0
- kicad_sch_api/core/no_connects.py +276 -0
- kicad_sch_api/core/parser.py +75 -41
- kicad_sch_api/core/schematic.py +904 -1074
- kicad_sch_api/core/texts.py +343 -0
- kicad_sch_api/core/types.py +13 -4
- kicad_sch_api/geometry/font_metrics.py +3 -1
- kicad_sch_api/geometry/symbol_bbox.py +56 -43
- kicad_sch_api/interfaces/__init__.py +17 -0
- kicad_sch_api/interfaces/parser.py +76 -0
- kicad_sch_api/interfaces/repository.py +70 -0
- kicad_sch_api/interfaces/resolver.py +117 -0
- kicad_sch_api/parsers/__init__.py +14 -0
- kicad_sch_api/parsers/base.py +145 -0
- kicad_sch_api/parsers/label_parser.py +254 -0
- kicad_sch_api/parsers/registry.py +155 -0
- kicad_sch_api/parsers/symbol_parser.py +222 -0
- kicad_sch_api/parsers/wire_parser.py +99 -0
- kicad_sch_api/symbols/__init__.py +18 -0
- kicad_sch_api/symbols/cache.py +467 -0
- kicad_sch_api/symbols/resolver.py +361 -0
- kicad_sch_api/symbols/validators.py +504 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/METADATA +1 -1
- kicad_sch_api-0.4.0.dist-info/RECORD +67 -0
- kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {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
|