kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__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 (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ import sexpdata
17
17
  from ..library.cache import get_symbol_cache
18
18
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
19
19
  from .components import ComponentCollection
20
+ from .factories import ElementFactory
20
21
  from .formatter import ExactFormatter
21
22
  from .junctions import JunctionCollection
22
23
  from .labels import LabelCollection
@@ -49,6 +50,7 @@ from .types import (
49
50
  TitleBlock,
50
51
  Wire,
51
52
  WireType,
53
+ point_from_dict_or_tuple,
52
54
  )
53
55
  from .wires import WireCollection
54
56
 
@@ -107,105 +109,22 @@ class Schematic:
107
109
 
108
110
  # Initialize wire collection
109
111
  wire_data = self._data.get("wires", [])
110
- wires = []
111
- for wire_dict in wire_data:
112
- if isinstance(wire_dict, dict):
113
- # Convert dict to Wire object
114
- points = []
115
- for point_data in wire_dict.get("points", []):
116
- if isinstance(point_data, dict):
117
- points.append(Point(point_data["x"], point_data["y"]))
118
- elif isinstance(point_data, (list, tuple)):
119
- points.append(Point(point_data[0], point_data[1]))
120
- else:
121
- points.append(point_data)
122
-
123
- wire = Wire(
124
- uuid=wire_dict.get("uuid", str(uuid.uuid4())),
125
- points=points,
126
- wire_type=WireType(wire_dict.get("wire_type", "wire")),
127
- stroke_width=wire_dict.get("stroke_width", 0.0),
128
- stroke_type=wire_dict.get("stroke_type", "default"),
129
- )
130
- wires.append(wire)
112
+ wires = ElementFactory.create_wires_from_list(wire_data)
131
113
  self._wires = WireCollection(wires)
132
114
 
133
115
  # Initialize junction collection
134
116
  junction_data = self._data.get("junctions", [])
135
- junctions = []
136
- for junction_dict in junction_data:
137
- if isinstance(junction_dict, dict):
138
- # Convert dict to Junction object
139
- position = junction_dict.get("position", {"x": 0, "y": 0})
140
- if isinstance(position, dict):
141
- pos = Point(position["x"], position["y"])
142
- elif isinstance(position, (list, tuple)):
143
- pos = Point(position[0], position[1])
144
- else:
145
- pos = position
146
-
147
- junction = Junction(
148
- uuid=junction_dict.get("uuid", str(uuid.uuid4())),
149
- position=pos,
150
- diameter=junction_dict.get("diameter", 0),
151
- color=junction_dict.get("color", (0, 0, 0, 0)),
152
- )
153
- junctions.append(junction)
117
+ junctions = ElementFactory.create_junctions_from_list(junction_data)
154
118
  self._junctions = JunctionCollection(junctions)
155
119
 
156
120
  # Initialize text collection
157
121
  text_data = self._data.get("texts", [])
158
- texts = []
159
- for text_dict in text_data:
160
- if isinstance(text_dict, dict):
161
- # Convert dict to Text object
162
- position = text_dict.get("position", {"x": 0, "y": 0})
163
- if isinstance(position, dict):
164
- pos = Point(position["x"], position["y"])
165
- elif isinstance(position, (list, tuple)):
166
- pos = Point(position[0], position[1])
167
- else:
168
- pos = position
169
-
170
- text = Text(
171
- uuid=text_dict.get("uuid", str(uuid.uuid4())),
172
- position=pos,
173
- text=text_dict.get("text", ""),
174
- rotation=text_dict.get("rotation", 0.0),
175
- size=text_dict.get("size", 1.27),
176
- exclude_from_sim=text_dict.get("exclude_from_sim", False),
177
- )
178
- texts.append(text)
122
+ texts = ElementFactory.create_texts_from_list(text_data)
179
123
  self._texts = TextCollection(texts)
180
124
 
181
125
  # Initialize label collection
182
126
  label_data = self._data.get("labels", [])
183
- labels = []
184
- for label_dict in label_data:
185
- if isinstance(label_dict, dict):
186
- # Convert dict to Label object
187
- position = label_dict.get("position", {"x": 0, "y": 0})
188
- if isinstance(position, dict):
189
- pos = Point(position["x"], position["y"])
190
- elif isinstance(position, (list, tuple)):
191
- pos = Point(position[0], position[1])
192
- else:
193
- pos = position
194
-
195
- label = Label(
196
- uuid=label_dict.get("uuid", str(uuid.uuid4())),
197
- position=pos,
198
- text=label_dict.get("text", ""),
199
- label_type=LabelType(label_dict.get("label_type", "local")),
200
- rotation=label_dict.get("rotation", 0.0),
201
- size=label_dict.get("size", 1.27),
202
- shape=(
203
- HierarchicalLabelShape(label_dict.get("shape"))
204
- if label_dict.get("shape")
205
- else None
206
- ),
207
- )
208
- labels.append(label)
127
+ labels = ElementFactory.create_labels_from_list(label_data)
209
128
  self._labels = LabelCollection(labels)
210
129
 
211
130
  # Initialize hierarchical labels collection (from both labels array and hierarchical_labels array)
@@ -215,68 +134,18 @@ class Schematic:
215
134
 
216
135
  # Also load from hierarchical_labels data if present
217
136
  hierarchical_label_data = self._data.get("hierarchical_labels", [])
218
- for hlabel_dict in hierarchical_label_data:
219
- if isinstance(hlabel_dict, dict):
220
- # Convert dict to Label object
221
- position = hlabel_dict.get("position", {"x": 0, "y": 0})
222
- if isinstance(position, dict):
223
- pos = Point(position["x"], position["y"])
224
- elif isinstance(position, (list, tuple)):
225
- pos = Point(position[0], position[1])
226
- else:
227
- pos = position
228
-
229
- hlabel = Label(
230
- uuid=hlabel_dict.get("uuid", str(uuid.uuid4())),
231
- position=pos,
232
- text=hlabel_dict.get("text", ""),
233
- label_type=LabelType.HIERARCHICAL,
234
- rotation=hlabel_dict.get("rotation", 0.0),
235
- size=hlabel_dict.get("size", 1.27),
236
- shape=(
237
- HierarchicalLabelShape(hlabel_dict.get("shape"))
238
- if hlabel_dict.get("shape")
239
- else None
240
- ),
241
- )
242
- hierarchical_labels.append(hlabel)
137
+ hierarchical_labels.extend(ElementFactory.create_labels_from_list(hierarchical_label_data))
243
138
 
244
139
  self._hierarchical_labels = LabelCollection(hierarchical_labels)
245
140
 
246
141
  # Initialize no-connect collection
247
142
  no_connect_data = self._data.get("no_connects", [])
248
- no_connects = []
249
- for no_connect_dict in no_connect_data:
250
- if isinstance(no_connect_dict, dict):
251
- # Convert dict to NoConnect object
252
- position = no_connect_dict.get("position", {"x": 0, "y": 0})
253
- if isinstance(position, dict):
254
- pos = Point(position["x"], position["y"])
255
- elif isinstance(position, (list, tuple)):
256
- pos = Point(position[0], position[1])
257
- else:
258
- pos = position
259
-
260
- no_connect = NoConnect(
261
- uuid=no_connect_dict.get("uuid", str(uuid.uuid4())),
262
- position=pos,
263
- )
264
- no_connects.append(no_connect)
143
+ no_connects = ElementFactory.create_no_connects_from_list(no_connect_data)
265
144
  self._no_connects = NoConnectCollection(no_connects)
266
145
 
267
146
  # Initialize net collection
268
147
  net_data = self._data.get("nets", [])
269
- nets = []
270
- for net_dict in net_data:
271
- if isinstance(net_dict, dict):
272
- # Convert dict to Net object
273
- net = Net(
274
- name=net_dict.get("name", ""),
275
- components=net_dict.get("components", []),
276
- wires=net_dict.get("wires", []),
277
- labels=net_dict.get("labels", []),
278
- )
279
- nets.append(net)
148
+ nets = ElementFactory.create_nets_from_list(net_data)
280
149
  self._nets = NetCollection(nets)
281
150
 
282
151
  # Initialize specialized managers
@@ -337,10 +206,10 @@ class Schematic:
337
206
  def create(
338
207
  cls,
339
208
  name: str = "Untitled",
340
- version: str = "20250114",
341
- generator: str = "eeschema",
342
- generator_version: str = "9.0",
343
- paper: str = "A4",
209
+ version: str = None,
210
+ generator: str = None,
211
+ generator_version: str = None,
212
+ paper: str = None,
344
213
  uuid: str = None,
345
214
  ) -> "Schematic":
346
215
  """
@@ -348,15 +217,23 @@ class Schematic:
348
217
 
349
218
  Args:
350
219
  name: Schematic name
351
- version: KiCAD version (default: "20250114")
352
- generator: Generator name (default: "eeschema")
353
- generator_version: Generator version (default: "9.0")
354
- paper: Paper size (default: "A4")
220
+ version: KiCAD version (default from config)
221
+ generator: Generator name (default from config)
222
+ generator_version: Generator version (default from config)
223
+ paper: Paper size (default from config)
355
224
  uuid: Specific UUID (auto-generated if None)
356
225
 
357
226
  Returns:
358
227
  New empty Schematic object
359
228
  """
229
+ # Apply config defaults for None values
230
+ from .config import config
231
+
232
+ version = version or config.file_format.version_default
233
+ generator = generator or config.file_format.generator_default
234
+ generator_version = generator_version or config.file_format.generator_version_default
235
+ paper = paper or config.paper.default
236
+
360
237
  # Special handling for blank schematic test case to match reference exactly
361
238
  if name == "Blank Schematic":
362
239
  schematic_data = {
@@ -1542,6 +1419,192 @@ class Schematic:
1542
1419
  # Recursively check nested elements
1543
1420
  self._update_project_in_instances(element)
1544
1421
 
1422
+ # ============================================================================
1423
+ # Export Methods (using kicad-cli)
1424
+ # ============================================================================
1425
+
1426
+ def run_erc(self, **kwargs):
1427
+ """
1428
+ Run Electrical Rule Check (ERC) on this schematic.
1429
+
1430
+ This requires the schematic to be saved first.
1431
+
1432
+ Args:
1433
+ **kwargs: Arguments passed to cli.erc.run_erc()
1434
+ - output_path: Path for ERC report
1435
+ - format: 'json' or 'report'
1436
+ - severity: 'all', 'error', 'warning', 'exclusions'
1437
+ - units: 'mm', 'in', 'mils'
1438
+
1439
+ Returns:
1440
+ ErcReport with violations and summary
1441
+
1442
+ Example:
1443
+ >>> report = sch.run_erc()
1444
+ >>> if report.has_errors():
1445
+ ... print(f"Found {report.error_count} errors")
1446
+ """
1447
+ from kicad_sch_api.cli.erc import run_erc
1448
+
1449
+ if not self._file_path:
1450
+ raise ValueError("Schematic must be saved before running ERC")
1451
+
1452
+ # Save first to ensure file is up-to-date
1453
+ self.save()
1454
+
1455
+ return run_erc(self._file_path, **kwargs)
1456
+
1457
+ def export_netlist(self, format="kicadsexpr", **kwargs):
1458
+ """
1459
+ Export netlist from this schematic.
1460
+
1461
+ This requires the schematic to be saved first.
1462
+
1463
+ Args:
1464
+ format: Netlist format (default: 'kicadsexpr')
1465
+ - kicadsexpr: KiCad S-expression (default)
1466
+ - kicadxml: KiCad XML
1467
+ - spice: SPICE netlist
1468
+ - spicemodel: SPICE with models
1469
+ - cadstar, orcadpcb2, pads, allegro
1470
+ **kwargs: Arguments passed to cli.netlist.export_netlist()
1471
+
1472
+ Returns:
1473
+ Path to generated netlist file
1474
+
1475
+ Example:
1476
+ >>> netlist = sch.export_netlist(format='spice')
1477
+ >>> print(f"Netlist: {netlist}")
1478
+ """
1479
+ from kicad_sch_api.cli.netlist import export_netlist
1480
+
1481
+ if not self._file_path:
1482
+ raise ValueError("Schematic must be saved before exporting netlist")
1483
+
1484
+ # Save first to ensure file is up-to-date
1485
+ self.save()
1486
+
1487
+ return export_netlist(self._file_path, format=format, **kwargs)
1488
+
1489
+ def export_bom(self, **kwargs):
1490
+ """
1491
+ Export Bill of Materials (BOM) from this schematic.
1492
+
1493
+ This requires the schematic to be saved first.
1494
+
1495
+ Args:
1496
+ **kwargs: Arguments passed to cli.bom.export_bom()
1497
+ - output_path: Path for BOM file
1498
+ - fields: List of fields to export
1499
+ - group_by: Fields to group by
1500
+ - exclude_dnp: Exclude Do-Not-Populate components
1501
+ - And many more options...
1502
+
1503
+ Returns:
1504
+ Path to generated BOM file
1505
+
1506
+ Example:
1507
+ >>> bom = sch.export_bom(
1508
+ ... fields=['Reference', 'Value', 'Footprint', 'MPN'],
1509
+ ... group_by=['Value', 'Footprint'],
1510
+ ... exclude_dnp=True,
1511
+ ... )
1512
+ """
1513
+ from kicad_sch_api.cli.bom import export_bom
1514
+
1515
+ if not self._file_path:
1516
+ raise ValueError("Schematic must be saved before exporting BOM")
1517
+
1518
+ # Save first to ensure file is up-to-date
1519
+ self.save()
1520
+
1521
+ return export_bom(self._file_path, **kwargs)
1522
+
1523
+ def export_pdf(self, **kwargs):
1524
+ """
1525
+ Export schematic as PDF.
1526
+
1527
+ This requires the schematic to be saved first.
1528
+
1529
+ Args:
1530
+ **kwargs: Arguments passed to cli.export_docs.export_pdf()
1531
+ - output_path: Path for PDF file
1532
+ - theme: Color theme
1533
+ - black_and_white: B&W export
1534
+ - And more options...
1535
+
1536
+ Returns:
1537
+ Path to generated PDF file
1538
+
1539
+ Example:
1540
+ >>> pdf = sch.export_pdf(theme='Kicad Classic')
1541
+ """
1542
+ from kicad_sch_api.cli.export_docs import export_pdf
1543
+
1544
+ if not self._file_path:
1545
+ raise ValueError("Schematic must be saved before exporting PDF")
1546
+
1547
+ # Save first to ensure file is up-to-date
1548
+ self.save()
1549
+
1550
+ return export_pdf(self._file_path, **kwargs)
1551
+
1552
+ def export_svg(self, **kwargs):
1553
+ """
1554
+ Export schematic as SVG.
1555
+
1556
+ This requires the schematic to be saved first.
1557
+
1558
+ Args:
1559
+ **kwargs: Arguments passed to cli.export_docs.export_svg()
1560
+ - output_dir: Output directory
1561
+ - theme: Color theme
1562
+ - black_and_white: B&W export
1563
+ - And more options...
1564
+
1565
+ Returns:
1566
+ List of paths to generated SVG files
1567
+
1568
+ Example:
1569
+ >>> svgs = sch.export_svg()
1570
+ >>> for svg in svgs:
1571
+ ... print(f"Generated: {svg}")
1572
+ """
1573
+ from kicad_sch_api.cli.export_docs import export_svg
1574
+
1575
+ if not self._file_path:
1576
+ raise ValueError("Schematic must be saved before exporting SVG")
1577
+
1578
+ # Save first to ensure file is up-to-date
1579
+ self.save()
1580
+
1581
+ return export_svg(self._file_path, **kwargs)
1582
+
1583
+ def export_dxf(self, **kwargs):
1584
+ """
1585
+ Export schematic as DXF.
1586
+
1587
+ This requires the schematic to be saved first.
1588
+
1589
+ Args:
1590
+ **kwargs: Arguments passed to cli.export_docs.export_dxf()
1591
+
1592
+ Returns:
1593
+ List of paths to generated DXF files
1594
+
1595
+ Example:
1596
+ >>> dxfs = sch.export_dxf()
1597
+ """
1598
+ from kicad_sch_api.cli.export_docs import export_dxf
1599
+
1600
+ if not self._file_path:
1601
+ raise ValueError("Schematic must be saved before exporting DXF")
1602
+
1603
+ # Save first to ensure file is up-to-date
1604
+ self.save()
1605
+
1606
+ return export_dxf(self._file_path, **kwargs)
1607
+
1545
1608
  def __str__(self) -> str:
1546
1609
  """String representation."""
1547
1610
  title = self.title_block.get("title", "Untitled")
@@ -10,6 +10,7 @@ import uuid
10
10
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
+ from .collections import BaseCollection
13
14
  from .types import Point, Text
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -124,10 +125,13 @@ class TextElement:
124
125
  return f"<Text '{self.text}' @ {self.position}>"
125
126
 
126
127
 
127
- class TextCollection:
128
+ class TextCollection(BaseCollection[TextElement]):
128
129
  """
129
130
  Collection class for efficient text element management.
130
131
 
132
+ Inherits from BaseCollection for standard operations and adds text-specific
133
+ functionality including content-based indexing.
134
+
131
135
  Provides fast lookup, filtering, and bulk operations for schematic text elements.
132
136
  """
133
137
 
@@ -138,18 +142,17 @@ class TextCollection:
138
142
  Args:
139
143
  texts: Initial list of text data
140
144
  """
141
- self._texts: List[TextElement] = []
142
- self._uuid_index: Dict[str, TextElement] = {}
145
+ # Initialize base collection with empty list (we'll add elements below)
146
+ super().__init__([], collection_name="texts")
147
+
148
+ # Additional text-specific index
143
149
  self._content_index: Dict[str, List[TextElement]] = {}
144
- self._modified = False
145
150
 
146
151
  # Add initial texts
147
152
  if texts:
148
153
  for text_data in texts:
149
154
  self._add_to_indexes(TextElement(text_data, self))
150
155
 
151
- logger.debug(f"TextCollection initialized with {len(self._texts)} texts")
152
-
153
156
  def add(
154
157
  self,
155
158
  text: str,
@@ -209,15 +212,10 @@ class TextCollection:
209
212
  # Create wrapper and add to collection
210
213
  text_element = TextElement(text_data, self)
211
214
  self._add_to_indexes(text_element)
212
- self._mark_modified()
213
215
 
214
216
  logger.debug(f"Added text: {text_element}")
215
217
  return text_element
216
218
 
217
- def get(self, text_uuid: str) -> Optional[TextElement]:
218
- """Get text by UUID."""
219
- return self._uuid_index.get(text_uuid)
220
-
221
219
  def remove(self, text_uuid: str) -> bool:
222
220
  """
223
221
  Remove text by UUID.
@@ -228,13 +226,19 @@ class TextCollection:
228
226
  Returns:
229
227
  True if text was removed, False if not found
230
228
  """
231
- text_element = self._uuid_index.get(text_uuid)
229
+ text_element = self.get(text_uuid)
232
230
  if not text_element:
233
231
  return False
234
232
 
235
- # Remove from indexes
236
- self._remove_from_indexes(text_element)
237
- self._mark_modified()
233
+ # Remove from content index
234
+ content = text_element.text
235
+ if content in self._content_index:
236
+ self._content_index[content].remove(text_element)
237
+ if not self._content_index[content]:
238
+ del self._content_index[content]
239
+
240
+ # Remove using base class method
241
+ super().remove(text_uuid)
238
242
 
239
243
  logger.debug(f"Removed text: {text_element}")
240
244
  return True
@@ -254,14 +258,14 @@ class TextCollection:
254
258
  return self._content_index.get(content, []).copy()
255
259
  else:
256
260
  matches = []
257
- for text_element in self._texts:
261
+ for text_element in self._items:
258
262
  if content.lower() in text_element.text.lower():
259
263
  matches.append(text_element)
260
264
  return matches
261
265
 
262
266
  def filter(self, predicate: Callable[[TextElement], bool]) -> List[TextElement]:
263
267
  """
264
- Filter texts by predicate function.
268
+ Filter texts by predicate function (delegates to base class find).
265
269
 
266
270
  Args:
267
271
  predicate: Function that returns True for texts to include
@@ -269,7 +273,7 @@ class TextCollection:
269
273
  Returns:
270
274
  List of texts matching predicate
271
275
  """
272
- return [text for text in self._texts if predicate(text)]
276
+ return self.find(predicate)
273
277
 
274
278
  def bulk_update(self, criteria: Callable[[TextElement], bool], updates: Dict[str, Any]):
275
279
  """
@@ -280,7 +284,7 @@ class TextCollection:
280
284
  updates: Dictionary of property updates
281
285
  """
282
286
  updated_count = 0
283
- for text_element in self._texts:
287
+ for text_element in self._items:
284
288
  if criteria(text_element):
285
289
  for prop, value in updates.items():
286
290
  if hasattr(text_element, prop):
@@ -293,15 +297,12 @@ class TextCollection:
293
297
 
294
298
  def clear(self):
295
299
  """Remove all texts from collection."""
296
- self._texts.clear()
297
- self._uuid_index.clear()
298
300
  self._content_index.clear()
299
- self._mark_modified()
301
+ super().clear()
300
302
 
301
303
  def _add_to_indexes(self, text_element: TextElement):
302
- """Add text to internal indexes."""
303
- self._texts.append(text_element)
304
- self._uuid_index[text_element.uuid] = text_element
304
+ """Add text to internal indexes (base + content index)."""
305
+ self._add_item(text_element)
305
306
 
306
307
  # Add to content index
307
308
  content = text_element.text
@@ -309,35 +310,7 @@ class TextCollection:
309
310
  self._content_index[content] = []
310
311
  self._content_index[content].append(text_element)
311
312
 
312
- def _remove_from_indexes(self, text_element: TextElement):
313
- """Remove text from internal indexes."""
314
- self._texts.remove(text_element)
315
- del self._uuid_index[text_element.uuid]
316
-
317
- # Remove from content index
318
- content = text_element.text
319
- if content in self._content_index:
320
- self._content_index[content].remove(text_element)
321
- if not self._content_index[content]:
322
- del self._content_index[content]
323
-
324
- def _mark_modified(self):
325
- """Mark collection as modified."""
326
- self._modified = True
327
-
328
- # Collection interface methods
329
- def __len__(self) -> int:
330
- """Return number of texts."""
331
- return len(self._texts)
332
-
333
- def __iter__(self) -> Iterator[TextElement]:
334
- """Iterate over texts."""
335
- return iter(self._texts)
336
-
337
- def __getitem__(self, index: int) -> TextElement:
338
- """Get text by index."""
339
- return self._texts[index]
340
-
313
+ # Collection interface methods - __len__, __iter__, __getitem__ inherited from BaseCollection
341
314
  def __bool__(self) -> bool:
342
315
  """Return True if collection has texts."""
343
- return len(self._texts) > 0
316
+ return len(self._items) > 0