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.

@@ -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}"