kicad-sch-api 0.4.1__py3-none-any.whl → 0.4.3__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/__init__.py CHANGED
@@ -42,7 +42,7 @@ Advanced Usage:
42
42
  print(f"Found {len(issues)} validation issues")
43
43
  """
44
44
 
45
- __version__ = "0.4.0"
45
+ __version__ = "0.4.3"
46
46
  __author__ = "Circuit-Synth"
47
47
  __email__ = "info@circuit-synth.com"
48
48
 
@@ -448,11 +448,25 @@ class ComponentCollection(BaseCollection[Component]):
448
448
  Remove component by reference.
449
449
 
450
450
  Args:
451
- reference: Component reference to remove
451
+ reference: Component reference to remove (e.g., "R1")
452
452
 
453
453
  Returns:
454
- True if component was removed
454
+ True if component was removed, False if not found
455
+
456
+ Raises:
457
+ TypeError: If reference is not a string
458
+
459
+ Examples:
460
+ sch.components.remove("R1")
461
+ sch.components.remove("C2")
462
+
463
+ Note:
464
+ For removing by UUID or component object, use remove_by_uuid() or remove_component()
465
+ respectively. This maintains a clear, simple API contract.
455
466
  """
467
+ if not isinstance(reference, str):
468
+ raise TypeError(f"reference must be a string, not {type(reference).__name__}")
469
+
456
470
  component = self._reference_index.get(reference)
457
471
  if not component:
458
472
  return False
@@ -466,6 +480,70 @@ class ComponentCollection(BaseCollection[Component]):
466
480
  logger.info(f"Removed component: {reference}")
467
481
  return True
468
482
 
483
+ def remove_by_uuid(self, component_uuid: str) -> bool:
484
+ """
485
+ Remove component by UUID.
486
+
487
+ Args:
488
+ component_uuid: Component UUID to remove
489
+
490
+ Returns:
491
+ True if component was removed, False if not found
492
+
493
+ Raises:
494
+ TypeError: If UUID is not a string
495
+ """
496
+ if not isinstance(component_uuid, str):
497
+ raise TypeError(f"component_uuid must be a string, not {type(component_uuid).__name__}")
498
+
499
+ if component_uuid not in self._uuid_index:
500
+ return False
501
+
502
+ component = self._items[self._uuid_index[component_uuid]]
503
+
504
+ # Remove from component-specific indexes
505
+ self._remove_from_indexes(component)
506
+
507
+ # Remove from base collection
508
+ super().remove(component_uuid)
509
+
510
+ logger.info(f"Removed component by UUID: {component_uuid}")
511
+ return True
512
+
513
+ def remove_component(self, component: "Component") -> bool:
514
+ """
515
+ Remove component by component object.
516
+
517
+ Args:
518
+ component: Component object to remove
519
+
520
+ Returns:
521
+ True if component was removed, False if not found
522
+
523
+ Raises:
524
+ TypeError: If component is not a Component instance
525
+
526
+ Examples:
527
+ comp = sch.components.get("R1")
528
+ sch.components.remove_component(comp)
529
+ """
530
+ if not isinstance(component, Component):
531
+ raise TypeError(
532
+ f"component must be a Component instance, not {type(component).__name__}"
533
+ )
534
+
535
+ if component.uuid not in self._uuid_index:
536
+ return False
537
+
538
+ # Remove from component-specific indexes
539
+ self._remove_from_indexes(component)
540
+
541
+ # Remove from base collection
542
+ super().remove(component.uuid)
543
+
544
+ logger.info(f"Removed component: {component.reference}")
545
+ return True
546
+
469
547
  def get(self, reference: str) -> Optional[Component]:
470
548
  """Get component by reference."""
471
549
  return self._reference_index.get(reference)
@@ -126,7 +126,7 @@ class ElementFactory:
126
126
  uuid=label_dict.get("uuid", str(uuid.uuid4())),
127
127
  position=pos,
128
128
  text=label_dict.get("text", ""),
129
- label_type=LabelType(label_dict.get("label_type", "local")),
129
+ label_type=LabelType(label_dict.get("label_type", "label")),
130
130
  rotation=label_dict.get("rotation", 0.0),
131
131
  size=label_dict.get("size", 1.27),
132
132
  shape=(
@@ -125,6 +125,10 @@ class ExactFormatter:
125
125
  self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
126
126
  self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
127
127
 
128
+ # Text elements
129
+ self.rules["text"] = FormatRule(inline=False, quote_indices={1})
130
+ self.rules["text_box"] = FormatRule(inline=False, quote_indices={1})
131
+
128
132
  # Effects and text formatting
129
133
  self.rules["effects"] = FormatRule(inline=False)
130
134
  self.rules["font"] = FormatRule(inline=False)
@@ -279,6 +283,8 @@ class ExactFormatter:
279
283
  "junction",
280
284
  "label",
281
285
  "hierarchical_label",
286
+ "text",
287
+ "text_box",
282
288
  "polyline",
283
289
  "rectangle",
284
290
  ):
@@ -384,7 +390,8 @@ class ExactFormatter:
384
390
  result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
385
391
  else:
386
392
  if i in rule.quote_indices and isinstance(element, str):
387
- result += f' "{element}"'
393
+ escaped_element = self._escape_string(element)
394
+ result += f' "{escaped_element}"'
388
395
  else:
389
396
  result += f" {self._format_element(element, 0)}"
390
397
 
@@ -396,7 +403,8 @@ class ExactFormatter:
396
403
  indent = "\t" * indent_level
397
404
  next_indent = "\t" * (indent_level + 1)
398
405
 
399
- result = f"({lst[0]}"
406
+ tag = str(lst[0])
407
+ result = f"({tag}"
400
408
 
401
409
  for i, element in enumerate(lst[1:], 1):
402
410
  if isinstance(element, list):
@@ -425,9 +433,18 @@ class ExactFormatter:
425
433
  return True
426
434
 
427
435
  def _escape_string(self, text: str) -> str:
428
- """Escape quotes in string for S-expression formatting."""
429
- # Replace double quotes with escaped quotes
430
- return text.replace('"', '\\"')
436
+ """Escape special characters in string for S-expression formatting."""
437
+ # Escape backslashes first (must be done before other replacements)
438
+ text = text.replace('\\', '\\\\')
439
+ # Escape double quotes
440
+ text = text.replace('"', '\\"')
441
+ # Escape newlines (convert actual newlines to escaped representation)
442
+ text = text.replace('\n', '\\n')
443
+ # Escape carriage returns
444
+ text = text.replace('\r', '\\r')
445
+ # Escape tabs
446
+ text = text.replace('\t', '\\t')
447
+ return text
431
448
 
432
449
  def _needs_quoting(self, text: str) -> bool:
433
450
  """Check if string needs to be quoted."""
@@ -0,0 +1,63 @@
1
+ """
2
+ Utility functions for parsing S-expression data.
3
+
4
+ This module contains helper functions used by various parsers
5
+ to handle common parsing patterns safely and consistently.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any
10
+
11
+ import sexpdata
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_bool_property(value: Any, default: bool = True) -> bool:
17
+ """
18
+ Parse a boolean property from S-expression data.
19
+
20
+ Handles both sexpdata.Symbol and string types, converting yes/no to bool.
21
+ This is the canonical way to parse boolean properties from KiCad files.
22
+
23
+ Args:
24
+ value: Value from S-expression (Symbol, str, bool, or None)
25
+ default: Default value if parsing fails or value is None
26
+
27
+ Returns:
28
+ bool: Parsed boolean value
29
+
30
+ Examples:
31
+ >>> parse_bool_property(sexpdata.Symbol('yes'))
32
+ True
33
+ >>> parse_bool_property('no')
34
+ False
35
+ >>> parse_bool_property(None, default=False)
36
+ False
37
+ >>> parse_bool_property('YES') # Case insensitive
38
+ True
39
+
40
+ Note:
41
+ This function was added to fix a critical bug where Symbol('yes') == 'yes'
42
+ returned False, causing properties like in_bom and on_board to be parsed
43
+ incorrectly.
44
+ """
45
+ # If value is None, use default
46
+ if value is None:
47
+ return default
48
+
49
+ # Convert Symbol to string
50
+ if isinstance(value, sexpdata.Symbol):
51
+ value = str(value)
52
+
53
+ # Handle string values (case-insensitive)
54
+ if isinstance(value, str):
55
+ return value.lower() == "yes"
56
+
57
+ # Handle boolean values directly
58
+ if isinstance(value, bool):
59
+ return value
60
+
61
+ # Unexpected type - use default
62
+ logger.warning(f"Unexpected type for boolean property: {type(value)}, using default={default}")
63
+ return default
@@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional
10
10
 
11
11
  import sexpdata
12
12
 
13
+ from ...core.parsing_utils import parse_bool_property
13
14
  from ...core.types import Point
14
15
  from ..base import BaseElementParser
15
16
 
@@ -74,9 +75,15 @@ class SymbolParser(BaseElementParser):
74
75
  prop_value = str(prop_value).replace('\\"', '"')
75
76
  symbol_data["properties"][prop_name] = prop_value
76
77
  elif element_type == "in_bom":
77
- symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
78
+ symbol_data["in_bom"] = parse_bool_property(
79
+ sub_item[1] if len(sub_item) > 1 else None,
80
+ default=True
81
+ )
78
82
  elif element_type == "on_board":
79
- symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
83
+ symbol_data["on_board"] = parse_bool_property(
84
+ sub_item[1] if len(sub_item) > 1 else None,
85
+ default=True
86
+ )
80
87
 
81
88
  return symbol_data
82
89
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kicad-sch-api
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Professional KiCAD schematic manipulation library with exact format preservation
5
5
  Author-email: Circuit-Synth <shane@circuit-synth.com>
6
6
  Maintainer-email: Circuit-Synth <shane@circuit-synth.com>
@@ -1,4 +1,4 @@
1
- kicad_sch_api/__init__.py,sha256=ofPkntVvSdgUnaguvhz6tMh556LfEnwxORpq3xPbj38,2919
1
+ kicad_sch_api/__init__.py,sha256=RqnJXbVVuA10zIozkuqnqv42nWBF-opcjChSAPne6nw,2919
2
2
  kicad_sch_api/cli.py,sha256=ZzmwzfHEvPgGfCiQBU4G2LBAyRtMNiBRoY21pivJSYc,7621
3
3
  kicad_sch_api/py.typed,sha256=e4ldqxwpY7pNDG1olbvj4HSKr8sZ9vxgA_2ek8xXn-Q,70
4
4
  kicad_sch_api/cli/__init__.py,sha256=mflSYoGLMJw2EFEByw9GD79pgj_Hebi6htJy7cKV5qM,952
@@ -16,9 +16,9 @@ kicad_sch_api/collections/labels.py,sha256=zg6_xe4ifwIbc8M1E5MDGTh8Ps57oBeqbS9Mc
16
16
  kicad_sch_api/collections/wires.py,sha256=o2Y_KIwOmFMytdOc2MjgnoUrK4Woj7wR9ROj4uRTuY0,12394
17
17
  kicad_sch_api/core/__init__.py,sha256=ur_KeYBlGKl-e1hLpLdxAhGV2A-PCCGkcqd0r6KSeBA,566
18
18
  kicad_sch_api/core/component_bounds.py,sha256=Qc-Fazq_RvASVZ57eGg99NahCIgnWq8FFg1J4HfggK0,17917
19
- kicad_sch_api/core/components.py,sha256=LUSIxpb8t2LD3bzpObxDguRkFz8VKQqdR53oDZiX8lg,26052
19
+ kicad_sch_api/core/components.py,sha256=powrqJGgqUBvMevMwhWSobrK6n8W525H7vzpPYCUXTs,28448
20
20
  kicad_sch_api/core/config.py,sha256=ECR6WLrC8i0itxRLJAZN3TDthbtOfd5NwNbiU6TSlW4,6224
21
- kicad_sch_api/core/formatter.py,sha256=RGN1Y1Ne5uBhKU4XRINVBb0ph6pm5Khycdv352r3cDw,22207
21
+ kicad_sch_api/core/formatter.py,sha256=ci-URhZFVHdSV4rEfOgTGtJu1tvW5MZEzSI1j2vtexE,22910
22
22
  kicad_sch_api/core/geometry.py,sha256=27SgN0padLbQuTi8MV6UUCp6Pyaiv8V9gmYDOhfwny8,2947
23
23
  kicad_sch_api/core/ic_manager.py,sha256=Kg0HIOMU-TGXiIkrnwcHFQ1Kfv_3rW2U1cwBKJsKopc,7219
24
24
  kicad_sch_api/core/junctions.py,sha256=HdEdaqF5zzFIWuRhJ2HJnVqPah4KJGNmeNv2KzgmFZQ,4757
@@ -26,6 +26,7 @@ kicad_sch_api/core/labels.py,sha256=roiwUrF-YWBOUMD-s2_eyL7x2efOhOde6WWoxI1b5JM,
26
26
  kicad_sch_api/core/nets.py,sha256=N1n6U7G7eXKRrc-osV48ReA6mOOLwn4TCwcNp2ISkNQ,9773
27
27
  kicad_sch_api/core/no_connects.py,sha256=6HCXzdVO124AdUUsQbCv9Y0f7KWmObkQ4OZLimYgET8,8548
28
28
  kicad_sch_api/core/parser.py,sha256=uvFVdSVzv5-RZmD_OKnqm51DIatFfsKF7NzLcWx4gfg,28761
29
+ kicad_sch_api/core/parsing_utils.py,sha256=tqQpuiNePNRx6Izsp9pnCBtlgiEYfMPkVFjkE5Uuofg,1776
29
30
  kicad_sch_api/core/pin_utils.py,sha256=XGEow3HzBTyT8a0B_ZC8foMvwzYaENSaqTUwDW1rz24,5417
30
31
  kicad_sch_api/core/schematic.py,sha256=W3URcKzjUrsTy3MfDejGuhuLmV2WM2YmFnGTihzGgt0,56489
31
32
  kicad_sch_api/core/texts.py,sha256=kBwUxwftqncl-yauiJjdnLXauV6EWq2oLhKw2bAxD_c,9990
@@ -34,7 +35,7 @@ kicad_sch_api/core/wires.py,sha256=lLqcpRodErD4fD4_VMB1HgC9ViPnYPGcT8b-N9S0q-g,7
34
35
  kicad_sch_api/core/collections/__init__.py,sha256=i75_p9v330S2hxi4LrAGLmcVjrF8O0nTlGjCCjNbkq0,118
35
36
  kicad_sch_api/core/collections/base.py,sha256=H-g1SC7hKD5QvbXPKVQotCK2e5pn04-y1ipkBQGcy3I,7076
36
37
  kicad_sch_api/core/factories/__init__.py,sha256=qFx_rAgcTgWRZ572hvzzKNAze-jE0BOLqLrSFzj7T6M,130
37
- kicad_sch_api/core/factories/element_factory.py,sha256=VbyBlEuIkVGkKf4soVIT8XUlq-T0-SJ_8w38JBfUH_k,8059
38
+ kicad_sch_api/core/factories/element_factory.py,sha256=zUmV1dnMXJl3fxXHdU9OJhqsHf_0D_DBOzWRLFYdG0c,8059
38
39
  kicad_sch_api/core/managers/__init__.py,sha256=Ec9H9RSBFt2MeJIhnZFUN9sa2ql0BSrO8FNOa1XsXeU,731
39
40
  kicad_sch_api/core/managers/file_io.py,sha256=k7yAfOvS5SqHrn1bCOH_wmOqDQnQe25wvhSPDmI8Cjk,7670
40
41
  kicad_sch_api/core/managers/format_sync.py,sha256=ar0FfhwXrU2l5ATV0aW5KtVRfIM2iM1WoyGvXQh-ShY,16963
@@ -65,7 +66,7 @@ kicad_sch_api/parsers/elements/label_parser.py,sha256=KL_AV_-BhA_KV1FLlqNpMj1Nu-
65
66
  kicad_sch_api/parsers/elements/library_parser.py,sha256=qHQMI3PatLgHtUWvWhQxbKC-NXJqgOVW33hVkMQ9sEU,6321
66
67
  kicad_sch_api/parsers/elements/metadata_parser.py,sha256=IFpgk5eLqp1kcjhpZB-jPThBCVyvdgsLo4YRydlvYD4,1897
67
68
  kicad_sch_api/parsers/elements/sheet_parser.py,sha256=xZld-yzW7r1FKABaK89K7U-Zenqz2cHUqtMnADhxCeQ,14246
68
- kicad_sch_api/parsers/elements/symbol_parser.py,sha256=nIKCZ67CLFHo6m62Xw2rLcOtjGLpyq0LhBdd0fPDiiY,12187
69
+ kicad_sch_api/parsers/elements/symbol_parser.py,sha256=G_N1kbTY2kCBG08gBHuXtmsFcw_dWMzwmVxOaO7F3Gw,12433
69
70
  kicad_sch_api/parsers/elements/text_parser.py,sha256=n5G_3czchmPCEdvTVoQsATTW1PQW7KcSjzglqzPlOKQ,9820
70
71
  kicad_sch_api/parsers/elements/wire_parser.py,sha256=geWI3jMXM7lZF26k7n7c6RQ55x4gsO4j8Ou0fk0O2j8,8271
71
72
  kicad_sch_api/symbols/__init__.py,sha256=NfakJ5-8AQxq5vi8nZVuaUtDpWHfwHm5AD4rC-p9BZI,501
@@ -79,9 +80,9 @@ kicad_sch_api/validation/erc.py,sha256=6WMHyMakKpn1-jXkfO9ltayLRA0OaU1N7GQm-pbZT
79
80
  kicad_sch_api/validation/erc_models.py,sha256=wFYMH-cbcdRX1j9LPn28IGUYBLioPDr4lP0m2QuhAnI,6628
80
81
  kicad_sch_api/validation/pin_matrix.py,sha256=AdBCvEmOByXACOUoskYxYMcwKvIasxrtJOqWM-WgVi0,7456
81
82
  kicad_sch_api/validation/validators.py,sha256=3txkQpR3qAUzp0-m-FnDYf3-_GGj6iaGZj4kSYu-Fe4,13166
82
- kicad_sch_api-0.4.1.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
83
- kicad_sch_api-0.4.1.dist-info/METADATA,sha256=y4meDeK6RX_G6PCcQ7pIBw-pgKZv2NzG8kvMGsEusQw,17953
84
- kicad_sch_api-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
- kicad_sch_api-0.4.1.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
86
- kicad_sch_api-0.4.1.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
87
- kicad_sch_api-0.4.1.dist-info/RECORD,,
83
+ kicad_sch_api-0.4.3.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
84
+ kicad_sch_api-0.4.3.dist-info/METADATA,sha256=W-b-H9K_70U5sUjAQgu0nbocWOElYs8mHIH1I_omOiw,17953
85
+ kicad_sch_api-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
86
+ kicad_sch_api-0.4.3.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
87
+ kicad_sch_api-0.4.3.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
88
+ kicad_sch_api-0.4.3.dist-info/RECORD,,