kicad-sch-api 0.3.4__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 (34) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +296 -0
  3. kicad_sch_api/collections/components.py +422 -0
  4. kicad_sch_api/collections/junctions.py +378 -0
  5. kicad_sch_api/collections/labels.py +412 -0
  6. kicad_sch_api/collections/wires.py +407 -0
  7. kicad_sch_api/core/labels.py +348 -0
  8. kicad_sch_api/core/nets.py +310 -0
  9. kicad_sch_api/core/no_connects.py +274 -0
  10. kicad_sch_api/core/schematic.py +136 -2
  11. kicad_sch_api/core/texts.py +343 -0
  12. kicad_sch_api/core/types.py +12 -0
  13. kicad_sch_api/geometry/symbol_bbox.py +26 -32
  14. kicad_sch_api/interfaces/__init__.py +17 -0
  15. kicad_sch_api/interfaces/parser.py +76 -0
  16. kicad_sch_api/interfaces/repository.py +70 -0
  17. kicad_sch_api/interfaces/resolver.py +117 -0
  18. kicad_sch_api/parsers/__init__.py +14 -0
  19. kicad_sch_api/parsers/base.py +148 -0
  20. kicad_sch_api/parsers/label_parser.py +254 -0
  21. kicad_sch_api/parsers/registry.py +153 -0
  22. kicad_sch_api/parsers/symbol_parser.py +227 -0
  23. kicad_sch_api/parsers/wire_parser.py +99 -0
  24. kicad_sch_api/symbols/__init__.py +18 -0
  25. kicad_sch_api/symbols/cache.py +470 -0
  26. kicad_sch_api/symbols/resolver.py +367 -0
  27. kicad_sch_api/symbols/validators.py +453 -0
  28. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/METADATA +1 -1
  29. kicad_sch_api-0.3.5.dist-info/RECORD +58 -0
  30. kicad_sch_api-0.3.4.dist-info/RECORD +0 -34
  31. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/WHEEL +0 -0
  32. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/entry_points.txt +0 -0
  33. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/licenses/LICENSE +0 -0
  34. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,274 @@
1
+ """
2
+ No-connect element management for KiCAD schematics.
3
+
4
+ This module provides collection classes for managing no-connect elements,
5
+ featuring fast lookup, bulk operations, and validation.
6
+ """
7
+
8
+ import logging
9
+ import uuid
10
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
+
12
+ from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .types import Point, NoConnect
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class NoConnectElement:
19
+ """
20
+ Enhanced wrapper for schematic no-connect elements with modern API.
21
+
22
+ Provides intuitive access to no-connect properties and operations
23
+ while maintaining exact format preservation.
24
+ """
25
+
26
+ def __init__(self, no_connect_data: NoConnect, parent_collection: "NoConnectCollection"):
27
+ """
28
+ Initialize no-connect element wrapper.
29
+
30
+ Args:
31
+ no_connect_data: Underlying no-connect data
32
+ parent_collection: Parent collection for updates
33
+ """
34
+ self._data = no_connect_data
35
+ self._collection = parent_collection
36
+ self._validator = SchematicValidator()
37
+
38
+ # Core properties with validation
39
+ @property
40
+ def uuid(self) -> str:
41
+ """No-connect element UUID."""
42
+ return self._data.uuid
43
+
44
+ @property
45
+ def position(self) -> Point:
46
+ """No-connect position."""
47
+ return self._data.position
48
+
49
+ @position.setter
50
+ def position(self, value: Union[Point, Tuple[float, float]]):
51
+ """Set no-connect position."""
52
+ if isinstance(value, tuple):
53
+ value = Point(value[0], value[1])
54
+ elif not isinstance(value, Point):
55
+ raise ValidationError(f"Position must be Point or tuple, got {type(value)}")
56
+ self._data.position = value
57
+ self._collection._mark_modified()
58
+
59
+ def validate(self) -> List[ValidationIssue]:
60
+ """Validate this no-connect element."""
61
+ return self._validator.validate_no_connect(self._data.__dict__)
62
+
63
+ def to_dict(self) -> Dict[str, Any]:
64
+ """Convert no-connect element to dictionary representation."""
65
+ return {
66
+ "uuid": self.uuid,
67
+ "position": {"x": self.position.x, "y": self.position.y},
68
+ }
69
+
70
+ def __str__(self) -> str:
71
+ """String representation."""
72
+ return f"<NoConnect @ {self.position}>"
73
+
74
+
75
+ class NoConnectCollection:
76
+ """
77
+ Collection class for efficient no-connect element management.
78
+
79
+ Provides fast lookup, filtering, and bulk operations for schematic no-connect elements.
80
+ """
81
+
82
+ def __init__(self, no_connects: List[NoConnect] = None):
83
+ """
84
+ Initialize no-connect collection.
85
+
86
+ Args:
87
+ no_connects: Initial list of no-connect data
88
+ """
89
+ self._no_connects: List[NoConnectElement] = []
90
+ self._uuid_index: Dict[str, NoConnectElement] = {}
91
+ self._position_index: Dict[Tuple[float, float], List[NoConnectElement]] = {}
92
+ self._modified = False
93
+
94
+ # Add initial no-connects
95
+ if no_connects:
96
+ for no_connect_data in no_connects:
97
+ self._add_to_indexes(NoConnectElement(no_connect_data, self))
98
+
99
+ logger.debug(f"NoConnectCollection initialized with {len(self._no_connects)} no-connects")
100
+
101
+ def add(
102
+ self,
103
+ position: Union[Point, Tuple[float, float]],
104
+ no_connect_uuid: Optional[str] = None,
105
+ ) -> NoConnectElement:
106
+ """
107
+ Add a new no-connect element to the schematic.
108
+
109
+ Args:
110
+ position: No-connect position
111
+ no_connect_uuid: Specific UUID for no-connect (auto-generated if None)
112
+
113
+ Returns:
114
+ Newly created NoConnectElement
115
+
116
+ Raises:
117
+ ValidationError: If no-connect data is invalid
118
+ """
119
+ # Validate inputs
120
+ if isinstance(position, tuple):
121
+ position = Point(position[0], position[1])
122
+ elif not isinstance(position, Point):
123
+ raise ValidationError(f"Position must be Point or tuple, got {type(position)}")
124
+
125
+ # Generate UUID if not provided
126
+ if not no_connect_uuid:
127
+ no_connect_uuid = str(uuid.uuid4())
128
+
129
+ # Check for duplicate UUID
130
+ if no_connect_uuid in self._uuid_index:
131
+ raise ValidationError(f"NoConnect UUID {no_connect_uuid} already exists")
132
+
133
+ # Create no-connect data
134
+ no_connect_data = NoConnect(
135
+ uuid=no_connect_uuid,
136
+ position=position,
137
+ )
138
+
139
+ # Create wrapper and add to collection
140
+ no_connect_element = NoConnectElement(no_connect_data, self)
141
+ self._add_to_indexes(no_connect_element)
142
+ self._mark_modified()
143
+
144
+ logger.debug(f"Added no-connect: {no_connect_element}")
145
+ return no_connect_element
146
+
147
+ def get(self, no_connect_uuid: str) -> Optional[NoConnectElement]:
148
+ """Get no-connect by UUID."""
149
+ return self._uuid_index.get(no_connect_uuid)
150
+
151
+ def remove(self, no_connect_uuid: str) -> bool:
152
+ """
153
+ Remove no-connect by UUID.
154
+
155
+ Args:
156
+ no_connect_uuid: UUID of no-connect to remove
157
+
158
+ Returns:
159
+ True if no-connect was removed, False if not found
160
+ """
161
+ no_connect_element = self._uuid_index.get(no_connect_uuid)
162
+ if not no_connect_element:
163
+ return False
164
+
165
+ # Remove from indexes
166
+ self._remove_from_indexes(no_connect_element)
167
+ self._mark_modified()
168
+
169
+ logger.debug(f"Removed no-connect: {no_connect_element}")
170
+ return True
171
+
172
+ def find_at_position(self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.1) -> List[NoConnectElement]:
173
+ """
174
+ Find no-connects at or near a position.
175
+
176
+ Args:
177
+ position: Position to search around
178
+ tolerance: Search tolerance in mm
179
+
180
+ Returns:
181
+ List of matching no-connect elements
182
+ """
183
+ if isinstance(position, tuple):
184
+ position = Point(position[0], position[1])
185
+
186
+ matches = []
187
+ for no_connect_element in self._no_connects:
188
+ distance = no_connect_element.position.distance_to(position)
189
+ if distance <= tolerance:
190
+ matches.append(no_connect_element)
191
+ return matches
192
+
193
+ def filter(self, predicate: Callable[[NoConnectElement], bool]) -> List[NoConnectElement]:
194
+ """
195
+ Filter no-connects by predicate function.
196
+
197
+ Args:
198
+ predicate: Function that returns True for no-connects to include
199
+
200
+ Returns:
201
+ List of no-connects matching predicate
202
+ """
203
+ return [no_connect for no_connect in self._no_connects if predicate(no_connect)]
204
+
205
+ def bulk_update(self, criteria: Callable[[NoConnectElement], bool], updates: Dict[str, Any]):
206
+ """
207
+ Update multiple no-connects matching criteria.
208
+
209
+ Args:
210
+ criteria: Function to select no-connects to update
211
+ updates: Dictionary of property updates
212
+ """
213
+ updated_count = 0
214
+ for no_connect_element in self._no_connects:
215
+ if criteria(no_connect_element):
216
+ for prop, value in updates.items():
217
+ if hasattr(no_connect_element, prop):
218
+ setattr(no_connect_element, prop, value)
219
+ updated_count += 1
220
+
221
+ if updated_count > 0:
222
+ self._mark_modified()
223
+ logger.debug(f"Bulk updated {updated_count} no-connect properties")
224
+
225
+ def clear(self):
226
+ """Remove all no-connects from collection."""
227
+ self._no_connects.clear()
228
+ self._uuid_index.clear()
229
+ self._position_index.clear()
230
+ self._mark_modified()
231
+
232
+ def _add_to_indexes(self, no_connect_element: NoConnectElement):
233
+ """Add no-connect to internal indexes."""
234
+ self._no_connects.append(no_connect_element)
235
+ self._uuid_index[no_connect_element.uuid] = no_connect_element
236
+
237
+ # Add to position index
238
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
239
+ if pos_key not in self._position_index:
240
+ self._position_index[pos_key] = []
241
+ self._position_index[pos_key].append(no_connect_element)
242
+
243
+ def _remove_from_indexes(self, no_connect_element: NoConnectElement):
244
+ """Remove no-connect from internal indexes."""
245
+ self._no_connects.remove(no_connect_element)
246
+ del self._uuid_index[no_connect_element.uuid]
247
+
248
+ # Remove from position index
249
+ pos_key = (no_connect_element.position.x, no_connect_element.position.y)
250
+ if pos_key in self._position_index:
251
+ self._position_index[pos_key].remove(no_connect_element)
252
+ if not self._position_index[pos_key]:
253
+ del self._position_index[pos_key]
254
+
255
+ def _mark_modified(self):
256
+ """Mark collection as modified."""
257
+ self._modified = True
258
+
259
+ # Collection interface methods
260
+ def __len__(self) -> int:
261
+ """Return number of no-connects."""
262
+ return len(self._no_connects)
263
+
264
+ def __iter__(self) -> Iterator[NoConnectElement]:
265
+ """Iterate over no-connects."""
266
+ return iter(self._no_connects)
267
+
268
+ def __getitem__(self, index: int) -> NoConnectElement:
269
+ """Get no-connect by index."""
270
+ return self._no_connects[index]
271
+
272
+ def __bool__(self) -> bool:
273
+ """Return True if collection has no-connects."""
274
+ return len(self._no_connects) > 0
@@ -18,13 +18,18 @@ from ..utils.validation import SchematicValidator, ValidationError, ValidationIs
18
18
  from .components import ComponentCollection
19
19
  from .formatter import ExactFormatter
20
20
  from .junctions import JunctionCollection
21
+ from .labels import LabelCollection
22
+ from .nets import NetCollection
23
+ from .no_connects import NoConnectCollection
21
24
  from .parser import SExpressionParser
25
+ from .texts import TextCollection
22
26
  from .types import (
23
27
  HierarchicalLabelShape,
24
28
  Junction,
25
29
  Label,
26
30
  LabelType,
27
31
  Net,
32
+ NoConnect,
28
33
  Point,
29
34
  SchematicSymbol,
30
35
  Sheet,
@@ -136,6 +141,97 @@ class Schematic:
136
141
  junctions.append(junction)
137
142
  self._junctions = JunctionCollection(junctions)
138
143
 
144
+ # Initialize text collection
145
+ text_data = self._data.get("texts", [])
146
+ texts = []
147
+ for text_dict in text_data:
148
+ if isinstance(text_dict, dict):
149
+ # Convert dict to Text object
150
+ position = text_dict.get("position", {"x": 0, "y": 0})
151
+ if isinstance(position, dict):
152
+ pos = Point(position["x"], position["y"])
153
+ elif isinstance(position, (list, tuple)):
154
+ pos = Point(position[0], position[1])
155
+ else:
156
+ pos = position
157
+
158
+ text = Text(
159
+ uuid=text_dict.get("uuid", str(uuid.uuid4())),
160
+ position=pos,
161
+ text=text_dict.get("text", ""),
162
+ rotation=text_dict.get("rotation", 0.0),
163
+ size=text_dict.get("size", 1.27),
164
+ exclude_from_sim=text_dict.get("exclude_from_sim", False),
165
+ )
166
+ texts.append(text)
167
+ self._texts = TextCollection(texts)
168
+
169
+ # Initialize label collection
170
+ label_data = self._data.get("labels", [])
171
+ labels = []
172
+ for label_dict in label_data:
173
+ if isinstance(label_dict, dict):
174
+ # Convert dict to Label object
175
+ position = label_dict.get("position", {"x": 0, "y": 0})
176
+ if isinstance(position, dict):
177
+ pos = Point(position["x"], position["y"])
178
+ elif isinstance(position, (list, tuple)):
179
+ pos = Point(position[0], position[1])
180
+ else:
181
+ pos = position
182
+
183
+ label = Label(
184
+ uuid=label_dict.get("uuid", str(uuid.uuid4())),
185
+ position=pos,
186
+ text=label_dict.get("text", ""),
187
+ label_type=LabelType(label_dict.get("label_type", "local")),
188
+ rotation=label_dict.get("rotation", 0.0),
189
+ size=label_dict.get("size", 1.27),
190
+ shape=HierarchicalLabelShape(label_dict.get("shape")) if label_dict.get("shape") else None,
191
+ )
192
+ labels.append(label)
193
+ self._labels = LabelCollection(labels)
194
+
195
+ # Initialize hierarchical labels collection (filter from labels)
196
+ hierarchical_labels = [label for label in labels if label.label_type == LabelType.HIERARCHICAL]
197
+ self._hierarchical_labels = LabelCollection(hierarchical_labels)
198
+
199
+ # Initialize no-connect collection
200
+ no_connect_data = self._data.get("no_connects", [])
201
+ no_connects = []
202
+ for no_connect_dict in no_connect_data:
203
+ if isinstance(no_connect_dict, dict):
204
+ # Convert dict to NoConnect object
205
+ position = no_connect_dict.get("position", {"x": 0, "y": 0})
206
+ if isinstance(position, dict):
207
+ pos = Point(position["x"], position["y"])
208
+ elif isinstance(position, (list, tuple)):
209
+ pos = Point(position[0], position[1])
210
+ else:
211
+ pos = position
212
+
213
+ no_connect = NoConnect(
214
+ uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
215
+ position=pos,
216
+ )
217
+ no_connects.append(no_connect)
218
+ self._no_connects = NoConnectCollection(no_connects)
219
+
220
+ # Initialize net collection
221
+ net_data = self._data.get("nets", [])
222
+ nets = []
223
+ for net_dict in net_data:
224
+ if isinstance(net_dict, dict):
225
+ # Convert dict to Net object
226
+ net = Net(
227
+ name=net_dict.get("name", ""),
228
+ components=net_dict.get("components", []),
229
+ wires=net_dict.get("wires", []),
230
+ labels=net_dict.get("labels", []),
231
+ )
232
+ nets.append(net)
233
+ self._nets = NetCollection(nets)
234
+
139
235
  # Track modifications for save optimization
140
236
  self._modified = False
141
237
  self._last_save_time = None
@@ -145,7 +241,10 @@ class Schematic:
145
241
  self._total_operation_time = 0.0
146
242
 
147
243
  logger.debug(
148
- f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, and {len(self._junctions)} junctions"
244
+ f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, "
245
+ f"{len(self._junctions)} junctions, {len(self._texts)} texts, {len(self._labels)} labels, "
246
+ f"{len(self._hierarchical_labels)} hierarchical labels, {len(self._no_connects)} no-connects, "
247
+ f"and {len(self._nets)} nets"
149
248
  )
150
249
 
151
250
  @classmethod
@@ -276,7 +375,42 @@ class Schematic:
276
375
  @property
277
376
  def modified(self) -> bool:
278
377
  """Whether schematic has been modified since last save."""
279
- return self._modified or self._components._modified
378
+ return (
379
+ self._modified
380
+ or self._components._modified
381
+ or self._wires._modified
382
+ or self._junctions._modified
383
+ or self._texts._modified
384
+ or self._labels._modified
385
+ or self._hierarchical_labels._modified
386
+ or self._no_connects._modified
387
+ or self._nets._modified
388
+ )
389
+
390
+ @property
391
+ def texts(self) -> TextCollection:
392
+ """Collection of all text elements in the schematic."""
393
+ return self._texts
394
+
395
+ @property
396
+ def labels(self) -> LabelCollection:
397
+ """Collection of all label elements in the schematic."""
398
+ return self._labels
399
+
400
+ @property
401
+ def hierarchical_labels(self) -> LabelCollection:
402
+ """Collection of all hierarchical label elements in the schematic."""
403
+ return self._hierarchical_labels
404
+
405
+ @property
406
+ def no_connects(self) -> NoConnectCollection:
407
+ """Collection of all no-connect elements in the schematic."""
408
+ return self._no_connects
409
+
410
+ @property
411
+ def nets(self) -> NetCollection:
412
+ """Collection of all electrical nets in the schematic."""
413
+ return self._nets
280
414
 
281
415
  # Pin positioning methods (migrated from circuit-synth)
282
416
  def get_component_pin_position(self, reference: str, pin_number: str) -> Optional[Point]: