v1.2
* Added ability to group components by any arbitrary fields (set in bom.ini file) * User can determine whether blank fields are merged (or not) * Improved part grouping * User can set a 'Configuration' field to determine whether a part is loaded or not under certain circumstances * Cleanup of preferences management * Added version number
This commit is contained in:
parent
75c46504d9
commit
0afa7bb26e
|
|
@ -35,14 +35,6 @@ class Component():
|
|||
#no match, return False
|
||||
return False
|
||||
|
||||
#compare footprint with another component
|
||||
def compareFootprint(self, other):
|
||||
return self.getFootprint().lower() == other.getFootprint().lower()
|
||||
|
||||
#compare the component library of this part to another part
|
||||
def compareLibName(self, other):
|
||||
return self.getLibName().lower() == other.getLibName().lower()
|
||||
|
||||
#determine if two parts have the same name
|
||||
def comparePartName(self, other):
|
||||
pn1 = self.getPartName().lower()
|
||||
|
|
@ -57,24 +49,43 @@ class Component():
|
|||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def compareField(self, other, field):
|
||||
|
||||
this_field = self.getField(field).lower()
|
||||
other_field = other.getField(field).lower()
|
||||
|
||||
if this_field == other_field: return True
|
||||
|
||||
#if blank comparisons are allowed
|
||||
if self.prefs.mergeBlankFields:
|
||||
if this_field == "" or other_field == "":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
#Equivalency operator is used to determine if two parts are 'equal'
|
||||
def __eq__(self, other):
|
||||
"""Equlivalency operator, remember this can be easily overloaded"""
|
||||
|
||||
valueResult = self.compareValue(other)
|
||||
|
||||
#if connector comparison is overridden, set valueResult to True
|
||||
if self.prefs.groupConnectors:
|
||||
if "conn" in self.getDescription().lower():
|
||||
valueResult = True
|
||||
|
||||
if self.prefs.compareFootprints:
|
||||
fpResult = self.compareFootprint(other)
|
||||
else:
|
||||
fpResult = True
|
||||
results = []
|
||||
|
||||
#'fitted' value must be the same for both parts
|
||||
results.append(self.isFitted() == other.isFitted())
|
||||
|
||||
for c in self.prefs.groups:
|
||||
#perform special matches
|
||||
if c.lower() == ColumnList.COL_VALUE.lower():
|
||||
results.append(self.compareValue(other))
|
||||
#match part name
|
||||
elif c.lower() == ColumnList.COL_PART.lower():
|
||||
results.append(self.comparePartName(other))
|
||||
|
||||
return valueResult and fpResult and self.compareLibName(other) and self.comparePartName(other) and self.isFitted() == other.isFitted()
|
||||
#generic match
|
||||
else:
|
||||
results.append(self.compareField(other, c))
|
||||
|
||||
return all(results)
|
||||
|
||||
def setLibPart(self, part):
|
||||
self.libpart = part
|
||||
|
|
@ -107,7 +118,7 @@ class Component():
|
|||
def getValue(self):
|
||||
return self.element.get("value")
|
||||
|
||||
def getField(self, name, libraryToo=True):
|
||||
def getField(self, name, ignoreCase=True, libraryToo=True):
|
||||
"""Return the value of a field named name. The component is first
|
||||
checked for the field, and then the components library part is checked
|
||||
for the field. If the field doesn't exist in either, an empty string is
|
||||
|
|
@ -118,10 +129,40 @@ class Component():
|
|||
libraryToo -- look in the libpart's fields for the same name if not found
|
||||
in component itself
|
||||
"""
|
||||
|
||||
|
||||
#special fields
|
||||
|
||||
if name.lower() == ColumnList.COL_REFERENCE.lower():
|
||||
return self.getRef()
|
||||
|
||||
if name.lower() == ColumnList.COL_DESCRIPTION.lower():
|
||||
return self.getDescription()
|
||||
|
||||
if name.lower() == ColumnList.COL_DATASHEET.lower():
|
||||
return self.getDatasheet()
|
||||
|
||||
if name.lower() == ColumnList.COL_FP.lower():
|
||||
return self.getFootprint().split(":")[0]
|
||||
|
||||
if name.lower() == ColumnList.COL_FP.lower():
|
||||
return self.getFootprint().split(":")[1]
|
||||
|
||||
if name.lower() == ColumnList.COL_VALUE.lower():
|
||||
return self.getValue()
|
||||
|
||||
if name.lower() == ColumnList.COL_PART.lower():
|
||||
return self.getPartName()
|
||||
|
||||
if name.lower() == ColumnList.COL_PART_LIB.lower():
|
||||
return self.getLibName()
|
||||
|
||||
|
||||
field = self.element.get("field", "name", name)
|
||||
|
||||
if field == "" and libraryToo:
|
||||
field = self.libpart.getField(name)
|
||||
|
||||
return field
|
||||
|
||||
def getFieldNames(self):
|
||||
|
|
@ -142,13 +183,31 @@ class Component():
|
|||
|
||||
#determine if a component is FITTED or not
|
||||
def isFitted(self):
|
||||
|
||||
check = [self.getValue().lower(), self.getField("Notes").lower()]
|
||||
|
||||
for item in check:
|
||||
if any([dnf in item for dnf in DNF]): return False
|
||||
|
||||
return True
|
||||
|
||||
check = self.getField(self.prefs.configField).lower()
|
||||
|
||||
#check the value field first
|
||||
if self.getValue().lower() in DNF or check.lower() in DNF:
|
||||
return False
|
||||
|
||||
if check == "":
|
||||
return True #empty is fitted
|
||||
|
||||
opts = check.split(",")
|
||||
|
||||
result = True
|
||||
|
||||
for opt in opts:
|
||||
#options that start with '-' are explicitly removed from certain configurations
|
||||
if opt.startswith('-') and opt[1:].lower() == self.prefs.pcbConfig.lower():
|
||||
result = False
|
||||
break
|
||||
if opt.startswith("+"):
|
||||
if opt.lower() == self.prefs.pcbConfig.lower():
|
||||
result = True
|
||||
|
||||
#by default, part is fitted
|
||||
return result
|
||||
|
||||
def getFootprint(self, libraryToo=True):
|
||||
ret = self.element.get("footprint")
|
||||
|
|
@ -180,6 +239,7 @@ class ComponentGroup():
|
|||
self.prefs = prefs
|
||||
|
||||
def getField(self, field):
|
||||
|
||||
if not field in self.fields.keys(): return ""
|
||||
if not self.fields[field]: return ""
|
||||
return u''.join((self.fields[field]))
|
||||
|
|
@ -191,6 +251,8 @@ class ComponentGroup():
|
|||
def matchComponent(self, c):
|
||||
if len(self.components) == 0: return True
|
||||
if c == self.components[0]: return True
|
||||
|
||||
return False
|
||||
|
||||
#test if a given component is already contained in this grop
|
||||
def containsComponent(self, c):
|
||||
|
|
@ -260,6 +322,7 @@ class ComponentGroup():
|
|||
self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}".format(
|
||||
n=q,
|
||||
dnf = " (DNF)" if not self.isFitted() else "")
|
||||
|
||||
self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.boards) if self.isFitted() else "0"
|
||||
self.fields[ColumnList.COL_VALUE] = self.components[0].getValue()
|
||||
self.fields[ColumnList.COL_PART] = self.components[0].getPartName()
|
||||
|
|
@ -278,7 +341,11 @@ class ComponentGroup():
|
|||
#return True if none match (i.e. this group is OK)
|
||||
#retunr False if any match
|
||||
def testRegex(self):
|
||||
|
||||
return True
|
||||
#run the excusion
|
||||
|
||||
"""
|
||||
for key in self.prefs.regex.keys():
|
||||
reg = self.prefs.regex[key]
|
||||
if not type(reg) in [str, list]: continue #regex must be a string, or a list of strings
|
||||
|
|
@ -303,6 +370,7 @@ class ComponentGroup():
|
|||
return False
|
||||
|
||||
return True
|
||||
"""
|
||||
|
||||
|
||||
#return a dict of the KiCAD data based on the supplied columns
|
||||
|
|
|
|||
|
|
@ -12,46 +12,57 @@ class BomPref:
|
|||
|
||||
SECTION_IGNORE = "IGNORE_COLUMNS"
|
||||
SECTION_GENERAL = "BOM_OPTIONS"
|
||||
SECTION_EXCLUDE_VALUES = "EXCLUDE_COMPONENT_VALUES"
|
||||
SECTION_EXCLUDE_REFS = "EXCLUDE_COMPONENT_REFS"
|
||||
SECTION_EXCLUDE_FP = "EXCLUDE_COMPONENT_FP"
|
||||
SECTION_EXCLUDE_PART = "EXCLUDE_COMPONENT_PART"
|
||||
SECTION_EXCLUDE_DESC = "EXCLUDE_COMPONENT_DESC"
|
||||
SECTION_ALIASES = "COMPONENT_ALIASES"
|
||||
SECTION_CONFIGURATIONS = "PCB_CONFIGURATIONS"
|
||||
SECTION_GROUPING_FIELDS = "GROUP_FIELDS"
|
||||
SECTION_REGEXCLUDES = "REGEX_EXCLUDE"
|
||||
SECTION_REGINCLUDES = "REGEX_INCLUDE"
|
||||
|
||||
OPT_IGNORE_DNF = "ignore_dnf"
|
||||
OPT_PCB_CONFIG = "pcb_configuration"
|
||||
OPT_NUMBER_ROWS = "number_rows"
|
||||
OPT_GROUP_CONN = "group_connectors"
|
||||
OPT_USE_REGEX = "test_regex"
|
||||
OPT_COMP_FP = "compare_footprints"
|
||||
OPT_INC_PRICE = "calculate_price"
|
||||
|
||||
#list of columns which we can use regex on
|
||||
COL_REG_EX = [
|
||||
ColumnList.COL_REFERENCE,
|
||||
ColumnList.COL_DESCRIPTION,
|
||||
ColumnList.COL_VALUE,
|
||||
ColumnList.COL_FP,
|
||||
ColumnList.COL_FP_LIB,
|
||||
ColumnList.COL_PART,
|
||||
ColumnList.COL_PART_LIB
|
||||
]
|
||||
OPT_MERGE_BLANK = "merge_blank_fields"
|
||||
OPT_IGNORE_DNF = "ignore_dnf"
|
||||
|
||||
OPT_CONFIG_FIELD = "configuration_field"
|
||||
|
||||
def __init__(self):
|
||||
self.ignore = [
|
||||
ColumnList.COL_PART_LIB,
|
||||
ColumnList.COL_FP_LIB,
|
||||
] #list of headings to ignore in BoM generation
|
||||
self.ignoreDNF = False #ignore rows for do-not-fit parts
|
||||
self.ignoreDNF = True #ignore rows for do-not-fit parts
|
||||
self.numberRows = True #add row-numbers to BoM output
|
||||
self.groupConnectors = True #group connectors and ignore component value
|
||||
self.useRegex = True #Test various columns with regex
|
||||
self.compareFootprints = True #test footprints when comparing components
|
||||
self.boards = 1
|
||||
self.mergeBlankFields = True #blanks fields will be merged when possible
|
||||
self.hideHeaders = False
|
||||
self.verbose = False #by default, is not verbose
|
||||
self.configurations = [] #list of various configurations
|
||||
self.configField = "Config" #default field used for part fitting config
|
||||
self.pcbConfig = "default"
|
||||
|
||||
#default fields used to group components
|
||||
self.groups = [
|
||||
ColumnList.COL_PART,
|
||||
ColumnList.COL_PART_LIB,
|
||||
ColumnList.COL_VALUE,
|
||||
ColumnList.COL_FP,
|
||||
ColumnList.COL_FP_LIB,
|
||||
#user can add custom grouping columns in bom.ini
|
||||
]
|
||||
|
||||
self.regIncludes = [] #none by default
|
||||
|
||||
self.regExcludes = [
|
||||
[ColumnList.COL_REFERENCE,'TP[0-9]'],
|
||||
[ColumnList.COL_PART,'mount[\s-_]*hole'],
|
||||
[ColumnList.COL_PART,'solder[\s-_]*bridge'],
|
||||
[ColumnList.COL_PART,'test[\s-_]*point'],
|
||||
[ColumnList.COL_FP,'test[\s-_]*point'],
|
||||
[ColumnList.COL_FP,'mount[\s-_]*hole'],
|
||||
[ColumnList.COL_FP,'fiducial'],
|
||||
]
|
||||
|
||||
#default component groupings
|
||||
self.aliases = [
|
||||
|
|
@ -62,29 +73,6 @@ class BomPref:
|
|||
["zener","zenersmall"],
|
||||
["d","diode","d_small"]
|
||||
]
|
||||
|
||||
#dictionary of possible regex expressions for ignoring component row(s)
|
||||
self.regex = dict.fromkeys(self.COL_REG_EX)
|
||||
|
||||
#default regex values
|
||||
self.regex[ColumnList.COL_REFERENCE] = [
|
||||
'TP[0-9]+',
|
||||
]
|
||||
|
||||
self.regex[ColumnList.COL_PART] = [
|
||||
'mounthole',
|
||||
'scopetest',
|
||||
'mount_hole',
|
||||
'solder_bridge',
|
||||
'test_point',
|
||||
]
|
||||
|
||||
self.regex[ColumnList.COL_FP] = [
|
||||
'mounthole'
|
||||
]
|
||||
|
||||
def columnToGroup(self, col):
|
||||
return "REGEXCLUDE_" + col.upper().replace(" ","_")
|
||||
|
||||
#check an option within the SECTION_GENERAL group
|
||||
def checkOption(self, parser, opt, default=False):
|
||||
|
|
@ -102,6 +90,7 @@ class BomPref:
|
|||
|
||||
with open(file, 'rb') as configfile:
|
||||
cf = ConfigParser.RawConfigParser(allow_no_value = True)
|
||||
cf.optionxform=str
|
||||
|
||||
cf.read(file)
|
||||
|
||||
|
|
@ -111,12 +100,18 @@ class BomPref:
|
|||
self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True)
|
||||
self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True)
|
||||
self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True)
|
||||
self.compareFootprints = self.checkOption(cf, self.OPT_COMP_FP, default=True)
|
||||
|
||||
#read out configurations
|
||||
if self.SECTION_CONFIGURATIONS in cf.sections():
|
||||
self.configurations = [i for i in cf.options(self.SECTION_CONFIGURATIONS)]
|
||||
|
||||
self.mergeBlankFields = self.checkOption(cf, self.OPT_MERGE_BLANK, default = True)
|
||||
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_PCB_CONFIG):
|
||||
self.pcbConfig = cf.get(self.SECTION_GENERAL, self.OPT_PCB_CONFIG)
|
||||
|
||||
if cf.has_option(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD):
|
||||
self.configField = cf.get(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD)
|
||||
|
||||
#read out grouping colums
|
||||
if self.SECTION_GROUPING_FIELDS in cf.sections():
|
||||
self.groups = [i for i in cf.options(self.SECTION_GROUPING_FIELDS)]
|
||||
|
||||
#read out ignored-rows
|
||||
if self.SECTION_IGNORE in cf.sections():
|
||||
self.ignore = [i for i in cf.options(self.SECTION_IGNORE)]
|
||||
|
|
@ -125,12 +120,11 @@ class BomPref:
|
|||
if self.SECTION_ALIASES in cf.sections():
|
||||
self.aliases = [a.split(" ") for a in cf.options(self.SECTION_ALIASES)]
|
||||
|
||||
#read out the regex
|
||||
for key in self.regex.keys():
|
||||
section = self.columnToGroup(key)
|
||||
if section in cf.sections():
|
||||
self.regex[key] = [r for r in cf.options(section)]
|
||||
|
||||
if self.SECTION_REGEXCLUDES in cf.sections():
|
||||
pass
|
||||
|
||||
if self.SECTION_REGINCLUDES in cf.sections():
|
||||
pass
|
||||
|
||||
#add an option to the SECTION_GENRAL group
|
||||
def addOption(self, parser, opt, value, comment=None):
|
||||
|
|
@ -145,6 +139,7 @@ class BomPref:
|
|||
file = os.path.abspath(file)
|
||||
|
||||
cf = ConfigParser.RawConfigParser(allow_no_value = True)
|
||||
cf.optionxform=str
|
||||
|
||||
cf.add_section(self.SECTION_GENERAL)
|
||||
cf.set(self.SECTION_GENERAL, "; General BoM options here")
|
||||
|
|
@ -152,7 +147,14 @@ class BomPref:
|
|||
self.addOption(cf, self.OPT_NUMBER_ROWS, self.numberRows, comment="If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS))
|
||||
self.addOption(cf, self.OPT_GROUP_CONN, self.groupConnectors, comment="If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN))
|
||||
self.addOption(cf, self.OPT_USE_REGEX, self.useRegex, comment="If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX))
|
||||
self.addOption(cf, self.OPT_COMP_FP, self.compareFootprints, comment="If '{opt}' option is set to 1, two components must have the same footprint to be grouped together. If '{opt}' is not set, then footprint comparison is ignored.".format(opt=self.OPT_COMP_FP))
|
||||
self.addOption(cf, self.OPT_MERGE_BLANK, self.mergeBlankFields, comment="If '{opt}' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible".format(opt=self.OPT_MERGE_BLANK))
|
||||
|
||||
cf.set(self.SECTION_GENERAL, '; Field name used to determine if a particular part is to be fitted')
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD, self.configField)
|
||||
|
||||
cf.set(self.SECTION_GENERAL, "; Configuration string used to determine which components are loaded on a particular board")
|
||||
cf.set(self.SECTION_GENERAL, '; Configuration string is case-insensitive')
|
||||
cf.set(self.SECTION_GENERAL, self.OPT_PCB_CONFIG, self.pcbConfig)
|
||||
|
||||
cf.add_section(self.SECTION_IGNORE)
|
||||
cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM")
|
||||
|
|
@ -161,36 +163,40 @@ class BomPref:
|
|||
for i in self.ignore:
|
||||
cf.set(self.SECTION_IGNORE, i)
|
||||
|
||||
cf.add_section(self.SECTION_CONFIGURATIONS)
|
||||
cf.set(self.SECTION_CONFIGURATIONS, '; List of PCB configuration parameters')
|
||||
for i in self.configurations:
|
||||
cf.set(self.SECTION_CONFIGURATIONS, i)
|
||||
|
||||
#write the component grouping fields
|
||||
cf.add_section(self.SECTION_GROUPING_FIELDS)
|
||||
cf.set(self.SECTION_GROUPING_FIELDS, '; List of fields used for sorting individual components into groups')
|
||||
cf.set(self.SECTION_GROUPING_FIELDS, '; Components which match (comparing *all* fields) will be grouped together')
|
||||
cf.set(self.SECTION_GROUPING_FIELDS, '; Field names are CASE-SENSITIVE!')
|
||||
|
||||
for i in self.groups:
|
||||
cf.set(self.SECTION_GROUPING_FIELDS, i)
|
||||
|
||||
cf.add_section(self.SECTION_ALIASES)
|
||||
cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name")
|
||||
cf.set(self.SECTION_ALIASES, "; Each line represents a space-separated list of equivalent component name values")
|
||||
cf.set(self.SECTION_ALIASES, "; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together")
|
||||
cf.set(self.SECTION_ALIASES, '; Aliases are case-insensitive')
|
||||
|
||||
for a in self.aliases:
|
||||
cf.set(self.SECTION_ALIASES, " ".join(a))
|
||||
|
||||
cf.add_section(self.SECTION_REGINCLUDES)
|
||||
cf.set(self.SECTION_REGINCLUDES, '; A series of regular expressions used to include parts in the BoM')
|
||||
cf.set(self.SECTION_REGINCLUDES, '; Column names are case-insensitive')
|
||||
for i in self.regIncludes:
|
||||
if not len(i) == 2: continue
|
||||
|
||||
for col in self.regex.keys():
|
||||
cf.set(self.SECTION_REGINCLUDE, i[0], i[1])
|
||||
|
||||
reg = self.regex[col]
|
||||
|
||||
section = self.columnToGroup(col)
|
||||
cf.add_section(section)
|
||||
#comments
|
||||
cf.set(section, "; A list of regex to compare against the '{col}' column".format(col=col))
|
||||
cf.set(section, "; If the value in the '{col}' column matches any of these expressions, the row will be excluded from the BoM".format(col=col))
|
||||
|
||||
if type(reg) == str:
|
||||
cf.set(section, reg)
|
||||
|
||||
elif type(reg) == list:
|
||||
for r in reg:
|
||||
cf.set(section, r)
|
||||
cf.add_section(self.SECTION_REGEXCLUDES)
|
||||
cf.set(self.SECTION_REGEXCLUDES, '; A series of regular expressions used to exclude parts from the BoM')
|
||||
cf.set(self.SECTION_REGINCLUDES, '; Column names are case-insensitive')
|
||||
|
||||
for i in self.regExcludes:
|
||||
if not len(i) == 2: continue
|
||||
|
||||
cf.set(self.SECTION_REGEXCLUDES, i[0], i[1])
|
||||
|
||||
|
||||
with open(file, 'wb') as configfile:
|
||||
cf.write(configfile)
|
||||
|
|
@ -0,0 +1 @@
|
|||
KIBOM_VERSION = 1.2
|
||||
24
KiBOM_CLI.py
24
KiBOM_CLI.py
|
|
@ -32,9 +32,9 @@ parser = argparse.ArgumentParser(description="KiBOM Bill of Materials generator
|
|||
|
||||
parser.add_argument("netlist", help='xml netlist file. Use "%%I" when running from within KiCad')
|
||||
parser.add_argument("output", default="", help='BoM output file name.\nUse "%%O" when running from within KiCad to use the default output name (csv file).\nFor e.g. HTML output, use "%%O.html"')
|
||||
parser.add_argument("-b", "--boards", help="Number of boards to build (default = 1)", type=int, default=1)
|
||||
parser.add_argument("-n", "--number", help="Number of boards to build (default = 1)", type=int, default=1)
|
||||
parser.add_argument("-v", "--verbose", help="Enable verbose output", action='count')
|
||||
parser.add_argument("-n", "--noheader", help="Do not generate file headers; data only.", action='count')
|
||||
parser.add_argument("-r", "--revision", help="Board variant, used to determine which components are output to the BoM", type=str, default=None)
|
||||
parser.add_argument("--cfg", help="BoM config file (script will try to use 'bom.ini' if not specified here)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
|
@ -62,20 +62,22 @@ if args.cfg:
|
|||
#read preferences from file. If file does not exists, default preferences will be used
|
||||
pref = BomPref()
|
||||
|
||||
#pass various command-line options through
|
||||
pref.verbose = verbose
|
||||
pref.boards = args.boards
|
||||
if args.noheader:
|
||||
pref.hideHeaders = True
|
||||
|
||||
if os.path.exists(config_file):
|
||||
pref.Read(config_file)
|
||||
say("Config:",config_file)
|
||||
|
||||
#pass various command-line options through
|
||||
pref.verbose = verbose
|
||||
pref.boards = args.number
|
||||
|
||||
if args.revision is not None:
|
||||
pref.pcbConfig = args.revision
|
||||
print("PCB Revision:",args.revision)
|
||||
|
||||
|
||||
#write preference file back out (first run will generate a file with default preferences)
|
||||
if not os.path.exists(ini):
|
||||
pref.Write(ini)
|
||||
say("Writing preferences file bom.ini")
|
||||
pref.Write(ini)
|
||||
say("Writing preferences file bom.ini")
|
||||
|
||||
#individual components
|
||||
components = []
|
||||
|
|
|
|||
Loading…
Reference in New Issue