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.

Files changed (47) hide show
  1. kicad_sch_api/collections/__init__.py +21 -0
  2. kicad_sch_api/collections/base.py +294 -0
  3. kicad_sch_api/collections/components.py +434 -0
  4. kicad_sch_api/collections/junctions.py +366 -0
  5. kicad_sch_api/collections/labels.py +404 -0
  6. kicad_sch_api/collections/wires.py +406 -0
  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 +348 -0
  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 +310 -0
  20. kicad_sch_api/core/no_connects.py +276 -0
  21. kicad_sch_api/core/parser.py +75 -41
  22. kicad_sch_api/core/schematic.py +904 -1074
  23. kicad_sch_api/core/texts.py +343 -0
  24. kicad_sch_api/core/types.py +13 -4
  25. kicad_sch_api/geometry/font_metrics.py +3 -1
  26. kicad_sch_api/geometry/symbol_bbox.py +56 -43
  27. kicad_sch_api/interfaces/__init__.py +17 -0
  28. kicad_sch_api/interfaces/parser.py +76 -0
  29. kicad_sch_api/interfaces/repository.py +70 -0
  30. kicad_sch_api/interfaces/resolver.py +117 -0
  31. kicad_sch_api/parsers/__init__.py +14 -0
  32. kicad_sch_api/parsers/base.py +145 -0
  33. kicad_sch_api/parsers/label_parser.py +254 -0
  34. kicad_sch_api/parsers/registry.py +155 -0
  35. kicad_sch_api/parsers/symbol_parser.py +222 -0
  36. kicad_sch_api/parsers/wire_parser.py +99 -0
  37. kicad_sch_api/symbols/__init__.py +18 -0
  38. kicad_sch_api/symbols/cache.py +467 -0
  39. kicad_sch_api/symbols/resolver.py +361 -0
  40. kicad_sch_api/symbols/validators.py +504 -0
  41. {kicad_sch_api-0.3.4.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.4.dist-info/RECORD +0 -34
  44. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/WHEEL +0 -0
  45. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/entry_points.txt +0 -0
  46. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/licenses/LICENSE +0 -0
  47. {kicad_sch_api-0.3.4.dist-info → kicad_sch_api-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,155 @@
1
+ """
2
+ Parser registry for managing S-expression element parsers.
3
+
4
+ Provides a central registry for all element parsers and handles
5
+ dispatching parsing requests to the appropriate parser.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from ..interfaces.parser import IElementParser
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ElementParserRegistry:
17
+ """
18
+ Central registry for all S-expression element parsers.
19
+
20
+ This class manages the registration of element-specific parsers
21
+ and provides a unified interface for parsing any S-expression element.
22
+ """
23
+
24
+ def __init__(self):
25
+ """Initialize the parser registry."""
26
+ self._parsers: Dict[str, IElementParser] = {}
27
+ self._fallback_parser: Optional[IElementParser] = None
28
+ self._logger = logger.getChild(self.__class__.__name__)
29
+
30
+ def register(self, element_type: str, parser: IElementParser) -> None:
31
+ """
32
+ Register a parser for a specific element type.
33
+
34
+ Args:
35
+ element_type: The S-expression element type (e.g., "symbol", "wire")
36
+ parser: Parser instance that handles this element type
37
+
38
+ Raises:
39
+ ValueError: If element_type is already registered
40
+ """
41
+ if element_type in self._parsers:
42
+ self._logger.warning(f"Overriding existing parser for element type: {element_type}")
43
+
44
+ self._parsers[element_type] = parser
45
+ self._logger.debug(f"Registered parser for element type: {element_type}")
46
+
47
+ def unregister(self, element_type: str) -> bool:
48
+ """
49
+ Unregister a parser for a specific element type.
50
+
51
+ Args:
52
+ element_type: The element type to unregister
53
+
54
+ Returns:
55
+ True if parser was removed, False if not found
56
+ """
57
+ if element_type in self._parsers:
58
+ del self._parsers[element_type]
59
+ self._logger.debug(f"Unregistered parser for element type: {element_type}")
60
+ return True
61
+ return False
62
+
63
+ def set_fallback_parser(self, parser: IElementParser) -> None:
64
+ """
65
+ Set a fallback parser for unknown element types.
66
+
67
+ Args:
68
+ parser: Parser to use when no specific parser is registered
69
+ """
70
+ self._fallback_parser = parser
71
+ self._logger.debug("Set fallback parser")
72
+
73
+ def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
74
+ """
75
+ Parse an S-expression element using the appropriate registered parser.
76
+
77
+ Args:
78
+ element: S-expression element to parse
79
+
80
+ Returns:
81
+ Parsed element data or None if parsing failed
82
+ """
83
+ if not element or not isinstance(element, list):
84
+ self._logger.debug("Invalid element: not a list or empty")
85
+ return None
86
+
87
+ element_type = element[0] if element else None
88
+ # Convert sexpdata.Symbol to string for lookup
89
+ element_type_str = str(element_type) if element_type else None
90
+ if not element_type_str:
91
+ self._logger.debug(f"Invalid element type: {element_type}")
92
+ return None
93
+
94
+ # Try specific parser first
95
+ parser = self._parsers.get(element_type_str)
96
+ if parser:
97
+ self._logger.debug(f"Using registered parser for element type: {element_type_str}")
98
+ return parser.parse(element)
99
+
100
+ # Try fallback parser
101
+ if self._fallback_parser:
102
+ self._logger.debug(
103
+ f"Using fallback parser for unknown element type: {element_type_str}"
104
+ )
105
+ return self._fallback_parser.parse(element)
106
+
107
+ # No parser available
108
+ self._logger.warning(f"No parser available for element type: {element_type_str}")
109
+ return None
110
+
111
+ def parse_elements(self, elements: List[List[Any]]) -> List[Dict[str, Any]]:
112
+ """
113
+ Parse multiple S-expression elements.
114
+
115
+ Args:
116
+ elements: List of S-expression elements to parse
117
+
118
+ Returns:
119
+ List of parsed element data (excluding failed parses)
120
+ """
121
+ results = []
122
+ for element in elements:
123
+ parsed = self.parse_element(element)
124
+ if parsed is not None:
125
+ results.append(parsed)
126
+
127
+ self._logger.debug(f"Parsed {len(results)} of {len(elements)} elements")
128
+ return results
129
+
130
+ def get_registered_types(self) -> List[str]:
131
+ """
132
+ Get list of all registered element types.
133
+
134
+ Returns:
135
+ List of registered element type names
136
+ """
137
+ return list(self._parsers.keys())
138
+
139
+ def has_parser(self, element_type: str) -> bool:
140
+ """
141
+ Check if a parser is registered for the given element type.
142
+
143
+ Args:
144
+ element_type: Element type to check
145
+
146
+ Returns:
147
+ True if parser is registered
148
+ """
149
+ return element_type in self._parsers
150
+
151
+ def clear(self) -> None:
152
+ """Clear all registered parsers."""
153
+ self._parsers.clear()
154
+ self._fallback_parser = None
155
+ self._logger.debug("Cleared all registered parsers")
@@ -0,0 +1,222 @@
1
+ """
2
+ Symbol element parser for S-expression symbol definitions.
3
+
4
+ Handles parsing of symbol elements with properties, positions, and library references.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from .base import BaseElementParser
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SymbolParser(BaseElementParser):
18
+ """Parser for symbol S-expression elements."""
19
+
20
+ def __init__(self):
21
+ """Initialize symbol parser."""
22
+ super().__init__("symbol")
23
+
24
+ def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
25
+ """
26
+ Parse a symbol S-expression element.
27
+
28
+ Expected format:
29
+ (symbol (lib_id "Device:R") (at x y angle) (property "Reference" "R1" ...)...)
30
+
31
+ Args:
32
+ element: Symbol S-expression element
33
+
34
+ Returns:
35
+ Parsed symbol data with library ID, position, and properties
36
+ """
37
+ symbol_data = {
38
+ "lib_id": "",
39
+ "position": {"x": 0, "y": 0, "angle": 0},
40
+ "mirror": "",
41
+ "unit": 1,
42
+ "in_bom": True,
43
+ "on_board": True,
44
+ "fields_autoplaced": False,
45
+ "uuid": None,
46
+ "properties": [],
47
+ "instances": [],
48
+ }
49
+
50
+ for elem in element[1:]:
51
+ if not isinstance(elem, list):
52
+ continue
53
+
54
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
55
+
56
+ if elem_type == "lib_id":
57
+ symbol_data["lib_id"] = str(elem[1]) if len(elem) > 1 else ""
58
+ elif elem_type == "at":
59
+ self._parse_position(elem, symbol_data)
60
+ elif elem_type == "mirror":
61
+ symbol_data["mirror"] = str(elem[1]) if len(elem) > 1 else ""
62
+ elif elem_type == "unit":
63
+ symbol_data["unit"] = int(elem[1]) if len(elem) > 1 else 1
64
+ elif elem_type == "in_bom":
65
+ symbol_data["in_bom"] = self._parse_boolean(elem[1]) if len(elem) > 1 else True
66
+ elif elem_type == "on_board":
67
+ symbol_data["on_board"] = self._parse_boolean(elem[1]) if len(elem) > 1 else True
68
+ elif elem_type == "fields_autoplaced":
69
+ symbol_data["fields_autoplaced"] = True
70
+ elif elem_type == "uuid":
71
+ symbol_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
72
+ elif elem_type == "property":
73
+ prop = self._parse_property(elem)
74
+ if prop:
75
+ symbol_data["properties"].append(prop)
76
+ elif elem_type == "instances":
77
+ symbol_data["instances"] = self._parse_instances(elem)
78
+
79
+ return symbol_data
80
+
81
+ def _parse_position(self, at_element: List[Any], symbol_data: Dict[str, Any]) -> None:
82
+ """
83
+ Parse position from at element.
84
+
85
+ Args:
86
+ at_element: (at x y [angle])
87
+ symbol_data: Symbol data dictionary to update
88
+ """
89
+ try:
90
+ if len(at_element) >= 3:
91
+ symbol_data["position"]["x"] = float(at_element[1])
92
+ symbol_data["position"]["y"] = float(at_element[2])
93
+ if len(at_element) >= 4:
94
+ symbol_data["position"]["angle"] = float(at_element[3])
95
+ except (ValueError, IndexError) as e:
96
+ self._logger.warning(f"Invalid position coordinates: {at_element}, error: {e}")
97
+
98
+ def _parse_property(self, prop_element: List[Any]) -> Optional[Dict[str, Any]]:
99
+ """
100
+ Parse a property element.
101
+
102
+ Args:
103
+ prop_element: (property "name" "value" (at x y angle) ...)
104
+
105
+ Returns:
106
+ Parsed property data or None if invalid
107
+ """
108
+ if len(prop_element) < 3:
109
+ return None
110
+
111
+ prop_data = {
112
+ "name": str(prop_element[1]),
113
+ "value": str(prop_element[2]),
114
+ "position": {"x": 0, "y": 0, "angle": 0},
115
+ "effects": {},
116
+ }
117
+
118
+ # Parse additional property elements
119
+ for elem in prop_element[3:]:
120
+ if isinstance(elem, list) and len(elem) > 0:
121
+ elem_type = str(elem[0])
122
+ if elem_type == "at":
123
+ self._parse_property_position(elem, prop_data)
124
+ elif elem_type == "effects":
125
+ prop_data["effects"] = self._parse_effects(elem)
126
+
127
+ return prop_data
128
+
129
+ def _parse_property_position(self, at_element: List[Any], prop_data: Dict[str, Any]) -> None:
130
+ """Parse property position from at element."""
131
+ try:
132
+ if len(at_element) >= 3:
133
+ prop_data["position"]["x"] = float(at_element[1])
134
+ prop_data["position"]["y"] = float(at_element[2])
135
+ if len(at_element) >= 4:
136
+ prop_data["position"]["angle"] = float(at_element[3])
137
+ except (ValueError, IndexError) as e:
138
+ self._logger.warning(f"Invalid property position: {at_element}, error: {e}")
139
+
140
+ def _parse_effects(self, effects_element: List[Any]) -> Dict[str, Any]:
141
+ """Parse effects element for text formatting."""
142
+ effects = {
143
+ "font_size": 1.27,
144
+ "font_thickness": 0.15,
145
+ "bold": False,
146
+ "italic": False,
147
+ "hide": False,
148
+ "justify": [],
149
+ }
150
+
151
+ for elem in effects_element[1:]:
152
+ if isinstance(elem, list) and len(elem) > 0:
153
+ elem_type = str(elem[0])
154
+ if elem_type == "font":
155
+ self._parse_font(elem, effects)
156
+ elif elem_type == "justify":
157
+ effects["justify"] = [str(j) for j in elem[1:]]
158
+ elif elem_type == "hide":
159
+ effects["hide"] = True
160
+
161
+ return effects
162
+
163
+ def _parse_font(self, font_element: List[Any], effects: Dict[str, Any]) -> None:
164
+ """Parse font element within effects."""
165
+ for elem in font_element[1:]:
166
+ if isinstance(elem, list) and len(elem) > 0:
167
+ elem_type = str(elem[0])
168
+ if elem_type == "size":
169
+ try:
170
+ if len(elem) >= 3:
171
+ effects["font_size"] = float(elem[1])
172
+ except (ValueError, IndexError):
173
+ pass
174
+ elif elem_type == "thickness":
175
+ try:
176
+ effects["font_thickness"] = float(elem[1])
177
+ except (ValueError, IndexError):
178
+ pass
179
+ elif elem_type == "bold":
180
+ effects["bold"] = True
181
+ elif elem_type == "italic":
182
+ effects["italic"] = True
183
+
184
+ def _parse_instances(self, instances_element: List[Any]) -> List[Dict[str, Any]]:
185
+ """Parse instances element for symbol instances."""
186
+ instances = []
187
+ for elem in instances_element[1:]:
188
+ if isinstance(elem, list) and len(elem) > 0 and str(elem[0]) == "instance":
189
+ instance = self._parse_instance(elem)
190
+ if instance:
191
+ instances.append(instance)
192
+ return instances
193
+
194
+ def _parse_instance(self, instance_element: List[Any]) -> Optional[Dict[str, Any]]:
195
+ """Parse a single instance element."""
196
+ instance = {"project": "", "path": "", "reference": "", "unit": 1}
197
+
198
+ for elem in instance_element[1:]:
199
+ if isinstance(elem, list) and len(elem) >= 2:
200
+ elem_type = str(elem[0])
201
+ if elem_type == "project":
202
+ instance["project"] = str(elem[1])
203
+ elif elem_type == "path":
204
+ instance["path"] = str(elem[1])
205
+ elif elem_type == "reference":
206
+ instance["reference"] = str(elem[1])
207
+ elif elem_type == "unit":
208
+ try:
209
+ instance["unit"] = int(elem[1])
210
+ except (ValueError, IndexError):
211
+ pass
212
+
213
+ return instance if instance["reference"] else None
214
+
215
+ def _parse_boolean(self, value: Any) -> bool:
216
+ """Parse boolean value from S-expression."""
217
+ if isinstance(value, str):
218
+ return value.lower() in ("yes", "true", "1")
219
+ elif isinstance(value, sexpdata.Symbol):
220
+ return str(value).lower() in ("yes", "true", "1")
221
+ else:
222
+ return bool(value)
@@ -0,0 +1,99 @@
1
+ """
2
+ Wire element parser for S-expression wire definitions.
3
+
4
+ Handles parsing of wire elements with points, stroke properties, and UUIDs.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ import sexpdata
11
+
12
+ from .base import BaseElementParser
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class WireParser(BaseElementParser):
18
+ """Parser for wire S-expression elements."""
19
+
20
+ def __init__(self):
21
+ """Initialize wire parser."""
22
+ super().__init__("wire")
23
+
24
+ def parse_element(self, element: List[Any]) -> Optional[Dict[str, Any]]:
25
+ """
26
+ Parse a wire S-expression element.
27
+
28
+ Expected format:
29
+ (wire (pts (xy x1 y1) (xy x2 y2) ...) (stroke (width w) (type t)) (uuid "..."))
30
+
31
+ Args:
32
+ element: Wire S-expression element
33
+
34
+ Returns:
35
+ Parsed wire data with points, stroke, and UUID information
36
+ """
37
+ wire_data = {
38
+ "points": [],
39
+ "stroke_width": 0.0,
40
+ "stroke_type": "default",
41
+ "uuid": None,
42
+ "wire_type": "wire", # Default to wire (vs bus)
43
+ }
44
+
45
+ for elem in element[1:]:
46
+ if not isinstance(elem, list):
47
+ continue
48
+
49
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
50
+
51
+ if elem_type == "pts":
52
+ self._parse_points(elem, wire_data)
53
+ elif elem_type == "stroke":
54
+ self._parse_stroke(elem, wire_data)
55
+ elif elem_type == "uuid":
56
+ wire_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
57
+
58
+ # Validate wire has sufficient points
59
+ if len(wire_data["points"]) >= 2:
60
+ return wire_data
61
+ else:
62
+ self._logger.warning(f"Wire has insufficient points: {len(wire_data['points'])}")
63
+ return None
64
+
65
+ def _parse_points(self, pts_element: List[Any], wire_data: Dict[str, Any]) -> None:
66
+ """
67
+ Parse points from pts element.
68
+
69
+ Args:
70
+ pts_element: (pts (xy x1 y1) (xy x2 y2) ...)
71
+ wire_data: Wire data dictionary to update
72
+ """
73
+ for pt in pts_element[1:]:
74
+ if isinstance(pt, list) and len(pt) >= 3:
75
+ if str(pt[0]) == "xy":
76
+ try:
77
+ x, y = float(pt[1]), float(pt[2])
78
+ wire_data["points"].append({"x": x, "y": y})
79
+ except (ValueError, IndexError) as e:
80
+ self._logger.warning(f"Invalid point coordinates: {pt}, error: {e}")
81
+
82
+ def _parse_stroke(self, stroke_element: List[Any], wire_data: Dict[str, Any]) -> None:
83
+ """
84
+ Parse stroke properties from stroke element.
85
+
86
+ Args:
87
+ stroke_element: (stroke (width w) (type t))
88
+ wire_data: Wire data dictionary to update
89
+ """
90
+ for stroke_elem in stroke_element[1:]:
91
+ if isinstance(stroke_elem, list) and len(stroke_elem) >= 2:
92
+ stroke_type = str(stroke_elem[0])
93
+ try:
94
+ if stroke_type == "width":
95
+ wire_data["stroke_width"] = float(stroke_elem[1])
96
+ elif stroke_type == "type":
97
+ wire_data["stroke_type"] = str(stroke_elem[1])
98
+ except (ValueError, IndexError) as e:
99
+ self._logger.warning(f"Invalid stroke property: {stroke_elem}, error: {e}")
@@ -0,0 +1,18 @@
1
+ """
2
+ Symbol resolution and caching architecture for KiCAD schematic API.
3
+
4
+ This module provides a unified symbol resolution system that separates concerns
5
+ between caching, inheritance resolution, and validation while maintaining
6
+ high performance and exact format preservation.
7
+ """
8
+
9
+ from .cache import ISymbolCache, SymbolCache
10
+ from .resolver import SymbolResolver
11
+ from .validators import SymbolValidator
12
+
13
+ __all__ = [
14
+ "ISymbolCache",
15
+ "SymbolCache",
16
+ "SymbolResolver",
17
+ "SymbolValidator",
18
+ ]