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
@@ -0,0 +1,501 @@
1
+ """
2
+ Format Synchronization Manager for KiCAD schematic data consistency.
3
+
4
+ Handles bidirectional synchronization between Python object models and
5
+ raw S-expression data structures while maintaining exact format preservation
6
+ and tracking changes for efficient updates.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any, Dict, List, Optional, Set, Union
11
+
12
+ from ..components import Component
13
+ from ..types import Point, Wire
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class FormatSyncManager:
19
+ """
20
+ Manages synchronization between object models and S-expression data.
21
+
22
+ Responsible for:
23
+ - Bidirectional data synchronization
24
+ - Change tracking and dirty flag management
25
+ - Format preservation during updates
26
+ - Incremental update optimization
27
+ - Data consistency validation
28
+ """
29
+
30
+ def __init__(self, schematic_data: Dict[str, Any]):
31
+ """
32
+ Initialize FormatSyncManager.
33
+
34
+ Args:
35
+ schematic_data: Reference to schematic data
36
+ """
37
+ self._data = schematic_data
38
+ self._dirty_flags: Set[str] = set()
39
+ self._change_log: List[Dict[str, Any]] = []
40
+ self._sync_lock = False
41
+
42
+ def mark_dirty(
43
+ self, section: str, operation: str = "update", context: Optional[Dict] = None
44
+ ) -> None:
45
+ """
46
+ Mark a data section as dirty for synchronization.
47
+
48
+ Args:
49
+ section: Data section that changed (e.g., 'components', 'wires')
50
+ operation: Type of operation (update, add, remove)
51
+ context: Additional context about the change
52
+ """
53
+ if self._sync_lock:
54
+ logger.debug(f"Sync locked, deferring dirty mark for {section}")
55
+ return
56
+
57
+ self._dirty_flags.add(section)
58
+
59
+ change_entry = {
60
+ "section": section,
61
+ "operation": operation,
62
+ "timestamp": None, # Would use datetime in real implementation
63
+ "context": context or {},
64
+ }
65
+ self._change_log.append(change_entry)
66
+
67
+ logger.debug(f"Marked section '{section}' as dirty ({operation})")
68
+
69
+ def sync_component_to_data(self, component: Component) -> None:
70
+ """
71
+ Synchronize a component object back to S-expression data.
72
+
73
+ Args:
74
+ component: Component to sync
75
+ """
76
+ symbols = self._data.get("symbol", [])
77
+
78
+ # Find the corresponding symbol entry
79
+ for symbol_data in symbols:
80
+ if symbol_data.get("uuid") == component.uuid:
81
+ self._update_symbol_data_from_component(symbol_data, component)
82
+ self.mark_dirty("symbol", "update", {"uuid": component.uuid})
83
+ return
84
+
85
+ logger.warning(f"Component not found in data for sync: {component.uuid}")
86
+
87
+ def sync_component_from_data(self, component: Component, symbol_data: Dict[str, Any]) -> None:
88
+ """
89
+ Update component object from S-expression data.
90
+
91
+ Args:
92
+ component: Component to update
93
+ symbol_data: Source symbol data
94
+ """
95
+ # Update component properties from symbol data
96
+ if "at" in symbol_data:
97
+ at_data = symbol_data["at"]
98
+ component.position = Point(at_data[0], at_data[1])
99
+ if len(at_data) > 2:
100
+ component.rotation = at_data[2]
101
+
102
+ # Update properties
103
+ if "property" in symbol_data:
104
+ for prop in symbol_data["property"]:
105
+ name = prop.get("name")
106
+ value = prop.get("value")
107
+ if name and value is not None:
108
+ component.set_property(name, value)
109
+
110
+ logger.debug(f"Synced component from data: {component.uuid}")
111
+
112
+ def sync_wire_to_data(self, wire: Wire) -> None:
113
+ """
114
+ Synchronize a wire object back to S-expression data.
115
+
116
+ Args:
117
+ wire: Wire to sync
118
+ """
119
+ wires = self._data.get("wire", [])
120
+
121
+ # Find the corresponding wire entry
122
+ for wire_data in wires:
123
+ if wire_data.get("uuid") == wire.uuid:
124
+ self._update_wire_data_from_object(wire_data, wire)
125
+ self.mark_dirty("wire", "update", {"uuid": wire.uuid})
126
+ return
127
+
128
+ logger.warning(f"Wire not found in data for sync: {wire.uuid}")
129
+
130
+ def sync_wire_from_data(self, wire: Wire, wire_data: Dict[str, Any]) -> None:
131
+ """
132
+ Update wire object from S-expression data.
133
+
134
+ Args:
135
+ wire: Wire to update
136
+ wire_data: Source wire data
137
+ """
138
+ # Update wire endpoints
139
+ if "pts" in wire_data:
140
+ pts = wire_data["pts"]
141
+ if len(pts) >= 2:
142
+ start_pt = pts[0]
143
+ end_pt = pts[-1]
144
+ wire.start = Point(start_pt["xy"][0], start_pt["xy"][1])
145
+ wire.end = Point(end_pt["xy"][0], end_pt["xy"][1])
146
+
147
+ # Update stroke properties
148
+ if "stroke" in wire_data:
149
+ wire.stroke_width = wire_data["stroke"].get("width", 0.0)
150
+
151
+ logger.debug(f"Synced wire from data: {wire.uuid}")
152
+
153
+ def sync_all_to_data(self, component_collection=None, wire_collection=None) -> None:
154
+ """
155
+ Perform full synchronization of all objects to S-expression data.
156
+
157
+ Args:
158
+ component_collection: Collection of components to sync
159
+ wire_collection: Collection of wires to sync
160
+ """
161
+ self._sync_lock = True
162
+
163
+ try:
164
+ # Sync components
165
+ if component_collection:
166
+ for component in component_collection:
167
+ self.sync_component_to_data(component)
168
+
169
+ # Sync wires
170
+ if wire_collection:
171
+ for wire in wire_collection:
172
+ self.sync_wire_to_data(wire)
173
+
174
+ # Clear dirty flags after successful sync
175
+ self._dirty_flags.clear()
176
+ logger.info("Full synchronization to data completed")
177
+
178
+ finally:
179
+ self._sync_lock = False
180
+
181
+ def sync_all_from_data(self, component_collection=None, wire_collection=None) -> None:
182
+ """
183
+ Perform full synchronization from S-expression data to objects.
184
+
185
+ Args:
186
+ component_collection: Collection of components to update
187
+ wire_collection: Collection of wires to update
188
+ """
189
+ self._sync_lock = True
190
+
191
+ try:
192
+ # Sync components from symbols
193
+ if component_collection:
194
+ symbols = self._data.get("symbol", [])
195
+ for symbol_data in symbols:
196
+ uuid = symbol_data.get("uuid")
197
+ if uuid:
198
+ component = component_collection.get_by_uuid(uuid)
199
+ if component:
200
+ self.sync_component_from_data(component, symbol_data)
201
+
202
+ # Sync wires
203
+ if wire_collection:
204
+ wires = self._data.get("wire", [])
205
+ for wire_data in wires:
206
+ uuid = wire_data.get("uuid")
207
+ if uuid:
208
+ wire = wire_collection.get_by_uuid(uuid)
209
+ if wire:
210
+ self.sync_wire_from_data(wire, wire_data)
211
+
212
+ logger.info("Full synchronization from data completed")
213
+
214
+ finally:
215
+ self._sync_lock = False
216
+
217
+ def perform_incremental_sync(self, component_collection=None, wire_collection=None) -> None:
218
+ """
219
+ Perform incremental synchronization of only dirty sections.
220
+
221
+ Args:
222
+ component_collection: Collection of components
223
+ wire_collection: Collection of wires
224
+ """
225
+ if not self._dirty_flags:
226
+ logger.debug("No dirty sections, skipping sync")
227
+ return
228
+
229
+ self._sync_lock = True
230
+
231
+ try:
232
+ # Sync dirty components
233
+ if "symbol" in self._dirty_flags and component_collection:
234
+ for component in component_collection:
235
+ self.sync_component_to_data(component)
236
+
237
+ # Sync dirty wires
238
+ if "wire" in self._dirty_flags and wire_collection:
239
+ for wire in wire_collection:
240
+ self.sync_wire_to_data(wire)
241
+
242
+ # Clear processed dirty flags
243
+ self._dirty_flags.clear()
244
+ logger.info("Incremental synchronization completed")
245
+
246
+ finally:
247
+ self._sync_lock = False
248
+
249
+ def add_component_to_data(self, component: Component) -> None:
250
+ """
251
+ Add a new component to S-expression data.
252
+
253
+ Args:
254
+ component: Component to add
255
+ """
256
+ symbol_data = self._create_symbol_data_from_component(component)
257
+
258
+ if "symbol" not in self._data:
259
+ self._data["symbol"] = []
260
+
261
+ self._data["symbol"].append(symbol_data)
262
+ self.mark_dirty("symbol", "add", {"uuid": component.uuid})
263
+
264
+ logger.debug(f"Added component to data: {component.reference}")
265
+
266
+ def remove_component_from_data(self, component_uuid: str) -> bool:
267
+ """
268
+ Remove a component from S-expression data.
269
+
270
+ Args:
271
+ component_uuid: UUID of component to remove
272
+
273
+ Returns:
274
+ True if removed, False if not found
275
+ """
276
+ symbols = self._data.get("symbol", [])
277
+
278
+ for i, symbol_data in enumerate(symbols):
279
+ if symbol_data.get("uuid") == component_uuid:
280
+ del symbols[i]
281
+ self.mark_dirty("symbol", "remove", {"uuid": component_uuid})
282
+ logger.debug(f"Removed component from data: {component_uuid}")
283
+ return True
284
+
285
+ logger.warning(f"Component not found for removal: {component_uuid}")
286
+ return False
287
+
288
+ def add_wire_to_data(self, wire: Wire) -> None:
289
+ """
290
+ Add a new wire to S-expression data.
291
+
292
+ Args:
293
+ wire: Wire to add
294
+ """
295
+ wire_data = self._create_wire_data_from_object(wire)
296
+
297
+ if "wire" not in self._data:
298
+ self._data["wire"] = []
299
+
300
+ self._data["wire"].append(wire_data)
301
+ self.mark_dirty("wire", "add", {"uuid": wire.uuid})
302
+
303
+ logger.debug(f"Added wire to data: {wire.uuid}")
304
+
305
+ def remove_wire_from_data(self, wire_uuid: str) -> bool:
306
+ """
307
+ Remove a wire from S-expression data.
308
+
309
+ Args:
310
+ wire_uuid: UUID of wire to remove
311
+
312
+ Returns:
313
+ True if removed, False if not found
314
+ """
315
+ wires = self._data.get("wire", [])
316
+
317
+ for i, wire_data in enumerate(wires):
318
+ if wire_data.get("uuid") == wire_uuid:
319
+ del wires[i]
320
+ self.mark_dirty("wire", "remove", {"uuid": wire_uuid})
321
+ logger.debug(f"Removed wire from data: {wire_uuid}")
322
+ return True
323
+
324
+ logger.warning(f"Wire not found for removal: {wire_uuid}")
325
+ return False
326
+
327
+ def is_dirty(self, section: Optional[str] = None) -> bool:
328
+ """
329
+ Check if data sections are dirty.
330
+
331
+ Args:
332
+ section: Specific section to check, or None for any
333
+
334
+ Returns:
335
+ True if section(s) are dirty
336
+ """
337
+ if section:
338
+ return section in self._dirty_flags
339
+ return bool(self._dirty_flags)
340
+
341
+ def get_dirty_sections(self) -> Set[str]:
342
+ """
343
+ Get all dirty data sections.
344
+
345
+ Returns:
346
+ Set of dirty section names
347
+ """
348
+ return self._dirty_flags.copy()
349
+
350
+ def clear_dirty_flags(self) -> None:
351
+ """Clear all dirty flags."""
352
+ self._dirty_flags.clear()
353
+ logger.debug("Cleared all dirty flags")
354
+
355
+ def get_change_log(self) -> List[Dict[str, Any]]:
356
+ """
357
+ Get the change log.
358
+
359
+ Returns:
360
+ List of change entries
361
+ """
362
+ return self._change_log.copy()
363
+
364
+ def clear_change_log(self) -> None:
365
+ """Clear the change log."""
366
+ self._change_log.clear()
367
+ logger.debug("Cleared change log")
368
+
369
+ def _update_symbol_data_from_component(
370
+ self, symbol_data: Dict[str, Any], component: Component
371
+ ) -> None:
372
+ """Update symbol S-expression data from component object."""
373
+ # Update position and rotation
374
+ symbol_data["at"] = [component.position.x, component.position.y, component.rotation]
375
+
376
+ # Update lib_id
377
+ symbol_data["lib_id"] = component.lib_id
378
+
379
+ # Update properties
380
+ if "property" not in symbol_data:
381
+ symbol_data["property"] = []
382
+
383
+ properties = symbol_data["property"]
384
+
385
+ # Update existing properties and add new ones
386
+ property_names = {prop.get("name") for prop in properties}
387
+
388
+ for name, value in component.properties.items():
389
+ # Find existing property or create new one
390
+ existing_prop = None
391
+ for prop in properties:
392
+ if prop.get("name") == name:
393
+ existing_prop = prop
394
+ break
395
+
396
+ if existing_prop:
397
+ existing_prop["value"] = value
398
+ else:
399
+ new_prop = {
400
+ "name": name,
401
+ "value": value,
402
+ "at": [0, 0, 0], # Default position
403
+ "effects": {"font": {"size": [1.27, 1.27]}},
404
+ }
405
+ properties.append(new_prop)
406
+
407
+ def _update_wire_data_from_object(self, wire_data: Dict[str, Any], wire: Wire) -> None:
408
+ """Update wire S-expression data from wire object."""
409
+ # Update endpoints
410
+ wire_data["pts"] = [{"xy": [wire.start.x, wire.start.y]}, {"xy": [wire.end.x, wire.end.y]}]
411
+
412
+ # Update stroke
413
+ if "stroke" not in wire_data:
414
+ wire_data["stroke"] = {}
415
+
416
+ wire_data["stroke"]["width"] = wire.stroke_width
417
+
418
+ def _create_symbol_data_from_component(self, component: Component) -> Dict[str, Any]:
419
+ """Create S-expression data structure from component object."""
420
+ symbol_data = {
421
+ "lib_id": component.lib_id,
422
+ "at": [component.position.x, component.position.y, component.rotation],
423
+ "uuid": component.uuid,
424
+ "property": [],
425
+ }
426
+
427
+ # Add properties
428
+ for name, value in component.properties.items():
429
+ prop_data = {
430
+ "name": name,
431
+ "value": value,
432
+ "at": [0, 0, 0], # Default position relative to symbol
433
+ "effects": {"font": {"size": [1.27, 1.27]}},
434
+ }
435
+ symbol_data["property"].append(prop_data)
436
+
437
+ return symbol_data
438
+
439
+ def _create_wire_data_from_object(self, wire: Wire) -> Dict[str, Any]:
440
+ """Create S-expression data structure from wire object."""
441
+ wire_data = {
442
+ "pts": [{"xy": [wire.start.x, wire.start.y]}, {"xy": [wire.end.x, wire.end.y]}],
443
+ "stroke": {"width": wire.stroke_width, "type": "default"},
444
+ "uuid": wire.uuid,
445
+ }
446
+
447
+ return wire_data
448
+
449
+ def validate_data_consistency(
450
+ self, component_collection=None, wire_collection=None
451
+ ) -> List[str]:
452
+ """
453
+ Validate consistency between objects and S-expression data.
454
+
455
+ Args:
456
+ component_collection: Collection of components to validate
457
+ wire_collection: Collection of wires to validate
458
+
459
+ Returns:
460
+ List of consistency issues found
461
+ """
462
+ issues = []
463
+
464
+ # Validate components
465
+ if component_collection:
466
+ symbols = self._data.get("symbol", [])
467
+ symbol_uuids = {sym.get("uuid") for sym in symbols if sym.get("uuid")}
468
+ component_uuids = {comp.uuid for comp in component_collection}
469
+
470
+ # Check for missing symbols
471
+ missing_symbols = component_uuids - symbol_uuids
472
+ for uuid in missing_symbols:
473
+ issues.append(f"Component {uuid} missing from symbol data")
474
+
475
+ # Check for orphaned symbols
476
+ orphaned_symbols = symbol_uuids - component_uuids
477
+ for uuid in orphaned_symbols:
478
+ issues.append(f"Symbol {uuid} has no corresponding component object")
479
+
480
+ # Validate wires
481
+ if wire_collection:
482
+ wires = self._data.get("wire", [])
483
+ wire_data_uuids = {w.get("uuid") for w in wires if w.get("uuid")}
484
+ wire_object_uuids = {wire.uuid for wire in wire_collection}
485
+
486
+ # Check for missing wire data
487
+ missing_wire_data = wire_object_uuids - wire_data_uuids
488
+ for uuid in missing_wire_data:
489
+ issues.append(f"Wire {uuid} missing from wire data")
490
+
491
+ # Check for orphaned wire data
492
+ orphaned_wire_data = wire_data_uuids - wire_object_uuids
493
+ for uuid in orphaned_wire_data:
494
+ issues.append(f"Wire data {uuid} has no corresponding wire object")
495
+
496
+ if issues:
497
+ logger.warning(f"Found {len(issues)} data consistency issues")
498
+ else:
499
+ logger.debug("Data consistency validation passed")
500
+
501
+ return issues