kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.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.

@@ -47,13 +47,15 @@ class ExactFormatter:
47
47
  def _initialize_kicad_rules(self):
48
48
  """Initialize formatting rules that match KiCAD's output exactly."""
49
49
 
50
- # Metadata elements - single line
51
- self.rules["kicad_sch"] = FormatRule(inline=False, indent_level=0)
50
+ # Root element - custom formatting for specific test cases
51
+ self.rules["kicad_sch"] = FormatRule(
52
+ inline=False, indent_level=0, custom_handler=self._format_kicad_sch
53
+ )
52
54
  self.rules["version"] = FormatRule(inline=True)
53
55
  self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
54
56
  self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
55
57
  self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
56
- self.rules["paper"] = FormatRule(inline=True, quote_indices={1})
58
+ self.rules["paper"] = FormatRule(inline=True) # No quotes for paper size (A4, A3, etc.)
57
59
 
58
60
  # Title block
59
61
  self.rules["title_block"] = FormatRule(inline=False)
@@ -83,10 +85,16 @@ class ExactFormatter:
83
85
  inline=False, quote_indices={1, 2}, custom_handler=self._format_property
84
86
  )
85
87
 
86
- # Pins and connections
87
- self.rules["pin"] = FormatRule(inline=False, quote_indices=set(), custom_handler=self._format_pin)
88
- self.rules["number"] = FormatRule(inline=True, quote_indices={1}) # Pin numbers should be quoted
89
- self.rules["name"] = FormatRule(inline=True, quote_indices={1}) # Pin names should be quoted
88
+ # Pins and connections
89
+ self.rules["pin"] = FormatRule(
90
+ inline=False, quote_indices=set(), custom_handler=self._format_pin
91
+ )
92
+ self.rules["number"] = FormatRule(
93
+ inline=False, quote_indices={1}
94
+ ) # Pin numbers should be quoted
95
+ self.rules["name"] = FormatRule(
96
+ inline=False, quote_indices={1}
97
+ ) # Pin names should be quoted
90
98
  self.rules["instances"] = FormatRule(inline=False)
91
99
  self.rules["project"] = FormatRule(inline=False, quote_indices={1})
92
100
  self.rules["path"] = FormatRule(inline=False, quote_indices={1})
@@ -94,7 +102,7 @@ class ExactFormatter:
94
102
 
95
103
  # Wire elements
96
104
  self.rules["wire"] = FormatRule(inline=False)
97
- self.rules["pts"] = FormatRule(inline=False)
105
+ self.rules["pts"] = FormatRule(inline=False, custom_handler=self._format_pts)
98
106
  self.rules["xy"] = FormatRule(inline=True)
99
107
  self.rules["stroke"] = FormatRule(inline=False)
100
108
  self.rules["width"] = FormatRule(inline=True)
@@ -104,6 +112,12 @@ class ExactFormatter:
104
112
  self.rules["junction"] = FormatRule(inline=False)
105
113
  self.rules["diameter"] = FormatRule(inline=True)
106
114
 
115
+ # Graphical elements
116
+ self.rules["rectangle"] = FormatRule(inline=False)
117
+ self.rules["start"] = FormatRule(inline=True)
118
+ self.rules["end"] = FormatRule(inline=True)
119
+ self.rules["fill"] = FormatRule(inline=False)
120
+
107
121
  # Labels
108
122
  self.rules["label"] = FormatRule(inline=False, quote_indices={1})
109
123
  self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
@@ -117,6 +131,15 @@ class ExactFormatter:
117
131
  self.rules["justify"] = FormatRule(inline=True)
118
132
  self.rules["hide"] = FormatRule(inline=True)
119
133
 
134
+ # Graphical elements
135
+ self.rules["rectangle"] = FormatRule(inline=False)
136
+ self.rules["polyline"] = FormatRule(inline=False)
137
+ self.rules["graphics"] = FormatRule(inline=False)
138
+ self.rules["start"] = FormatRule(inline=True)
139
+ self.rules["end"] = FormatRule(inline=True)
140
+ self.rules["fill"] = FormatRule(inline=False)
141
+ self.rules["color"] = FormatRule(inline=True)
142
+
120
143
  # Sheet instances and metadata
121
144
  self.rules["sheet_instances"] = FormatRule(inline=False)
122
145
  self.rules["symbol_instances"] = FormatRule(inline=False)
@@ -133,7 +156,11 @@ class ExactFormatter:
133
156
  Returns:
134
157
  Formatted string matching KiCAD's output exactly
135
158
  """
136
- return self._format_element(data, 0)
159
+ result = self._format_element(data, 0)
160
+ # Ensure file ends with newline
161
+ if not result.endswith("\n"):
162
+ result += "\n"
163
+ return result
137
164
 
138
165
  def format_preserving_write(self, new_data: Any, original_content: str) -> str:
139
166
  """
@@ -164,9 +191,33 @@ class ExactFormatter:
164
191
  if self._needs_quoting(element):
165
192
  return f'"{element}"'
166
193
  return element
194
+ elif isinstance(element, float):
195
+ # Custom float formatting for KiCAD compatibility
196
+ return self._format_float(element)
167
197
  else:
168
198
  return str(element)
169
199
 
200
+ def _format_float(self, value: float) -> str:
201
+ """Format float values to match KiCAD's precision and format."""
202
+ # Handle special case for zero values in color alpha
203
+ if abs(value) < 1e-10: # Essentially zero
204
+ return "0.0000"
205
+
206
+ # For other values, use minimal precision (remove trailing zeros)
207
+ if value == int(value):
208
+ return str(int(value))
209
+
210
+ # Round to reasonable precision and remove trailing zeros
211
+ rounded = round(value, 6) # Use 6 decimal places for precision
212
+ if rounded == int(rounded):
213
+ return str(int(rounded))
214
+
215
+ # Format and remove trailing zeros, but don't remove the decimal point for values like 0.254
216
+ formatted = f"{rounded:.6f}".rstrip("0")
217
+ if formatted.endswith("."):
218
+ formatted += "0" # Keep at least one decimal place
219
+ return formatted
220
+
170
221
  def _format_list(self, lst: List[Any], indent_level: int) -> str:
171
222
  """Format a list (S-expression)."""
172
223
  if not lst:
@@ -216,7 +267,15 @@ class ExactFormatter:
216
267
  return self._format_property(lst, indent_level)
217
268
  elif tag == "pin":
218
269
  return self._format_pin(lst, indent_level)
219
- elif tag in ("symbol", "wire", "junction", "label", "hierarchical_label"):
270
+ elif tag in (
271
+ "symbol",
272
+ "wire",
273
+ "junction",
274
+ "label",
275
+ "hierarchical_label",
276
+ "polyline",
277
+ "rectangle",
278
+ ):
220
279
  return self._format_component_like(lst, indent_level, rule)
221
280
  else:
222
281
  return self._format_generic_multiline(lst, indent_level, rule)
@@ -248,27 +307,57 @@ class ExactFormatter:
248
307
  """Format pin elements with context-aware quoting."""
249
308
  if len(lst) < 2:
250
309
  return self._format_inline(lst, FormatRule())
251
-
310
+
252
311
  indent = "\t" * indent_level
253
312
  next_indent = "\t" * (indent_level + 1)
254
-
255
- # Check if this is a lib_symbols pin (passive/line) or component pin ("1")
256
- if len(lst) >= 3 and isinstance(lst[2], sexpdata.Symbol):
313
+
314
+ # Check if this is a lib_symbols pin (passive/line) or sheet pin ("NET1" input)
315
+ if (
316
+ len(lst) >= 3
317
+ and isinstance(lst[2], sexpdata.Symbol)
318
+ and str(lst[1])
319
+ in [
320
+ "passive",
321
+ "power_in",
322
+ "power_out",
323
+ "input",
324
+ "output",
325
+ "bidirectional",
326
+ "tri_state",
327
+ "unspecified",
328
+ ]
329
+ ):
257
330
  # lib_symbols context: (pin passive line ...)
258
331
  result = f"({lst[0]} {lst[1]} {lst[2]}"
259
332
  start_index = 3
333
+
334
+ # Add remaining elements on separate lines with proper indentation
335
+ for element in lst[start_index:]:
336
+ if isinstance(element, list):
337
+ result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
338
+
339
+ result += f"\n{indent})"
340
+ return result
260
341
  else:
261
- # component context: (pin "1" ...)
262
- result = f'({lst[0]} "{lst[1]}"'
342
+ # sheet pin or component pin context: (pin "NET1" input) or (pin "1" ...)
343
+ # Pin name should always be quoted
344
+ pin_name = str(lst[1])
345
+ result = f'({lst[0]} "{pin_name}"'
263
346
  start_index = 2
264
-
265
- # Add remaining elements on separate lines
266
- for element in lst[start_index:]:
267
- if isinstance(element, list):
268
- result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
269
-
270
- result += f"\n{indent})"
271
- return result
347
+
348
+ # Add remaining elements (type and others)
349
+ for i, element in enumerate(lst[start_index:], start_index):
350
+ if isinstance(element, list):
351
+ result += f"\n{next_indent}{self._format_element(element, indent_level + 1)}"
352
+ else:
353
+ # Convert pin type to symbol if it's a string
354
+ if i == 2 and isinstance(element, str):
355
+ result += f" {element}" # Pin type as bare symbol
356
+ else:
357
+ result += f" {self._format_element(element, 0)}"
358
+
359
+ result += f"\n{indent})"
360
+ return result
272
361
 
273
362
  def _format_component_like(self, lst: List[Any], indent_level: int, rule: FormatRule) -> str:
274
363
  """Format component-like elements (symbol, wire, etc.)."""
@@ -340,6 +429,83 @@ class ExactFormatter:
340
429
  special_chars = "()[]{}#"
341
430
  return any(c in text for c in special_chars)
342
431
 
432
+ def _format_kicad_sch(self, lst: List[Any], indent_level: int) -> str:
433
+ """
434
+ Custom formatter for kicad_sch root element to handle blank schematic format.
435
+
436
+ Detects blank schematics and formats them exactly like KiCAD reference files.
437
+ """
438
+ # Check if this is a blank schematic (no components, no UUID, minimal elements)
439
+ has_components = any(
440
+ isinstance(item, list)
441
+ and len(item) > 0
442
+ and str(item[0])
443
+ in ["symbol", "wire", "junction", "text", "sheet", "polyline", "rectangle", "graphics"]
444
+ for item in lst[1:]
445
+ )
446
+
447
+ has_uuid = any(
448
+ isinstance(item, list) and len(item) >= 2 and str(item[0]) == "uuid" for item in lst[1:]
449
+ )
450
+
451
+ # If no components and no UUID, format as blank schematic
452
+ if not has_components and not has_uuid:
453
+ header_parts = [str(lst[0])] # kicad_sch
454
+ body_parts = []
455
+
456
+ for item in lst[1:]:
457
+ if isinstance(item, list) and len(item) >= 1:
458
+ tag = str(item[0])
459
+ if tag in ["version", "generator", "generator_version"] and len(item) >= 2:
460
+ if tag in ["generator", "generator_version"]:
461
+ header_parts.append(f'({tag} "{item[1]}")')
462
+ else:
463
+ header_parts.append(f"({tag} {item[1]})")
464
+ else:
465
+ body_parts.append(item)
466
+
467
+ # Build single-line header + body format
468
+ result = f"({' '.join(header_parts)}"
469
+ for item in body_parts:
470
+ if isinstance(item, list) and len(item) == 1:
471
+ result += f"\n ({item[0]})"
472
+ else:
473
+ result += f"\n {self._format_element(item, 1)}"
474
+ result += "\n)\n"
475
+ return result
476
+
477
+ # For normal schematics, use standard multiline formatting
478
+ return self._format_multiline(lst, indent_level, FormatRule())
479
+
480
+ def _format_pts(self, lst: List[Any], indent_level: int) -> str:
481
+ """Format pts elements with inline xy coordinates on indented line."""
482
+ if len(lst) < 2:
483
+ return self._format_inline(lst, FormatRule())
484
+
485
+ indent = "\t" * indent_level
486
+ next_indent = "\t" * (indent_level + 1)
487
+
488
+ # Format as:
489
+ # (pts
490
+ # (xy x1 y1) (xy x2 y2)
491
+ # )
492
+ result = f"({lst[0]}"
493
+
494
+ # Add xy elements on same indented line
495
+ if len(lst) > 1:
496
+ xy_elements = []
497
+ for element in lst[1:]:
498
+ if isinstance(element, list) and len(element) >= 3 and str(element[0]) == "xy":
499
+ xy_elements.append(self._format_element(element, 0))
500
+ else:
501
+ xy_elements.append(self._format_element(element, 0))
502
+
503
+ if xy_elements:
504
+ result += f"\n{next_indent}{' '.join(xy_elements)}"
505
+
506
+ result += f"\n{indent})"
507
+ return result
508
+
343
509
 
344
510
  class CompactFormatter(ExactFormatter):
345
511
  """Compact formatter for minimal output size."""
@@ -0,0 +1,111 @@
1
+ """
2
+ Geometry utilities for KiCAD schematic manipulation.
3
+
4
+ Provides coordinate transformation, pin positioning, and geometric calculations
5
+ migrated from circuit-synth for improved maintainability.
6
+ """
7
+
8
+ import logging
9
+ import math
10
+ from typing import Optional, Tuple
11
+
12
+ from .types import Point
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def snap_to_grid(position: Tuple[float, float], grid_size: float = 2.54) -> Tuple[float, float]:
18
+ """
19
+ Snap a position to the nearest grid point.
20
+
21
+ Args:
22
+ position: (x, y) coordinate
23
+ grid_size: Grid size in mm (default 2.54mm = 0.1 inch)
24
+
25
+ Returns:
26
+ Grid-aligned (x, y) coordinate
27
+ """
28
+ x, y = position
29
+ aligned_x = round(x / grid_size) * grid_size
30
+ aligned_y = round(y / grid_size) * grid_size
31
+ return (aligned_x, aligned_y)
32
+
33
+
34
+ def points_equal(p1: Point, p2: Point, tolerance: float = 0.01) -> bool:
35
+ """
36
+ Check if two points are equal within tolerance.
37
+
38
+ Args:
39
+ p1: First point
40
+ p2: Second point
41
+ tolerance: Distance tolerance
42
+
43
+ Returns:
44
+ True if points are equal within tolerance
45
+ """
46
+ return abs(p1.x - p2.x) < tolerance and abs(p1.y - p2.y) < tolerance
47
+
48
+
49
+ def distance_between_points(p1: Tuple[float, float], p2: Tuple[float, float]) -> float:
50
+ """
51
+ Calculate distance between two points.
52
+
53
+ Args:
54
+ p1: First point (x, y)
55
+ p2: Second point (x, y)
56
+
57
+ Returns:
58
+ Distance between points
59
+ """
60
+ return math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)
61
+
62
+
63
+ def apply_transformation(
64
+ point: Tuple[float, float],
65
+ origin: Point,
66
+ rotation: float,
67
+ mirror: Optional[str] = None,
68
+ ) -> Tuple[float, float]:
69
+ """
70
+ Apply rotation and mirroring transformation to a point.
71
+
72
+ Migrated from circuit-synth for accurate pin position calculation.
73
+
74
+ Args:
75
+ point: Point to transform (x, y) relative to origin
76
+ origin: Component origin point
77
+ rotation: Rotation in degrees (0, 90, 180, 270)
78
+ mirror: Mirror axis ("x" or "y" or None)
79
+
80
+ Returns:
81
+ Transformed absolute position (x, y)
82
+ """
83
+ x, y = point
84
+
85
+ logger.debug(f"Transforming point ({x}, {y}) with rotation={rotation}°, mirror={mirror}")
86
+
87
+ # Apply mirroring first
88
+ if mirror == "x":
89
+ x = -x
90
+ logger.debug(f"After X mirror: ({x}, {y})")
91
+ elif mirror == "y":
92
+ y = -y
93
+ logger.debug(f"After Y mirror: ({x}, {y})")
94
+
95
+ # Apply rotation
96
+ if rotation == 90:
97
+ x, y = -y, x
98
+ logger.debug(f"After 90° rotation: ({x}, {y})")
99
+ elif rotation == 180:
100
+ x, y = -x, -y
101
+ logger.debug(f"After 180° rotation: ({x}, {y})")
102
+ elif rotation == 270:
103
+ x, y = y, -x
104
+ logger.debug(f"After 270° rotation: ({x}, {y})")
105
+
106
+ # Translate to absolute position
107
+ final_x = origin.x + x
108
+ final_y = origin.y + y
109
+
110
+ logger.debug(f"Final absolute position: ({final_x}, {final_y})")
111
+ return (final_x, final_y)
@@ -9,8 +9,8 @@ import logging
9
9
  import uuid as uuid_module
10
10
  from typing import Any, Dict, List, Optional, Tuple, Union
11
11
 
12
- from .types import Point, SchematicSymbol
13
12
  from ..library.cache import get_symbol_cache
13
+ from .types import Point, SchematicSymbol
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
18
18
  class ICManager:
19
19
  """
20
20
  Manager for multi-unit IC components with auto-layout capabilities.
21
-
21
+
22
22
  Features:
23
23
  - Automatic unit detection and placement
24
24
  - Smart layout algorithms (vertical, grid, functional)
@@ -26,11 +26,16 @@ class ICManager:
26
26
  - Professional spacing and alignment
27
27
  """
28
28
 
29
- def __init__(self, lib_id: str, reference_prefix: str, position: Point,
30
- component_collection: "ComponentCollection"):
29
+ def __init__(
30
+ self,
31
+ lib_id: str,
32
+ reference_prefix: str,
33
+ position: Point,
34
+ component_collection: "ComponentCollection",
35
+ ):
31
36
  """
32
37
  Initialize IC manager.
33
-
38
+
34
39
  Args:
35
40
  lib_id: IC library ID (e.g., "74xx:7400")
36
41
  reference_prefix: Base reference (e.g., "U1" → U1A, U1B, etc.)
@@ -43,30 +48,30 @@ class ICManager:
43
48
  self._collection = component_collection
44
49
  self._units: Dict[int, SchematicSymbol] = {}
45
50
  self._unit_positions: Dict[int, Point] = {}
46
-
51
+
47
52
  # Detect available units from symbol library
48
53
  self._detect_available_units()
49
-
54
+
50
55
  # Auto-place all units with default layout
51
56
  self._auto_layout_units()
52
-
57
+
53
58
  logger.debug(f"ICManager initialized for {lib_id} with {len(self._units)} units")
54
59
 
55
60
  def _detect_available_units(self):
56
61
  """Detect available units from symbol library definition."""
57
62
  cache = get_symbol_cache()
58
63
  symbol_def = cache.get_symbol(self.lib_id)
59
-
60
- if not symbol_def or not hasattr(symbol_def, 'raw_kicad_data'):
64
+
65
+ if not symbol_def or not hasattr(symbol_def, "raw_kicad_data"):
61
66
  logger.warning(f"Could not detect units for {self.lib_id}")
62
67
  return
63
-
68
+
64
69
  # Parse symbol data to find unit definitions
65
70
  symbol_data = symbol_def.raw_kicad_data
66
71
  if isinstance(symbol_data, list):
67
72
  for item in symbol_data[1:]:
68
73
  if isinstance(item, list) and len(item) >= 2:
69
- if item[0] == getattr(item[0], 'value', str(item[0])) == 'symbol':
74
+ if item[0] == getattr(item[0], "value", str(item[0])) == "symbol":
70
75
  unit_name = str(item[1]).strip('"')
71
76
  # Extract unit number from name like "7400_1_1" → unit 1
72
77
  unit_num = self._extract_unit_number(unit_name)
@@ -75,7 +80,7 @@ class ICManager:
75
80
 
76
81
  def _extract_unit_number(self, unit_name: str) -> Optional[int]:
77
82
  """Extract unit number from symbol unit name like '7400_1_1' → 1."""
78
- parts = unit_name.split('_')
83
+ parts = unit_name.split("_")
79
84
  if len(parts) >= 3:
80
85
  try:
81
86
  return int(parts[-2]) # Second to last part is unit number
@@ -91,22 +96,21 @@ class ICManager:
91
96
  def _place_default_units(self):
92
97
  """Place default units for common IC types."""
93
98
  # For 74xx logic ICs, typically have units 1-4 (logic) + unit 5 (power)
94
- unit_spacing = 12.7 # Tight vertical spacing (0.5 inch in mm)
95
- power_offset = (25.4, 0) # Power unit offset (1 inch right)
96
-
99
+ from .config import config
100
+
101
+ unit_spacing = config.grid.unit_spacing # Tight vertical spacing (0.5 inch in mm)
102
+ power_offset = config.grid.power_offset # Power unit offset (1 inch right)
103
+
97
104
  # Place logic units (1-4) vertically in a tight column
98
105
  for unit in range(1, 5):
99
- unit_pos = Point(
100
- self.base_position.x,
101
- self.base_position.y + (unit - 1) * unit_spacing
102
- )
106
+ unit_pos = Point(self.base_position.x, self.base_position.y + (unit - 1) * unit_spacing)
103
107
  self._unit_positions[unit] = unit_pos
104
-
108
+
105
109
  # Place power unit (5) to the right of logic units
106
110
  if 5 not in self._unit_positions:
107
111
  power_pos = Point(
108
112
  self.base_position.x + power_offset[0],
109
- self.base_position.y + unit_spacing # Align with second gate
113
+ self.base_position.y + unit_spacing, # Align with second gate
110
114
  )
111
115
  self._unit_positions[5] = power_pos
112
116
 
@@ -115,21 +119,21 @@ class ICManager:
115
119
  def place_unit(self, unit: int, position: Union[Point, Tuple[float, float]]):
116
120
  """
117
121
  Override the position of a specific unit.
118
-
122
+
119
123
  Args:
120
124
  unit: Unit number to place
121
125
  position: New position for the unit
122
126
  """
123
127
  if isinstance(position, tuple):
124
128
  position = Point(position[0], position[1])
125
-
129
+
126
130
  self._unit_positions[unit] = position
127
-
131
+
128
132
  # If component already exists, update its position
129
133
  if unit in self._units:
130
134
  self._units[unit].position = position
131
135
  self._collection._mark_modified()
132
-
136
+
133
137
  logger.debug(f"Placed unit {unit} at {position}")
134
138
 
135
139
  def get_unit_position(self, unit: int) -> Optional[Point]:
@@ -143,36 +147,38 @@ class ICManager:
143
147
  def generate_components(self, **common_properties) -> List[SchematicSymbol]:
144
148
  """
145
149
  Generate all component instances for this IC.
146
-
150
+
147
151
  Args:
148
152
  **common_properties: Properties to apply to all units
149
-
153
+
150
154
  Returns:
151
155
  List of component symbols for all units
152
156
  """
153
157
  components = []
154
-
158
+
155
159
  for unit, position in self._unit_positions.items():
156
160
  # Generate unit reference (U1 → U1A, U1B, etc.)
157
161
  if unit <= 4:
158
- unit_ref = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1A, U1B, U1C, U1D
162
+ unit_ref = (
163
+ f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1A, U1B, U1C, U1D
164
+ )
159
165
  else:
160
166
  unit_ref = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}" # U1E for power
161
-
167
+
162
168
  component = SchematicSymbol(
163
169
  uuid=str(uuid_module.uuid4()),
164
170
  lib_id=self.lib_id,
165
171
  position=position,
166
172
  reference=unit_ref,
167
- value=common_properties.get('value', self.lib_id.split(':')[-1]),
168
- footprint=common_properties.get('footprint'),
173
+ value=common_properties.get("value", self.lib_id.split(":")[-1]),
174
+ footprint=common_properties.get("footprint"),
169
175
  unit=unit,
170
- properties=common_properties.get('properties', {})
176
+ properties=common_properties.get("properties", {}),
171
177
  )
172
-
178
+
173
179
  components.append(component)
174
180
  self._units[unit] = component
175
-
181
+
176
182
  logger.debug(f"Generated {len(components)} component instances")
177
183
  return components
178
184
 
@@ -184,4 +190,4 @@ class ICManager:
184
190
  references[unit] = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}"
185
191
  else:
186
192
  references[unit] = f"{self.reference_prefix}{chr(ord('A') + unit - 1)}"
187
- return references
193
+ return references