kicad-sch-api 0.0.1__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 +112 -0
- kicad_sch_api/core/__init__.py +23 -0
- kicad_sch_api/core/components.py +652 -0
- kicad_sch_api/core/formatter.py +312 -0
- kicad_sch_api/core/parser.py +434 -0
- kicad_sch_api/core/schematic.py +478 -0
- kicad_sch_api/core/types.py +369 -0
- kicad_sch_api/library/__init__.py +10 -0
- kicad_sch_api/library/cache.py +548 -0
- kicad_sch_api/mcp/__init__.py +5 -0
- kicad_sch_api/mcp/server.py +500 -0
- kicad_sch_api/py.typed +1 -0
- kicad_sch_api/utils/__init__.py +15 -0
- kicad_sch_api/utils/validation.py +447 -0
- kicad_sch_api-0.0.1.dist-info/METADATA +226 -0
- kicad_sch_api-0.0.1.dist-info/RECORD +20 -0
- kicad_sch_api-0.0.1.dist-info/WHEEL +5 -0
- kicad_sch_api-0.0.1.dist-info/entry_points.txt +2 -0
- kicad_sch_api-0.0.1.dist-info/licenses/LICENSE +21 -0
- kicad_sch_api-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exact formatting preservation for KiCAD schematic files.
|
|
3
|
+
|
|
4
|
+
This module provides precise S-expression formatting that matches KiCAD's native output exactly,
|
|
5
|
+
ensuring round-trip compatibility and professional output quality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Union
|
|
12
|
+
|
|
13
|
+
import sexpdata
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FormatRule:
|
|
20
|
+
"""Formatting rule for S-expression elements."""
|
|
21
|
+
|
|
22
|
+
inline: bool = False
|
|
23
|
+
max_inline_elements: Optional[int] = None
|
|
24
|
+
quote_indices: Set[int] = None
|
|
25
|
+
custom_handler: Optional[Callable] = None
|
|
26
|
+
indent_level: int = 1
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
if self.quote_indices is None:
|
|
30
|
+
self.quote_indices = set()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ExactFormatter:
|
|
34
|
+
"""
|
|
35
|
+
S-expression formatter that produces output identical to KiCAD's native formatting.
|
|
36
|
+
|
|
37
|
+
This formatter ensures exact format preservation for professional schematic manipulation,
|
|
38
|
+
matching KiCAD's indentation, spacing, and quoting conventions precisely.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
"""Initialize the formatter with KiCAD-specific rules."""
|
|
43
|
+
self.rules = {}
|
|
44
|
+
self._initialize_kicad_rules()
|
|
45
|
+
logger.debug("Exact formatter initialized with KiCAD rules")
|
|
46
|
+
|
|
47
|
+
def _initialize_kicad_rules(self):
|
|
48
|
+
"""Initialize formatting rules that match KiCAD's output exactly."""
|
|
49
|
+
|
|
50
|
+
# Metadata elements - single line
|
|
51
|
+
self.rules["kicad_sch"] = FormatRule(inline=False, indent_level=0)
|
|
52
|
+
self.rules["version"] = FormatRule(inline=True)
|
|
53
|
+
self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
|
|
54
|
+
self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
|
|
55
|
+
|
|
56
|
+
# Title block
|
|
57
|
+
self.rules["title_block"] = FormatRule(inline=False)
|
|
58
|
+
self.rules["title"] = FormatRule(inline=True, quote_indices={1})
|
|
59
|
+
self.rules["company"] = FormatRule(inline=True, quote_indices={1})
|
|
60
|
+
self.rules["revision"] = FormatRule(inline=True, quote_indices={1})
|
|
61
|
+
self.rules["date"] = FormatRule(inline=True, quote_indices={1})
|
|
62
|
+
self.rules["size"] = FormatRule(inline=True, quote_indices={1})
|
|
63
|
+
self.rules["comment"] = FormatRule(inline=True, quote_indices={2})
|
|
64
|
+
|
|
65
|
+
# Library symbols
|
|
66
|
+
self.rules["lib_symbols"] = FormatRule(inline=False)
|
|
67
|
+
self.rules["symbol"] = FormatRule(inline=False, quote_indices={1})
|
|
68
|
+
|
|
69
|
+
# Component elements
|
|
70
|
+
self.rules["lib_id"] = FormatRule(inline=True, quote_indices={1})
|
|
71
|
+
self.rules["at"] = FormatRule(inline=True)
|
|
72
|
+
self.rules["unit"] = FormatRule(inline=True)
|
|
73
|
+
self.rules["exclude_from_sim"] = FormatRule(inline=True)
|
|
74
|
+
self.rules["in_bom"] = FormatRule(inline=True)
|
|
75
|
+
self.rules["on_board"] = FormatRule(inline=True)
|
|
76
|
+
self.rules["dnp"] = FormatRule(inline=True)
|
|
77
|
+
self.rules["fields_autoplaced"] = FormatRule(inline=True)
|
|
78
|
+
|
|
79
|
+
# Properties - KiCAD specific format
|
|
80
|
+
self.rules["property"] = FormatRule(
|
|
81
|
+
inline=False, quote_indices={1, 2}, custom_handler=self._format_property
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Pins and connections
|
|
85
|
+
self.rules["pin"] = FormatRule(inline=False, quote_indices={1, 2})
|
|
86
|
+
self.rules["instances"] = FormatRule(inline=False)
|
|
87
|
+
self.rules["project"] = FormatRule(inline=True, quote_indices={1})
|
|
88
|
+
self.rules["path"] = FormatRule(inline=True, quote_indices={1})
|
|
89
|
+
self.rules["reference"] = FormatRule(inline=True, quote_indices={1})
|
|
90
|
+
|
|
91
|
+
# Wire elements
|
|
92
|
+
self.rules["wire"] = FormatRule(inline=False)
|
|
93
|
+
self.rules["pts"] = FormatRule(inline=False)
|
|
94
|
+
self.rules["xy"] = FormatRule(inline=True)
|
|
95
|
+
self.rules["stroke"] = FormatRule(inline=False)
|
|
96
|
+
self.rules["width"] = FormatRule(inline=True)
|
|
97
|
+
self.rules["type"] = FormatRule(inline=True)
|
|
98
|
+
|
|
99
|
+
# Junction
|
|
100
|
+
self.rules["junction"] = FormatRule(inline=False)
|
|
101
|
+
self.rules["diameter"] = FormatRule(inline=True)
|
|
102
|
+
|
|
103
|
+
# Labels
|
|
104
|
+
self.rules["label"] = FormatRule(inline=False, quote_indices={1})
|
|
105
|
+
self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
|
|
106
|
+
self.rules["hierarchical_label"] = FormatRule(inline=False, quote_indices={1})
|
|
107
|
+
|
|
108
|
+
# Effects and text formatting
|
|
109
|
+
self.rules["effects"] = FormatRule(inline=False)
|
|
110
|
+
self.rules["font"] = FormatRule(inline=False)
|
|
111
|
+
self.rules["size"] = FormatRule(inline=True)
|
|
112
|
+
self.rules["thickness"] = FormatRule(inline=True)
|
|
113
|
+
self.rules["justify"] = FormatRule(inline=True)
|
|
114
|
+
self.rules["hide"] = FormatRule(inline=True)
|
|
115
|
+
|
|
116
|
+
def format(self, data: Any) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Format S-expression data with exact KiCAD formatting.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
data: S-expression data structure
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Formatted string matching KiCAD's output exactly
|
|
125
|
+
"""
|
|
126
|
+
return self._format_element(data, 0)
|
|
127
|
+
|
|
128
|
+
def format_preserving_write(self, new_data: Any, original_content: str) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Write new data while preserving as much original formatting as possible.
|
|
131
|
+
|
|
132
|
+
This method attempts to maintain the original file's formatting style
|
|
133
|
+
while incorporating changes from the new data structure.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
new_data: New S-expression data to write
|
|
137
|
+
original_content: Original file content for format reference
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Formatted string with preserved styling where possible
|
|
141
|
+
"""
|
|
142
|
+
# For now, use standard formatting - future enhancement could
|
|
143
|
+
# analyze original formatting patterns and apply them
|
|
144
|
+
return self.format(new_data)
|
|
145
|
+
|
|
146
|
+
def _format_element(self, element: Any, indent_level: int) -> str:
|
|
147
|
+
"""Format a single S-expression element."""
|
|
148
|
+
if isinstance(element, list):
|
|
149
|
+
return self._format_list(element, indent_level)
|
|
150
|
+
elif isinstance(element, sexpdata.Symbol):
|
|
151
|
+
return str(element)
|
|
152
|
+
elif isinstance(element, str):
|
|
153
|
+
# Quote strings that need quoting
|
|
154
|
+
if self._needs_quoting(element):
|
|
155
|
+
return f'"{element}"'
|
|
156
|
+
return element
|
|
157
|
+
else:
|
|
158
|
+
return str(element)
|
|
159
|
+
|
|
160
|
+
def _format_list(self, lst: List[Any], indent_level: int) -> str:
|
|
161
|
+
"""Format a list (S-expression)."""
|
|
162
|
+
if not lst:
|
|
163
|
+
return "()"
|
|
164
|
+
|
|
165
|
+
# Get the tag (first element)
|
|
166
|
+
tag = str(lst[0]) if isinstance(lst[0], sexpdata.Symbol) else None
|
|
167
|
+
rule = self.rules.get(tag, FormatRule())
|
|
168
|
+
|
|
169
|
+
# Use custom handler if available
|
|
170
|
+
if rule.custom_handler:
|
|
171
|
+
return rule.custom_handler(lst, indent_level)
|
|
172
|
+
|
|
173
|
+
# Format based on rule
|
|
174
|
+
if rule.inline or self._should_format_inline(lst, rule):
|
|
175
|
+
return self._format_inline(lst, rule)
|
|
176
|
+
else:
|
|
177
|
+
return self._format_multiline(lst, indent_level, rule)
|
|
178
|
+
|
|
179
|
+
def _format_inline(self, lst: List[Any], rule: FormatRule) -> str:
|
|
180
|
+
"""Format list on a single line."""
|
|
181
|
+
elements = []
|
|
182
|
+
for i, element in enumerate(lst):
|
|
183
|
+
if i in rule.quote_indices and isinstance(element, str):
|
|
184
|
+
elements.append(f'"{element}"')
|
|
185
|
+
else:
|
|
186
|
+
elements.append(self._format_element(element, 0))
|
|
187
|
+
return f"({' '.join(elements)})"
|
|
188
|
+
|
|
189
|
+
def _format_multiline(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
190
|
+
"""Format list across multiple lines with proper indentation."""
|
|
191
|
+
if not lst:
|
|
192
|
+
return "()"
|
|
193
|
+
|
|
194
|
+
result = []
|
|
195
|
+
indent = "\t" * indent_level
|
|
196
|
+
|
|
197
|
+
# First element (tag) on opening line
|
|
198
|
+
tag = str(lst[0])
|
|
199
|
+
|
|
200
|
+
if len(lst) == 1:
|
|
201
|
+
return f"({tag})"
|
|
202
|
+
|
|
203
|
+
# Handle different multiline formats based on tag
|
|
204
|
+
if tag == "property":
|
|
205
|
+
return self._format_property(lst, indent_level)
|
|
206
|
+
elif tag in ("symbol", "wire", "junction", "label"):
|
|
207
|
+
return self._format_component_like(lst, indent_level, rule)
|
|
208
|
+
else:
|
|
209
|
+
return self._format_generic_multiline(lst, indent_level, rule)
|
|
210
|
+
|
|
211
|
+
def _format_property(self, lst: List[Any], indent_level: int) -> str:
|
|
212
|
+
"""Format property elements in KiCAD style."""
|
|
213
|
+
if len(lst) < 3:
|
|
214
|
+
return self._format_inline(lst, FormatRule(quote_indices={1, 2}))
|
|
215
|
+
|
|
216
|
+
indent = "\t" * indent_level
|
|
217
|
+
next_indent = "\t" * (indent_level + 1)
|
|
218
|
+
|
|
219
|
+
# Property format: (property "Name" "Value" (at x y rotation) (effects ...))
|
|
220
|
+
result = f'({lst[0]} "{lst[1]}" "{lst[2]}"'
|
|
221
|
+
|
|
222
|
+
# Add position and effects on separate lines
|
|
223
|
+
for element in lst[3:]:
|
|
224
|
+
if isinstance(element, list):
|
|
225
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
226
|
+
else:
|
|
227
|
+
result += f" {element}"
|
|
228
|
+
|
|
229
|
+
result += ")"
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
def _format_component_like(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
233
|
+
"""Format component-like elements (symbol, wire, etc.)."""
|
|
234
|
+
indent = "\t" * indent_level
|
|
235
|
+
next_indent = "\t" * (indent_level + 1)
|
|
236
|
+
|
|
237
|
+
tag = str(lst[0])
|
|
238
|
+
result = f"({tag}"
|
|
239
|
+
|
|
240
|
+
# Add quoted elements if specified
|
|
241
|
+
for i in range(1, len(lst)):
|
|
242
|
+
element = lst[i]
|
|
243
|
+
if isinstance(element, list):
|
|
244
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
245
|
+
else:
|
|
246
|
+
if i in rule.quote_indices and isinstance(element, str):
|
|
247
|
+
result += f' "{element}"'
|
|
248
|
+
else:
|
|
249
|
+
result += f" {self._format_element(element, 0)}"
|
|
250
|
+
|
|
251
|
+
result += ")"
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
def _format_generic_multiline(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
255
|
+
"""Generic multiline formatting."""
|
|
256
|
+
indent = "\t" * indent_level
|
|
257
|
+
next_indent = "\t" * (indent_level + 1)
|
|
258
|
+
|
|
259
|
+
result = f"({lst[0]}"
|
|
260
|
+
|
|
261
|
+
for i, element in enumerate(lst[1:], 1):
|
|
262
|
+
if isinstance(element, list):
|
|
263
|
+
result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
|
|
264
|
+
else:
|
|
265
|
+
if i in rule.quote_indices and isinstance(element, str):
|
|
266
|
+
result += f' "{element}"'
|
|
267
|
+
else:
|
|
268
|
+
result += f" {self._format_element(element, 0)}"
|
|
269
|
+
|
|
270
|
+
result += ")"
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
def _should_format_inline(self, lst: List[Any], rule: FormatRule) -> bool:
|
|
274
|
+
"""Determine if list should be formatted inline."""
|
|
275
|
+
if rule.max_inline_elements is not None:
|
|
276
|
+
if len(lst) > rule.max_inline_elements:
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
# Check if any element is a list (nested structure)
|
|
280
|
+
for element in lst[1:]: # Skip tag
|
|
281
|
+
if isinstance(element, list):
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
def _needs_quoting(self, text: str) -> bool:
|
|
287
|
+
"""Check if string needs to be quoted."""
|
|
288
|
+
# Quote if contains spaces, special characters, or is empty
|
|
289
|
+
if not text or " " in text or '"' in text:
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
# Quote if contains S-expression special characters
|
|
293
|
+
special_chars = "()[]{}#"
|
|
294
|
+
return any(c in text for c in special_chars)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class CompactFormatter(ExactFormatter):
|
|
298
|
+
"""Compact formatter for minimal output size."""
|
|
299
|
+
|
|
300
|
+
def _format_multiline(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
|
|
301
|
+
"""Override to use minimal spacing."""
|
|
302
|
+
# Use single spaces instead of tabs for compact output
|
|
303
|
+
return super()._format_multiline(lst, indent_level, rule).replace("\t", " ")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class DebugFormatter(ExactFormatter):
|
|
307
|
+
"""Debug formatter with extra spacing and comments."""
|
|
308
|
+
|
|
309
|
+
def format(self, data: Any) -> str:
|
|
310
|
+
"""Format with debug information."""
|
|
311
|
+
result = super().format(data)
|
|
312
|
+
return f"; Generated by kicad-sch-api ExactFormatter\n{result}"
|