Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for updating children units symbol ID #41

Merged
merged 9 commits into from
Feb 14, 2023
70 changes: 64 additions & 6 deletions src/kiutils/symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from dataclasses import dataclass, field
from typing import Optional, List
from os import path
import re

from kiutils.items.common import Effects, Position, Property, Font
from kiutils.items.syitems import *
Expand Down Expand Up @@ -189,10 +190,41 @@ class Symbol():
https://dev-docs.kicad.org/en/file-formats/sexpr-intro/index.html#_symbols
"""

id: str = ""
"""Each symbol must have a unique "LIBRARY_ID" for each top level symbol in the library or a unique
"UNIT_ID" for each unit embedded in a parent symbol. Library identifiers are only valid it top
level symbols and unit identifiers are on valid as unit symbols inside a parent symbol."""
@property
def id(self):
return self._id
eeintech marked this conversation as resolved.
Show resolved Hide resolved
@id.setter
def id(self, symbol_id):
self._id = symbol_id
eeintech marked this conversation as resolved.
Show resolved Hide resolved

library_identifier = re.match(r"^(\w+):(.+)$", self._id)
eeintech marked this conversation as resolved.
Show resolved Hide resolved
if library_identifier:
# Split library indentifier into library nickname and entry name
self.libraryNickname = library_identifier.group(1)
self.entryName = library_identifier.group(2)
if self.entryName:
symbol_name = self.entryName
else:
symbol_name = self._id

# Update Value property
for property in self.properties:
eeintech marked this conversation as resolved.
Show resolved Hide resolved
eeintech marked this conversation as resolved.
Show resolved Hide resolved
if property.key == 'Value':
property.value = symbol_name

# Update units parent symbol name
for unit in self.units:
unit.parentSymbolName = symbol_name
eeintech marked this conversation as resolved.
Show resolved Hide resolved

libraryNickname: Optional[str] = None
entryName: Optional[str] = None
""" The schematic symbol library and printed circuit board footprint library file formats use library identifiers.
Library identifiers are defined as a quoted string using the "LIBRARY_NICKNAME:ENTRY_NAME" format where
"LIBRARY_NICKNAME" is the nickname of the library in the symbol or footprint library table and
"ENTRY_NAME" is the name of the symbol or footprint in the library separated by a colon. """

extends: Optional[str] = None
"""The optional `extends` token attribute defines the "LIBRARY_ID" of another symbol inside the
Expand Down Expand Up @@ -243,9 +275,18 @@ class Symbol():
units: List = field(default_factory=list)
"""The `units` can be one or more child symbol tokens embedded in a parent symbol"""

parentSymbolName: Optional[str] = None
eeintech marked this conversation as resolved.
Show resolved Hide resolved
"""The parent symbol name each unit belongs to"""

unitId: Optional[int] = None
"""Unit identifier: an integer that identifies which unit the symbol represents"""

styleId: Optional[int] = None
"""Style identifier: indicates which body style the unit represents"""

@classmethod
def from_sexpr(cls, exp: list):
"""Convert the given S-Expresstion into a Symbol object
"""Convert the given S-Expression into a Symbol object

Args:
exp (list): Part of parsed S-Expression `(symbol ...)`
Expand Down Expand Up @@ -280,8 +321,20 @@ def from_sexpr(cls, exp: list):
if item[0] == 'in_bom': object.inBom = True if item[1] == 'yes' else False
if item[0] == 'on_board': object.onBoard = True if item[1] == 'yes' else False
if item[0] == 'power': object.isPower = True

if item[0] == 'symbol': object.units.append(Symbol().from_sexpr(item))

if item[0] == 'symbol':
eeintech marked this conversation as resolved.
Show resolved Hide resolved
# Create a new subsymbol and parse its unit and style identifiers
symbol_unit = Symbol().from_sexpr(item)
if object.entryName:
symbol_unit.parentSymbolName = object.entryName
else:
symbol_unit.parentSymbolName = object.id
symbol_id_parse = re.match(r"^" + re.escape(symbol_unit.parentSymbolName) + r"_(\d+?)_(\d+?)$", symbol_unit.id)
if not symbol_id_parse:
raise Exception(f'Failed to parse symbol unit identifiers due to invalid format: {symbol_unit.id=}')
symbol_unit.unitId = int(symbol_id_parse.group(1))
symbol_unit.styleId = int(symbol_id_parse.group(2))
object.units.append(symbol_unit)
if item[0] == 'property': object.properties.append(Property().from_sexpr(item))

if item[0] == 'pin': object.pins.append(SymbolPin().from_sexpr(item))
Expand Down Expand Up @@ -353,8 +406,13 @@ def to_sexpr(self, indent: int = 2, newline: bool = True) -> str:
pinnames = f' (pin_names{pnoffset}{pnhide})' if self.pinNames else ''
pinnumbers = f' (pin_numbers hide)' if self.hidePinNumbers else ''
extends = f' (extends "{dequote(self.extends)}")' if self.extends is not None else ''

expression = f'{indents}(symbol "{dequote(self.id)}"{extends}{power}{pinnumbers}{pinnames}{inbom}{onboard}\n'

# Construct Symbol Unit Identifier
symbol_id = dequote(self.id)
if self.parentSymbolName is not None and self.unitId is not None and self.styleId is not None:
eeintech marked this conversation as resolved.
Show resolved Hide resolved
symbol_id = f'{self.parentSymbolName}_{self.unitId}_{self.styleId}'

expression = f'{indents}(symbol "{symbol_id}"{extends}{power}{pinnumbers}{pinnames}{inbom}{onboard}\n'
for item in self.properties:
expression += item.to_sexpr(indent+2)
for item in self.graphicItems:
Expand Down
20 changes: 19 additions & 1 deletion tests/test_symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_createNewSymbolInEmptyLibrary(self):
self.testData.compareToTestFile = True
self.testData.pathToTestFile = path.join(SYMBOL_BASE, 'test_createNewSymbolInEmptyLibrary')

# Create an empty symbol libraray
# Create an empty symbol library
symbolLib = SymbolLib(
version = KIUTILS_CREATE_NEW_VERSION_STR,
generator = 'kiutils'
Expand All @@ -88,3 +88,21 @@ def test_createNewSymbolInEmptyLibrary(self):

self.assertTrue(to_file_and_compare(symbolLib, self.testData))

def test_renameSymbol(self):
"""Rename symbol inside library and verify all units are also renamed"""
self.testData.compareToTestFile = True
self.testData.pathToTestFile = path.join(SYMBOL_BASE, 'test_renameSymbol')
symbolLib = SymbolLib().from_file(path.join(SYMBOL_BASE, 'test_symbolDemorganSyItems'))
symbol = symbolLib.symbols[0]
symbol.id = 'AD1853JRS'
self.assertTrue(to_file_and_compare(symbolLib, self.testData))

def test_mergeLibraries(self):
"""Merge two symbol libraries together"""
self.testData.compareToTestFile = True
self.testData.pathToTestFile = path.join(SYMBOL_BASE, 'test_mergedLibraries')
symbolLib1 = SymbolLib().from_file(path.join(SYMBOL_BASE, 'test_analogDACs'))
symbolLib2 = SymbolLib().from_file(path.join(SYMBOL_BASE, 'test_symbolDemorganSyItems'))
for symbol in symbolLib2.symbols:
symbolLib1.symbols.insert(0, symbol)
self.assertTrue(to_file_and_compare(symbolLib1, self.testData))
Loading