import os
import numpy as np
[docs]class IniError(Exception):
pass
[docs]class IniFile:
"""
Class for storing option parameter values and reading/saving to file
Unlike standard .ini files, IniFile allows inheritance, in that a .ini file can use
INCLUDE(..) and DEFAULT(...) to include or override settings in another file (to avoid duplication)
:ivar params: dictionary of name, values stored
:ivar comments: dictionary of optional comments for parameter names
"""
def __init__(self, settings=None, keep_includes=False, expand_environment_variables=True):
"""
:param settings: a filename of a .ini file to read, or a dictionary of name/values
:param keep_includes:
- False: load all INCLUDE and DEFAULT files, making one params dictionary
- True: only load settings in main file, and store INCLUDE and DEFAULT entries into defaults
and includes filename lists.
:param expand_environment_variables: whether to expand $(var) placeholders in parameter values
using environment variables
"""
self.params = dict()
self.comments = dict()
self.readOrder = []
self.defaults = []
self.includes = []
self.original_filename = None
self.expand_environment_variables = expand_environment_variables
if isinstance(settings, str):
self.readFile(settings, keep_includes)
elif settings:
self.params.update(settings)
[docs] def expand_placeholders(self, s):
"""Expand shell variables of the forms $(var), like in Makefiles"""
if '$(' not in s:
return s
res = ''
index = 0
pathlen = len(s)
while index < pathlen:
c = s[index]
if c == '$':
if s[index + 1] == '$':
res = res + c
index += 1
elif s[index + 1] == '(':
s = s[index + 2:]
pathlen = len(s)
index = s.index(')')
var = s[:index]
if var in os.environ:
res += os.environ[var]
else:
res = res + c
index += 1
return res
def readFile(self, filename, keep_includes=False, if_not_defined=False):
try:
fileincludes = []
filedefaults = []
self.original_filename = filename
comments = []
with open(filename, encoding='utf-8-sig') as textFileHandle:
# Remove blank lines and comment lines from the python list of lists.
for line in textFileHandle:
s = line.strip()
if s == 'END':
break
if s.startswith('#'):
comments.append(s[1:].rstrip())
continue
elif s.startswith('INCLUDE('):
fileincludes.append(s[s.find('(') + 1:s.rfind(')')])
elif s.startswith('DEFAULT('):
filedefaults.append(s[s.find('(') + 1:s.rfind(')')])
elif s != '':
eq = s.find('=')
if eq >= 0:
key = s[0:eq].strip()
if key in self.params:
if if_not_defined:
continue
raise IniError('Error: duplicate key: ' + key + ' in ' + filename)
value = s[eq + 1:].strip()
if self.expand_environment_variables:
value = self.expand_placeholders(value)
self.params[key] = value
self.readOrder.append(key)
if len(comments):
self.comments[key] = comments
if not s.startswith('#'):
comments = []
if keep_includes:
self.includes += fileincludes
self.defaults += filedefaults
else:
for ffile in fileincludes:
if os.path.isabs(ffile):
self.readFile(ffile, if_not_defined=if_not_defined)
else:
self.readFile(os.path.join(os.path.dirname(filename), ffile), if_not_defined=if_not_defined)
for ffile in filedefaults:
if os.path.isabs(ffile):
self.readFile(ffile, if_not_defined=True)
else:
self.readFile(os.path.join(os.path.dirname(filename), ffile), if_not_defined=True)
return self.params
except:
print('Error in ' + filename)
raise
def __str__(self):
return "\n".join(self.fileLines())
[docs] def saveFile(self, filename=None):
"""
Save to a .ini file
:param filename: name of file to save to
"""
if not filename:
filename = self.original_filename
if not filename:
raise IniError('No filename for iniFile.saveFile()')
with open(filename, 'w', encoding='utf-8') as f:
f.write(str(self))
def fileLines(self):
def asIniText(value):
if isinstance(value, type('')):
return value
if type(value) == bool:
return str(value)[0]
return str(value)
parameterLines = []
for include in self.includes:
parameterLines.append('INCLUDE(' + include + ')')
for default in self.defaults:
parameterLines.append('DEFAULT(' + default + ')')
keys = list(self.params.keys())
keys.sort()
for key in self.readOrder:
if key in keys:
parameterLines.append(key + '=' + asIniText(self.params[key]))
keys.remove(key)
for key in keys:
parameterLines.append(key + '=' + asIniText(self.params[key]))
return parameterLines
def replaceTags(self, placeholder, text):
for key in self.params:
self.params[key] = self.params[key].replace(placeholder, text)
return self.params
def delete_keys(self, keys):
for k in keys:
self.params.pop(k, None)
def _undefined(self, name):
raise IniError('parameter not defined: ' + name)
[docs] def hasKey(self, name):
"""
Test if key name exists
:param name: parameter name
:return: True or False test if key name exists
"""
return name in self.params
[docs] def isSet(self, name, allowEmpty=False):
"""
Tests whether value for name is set or is empty
:param name: name of parameter
:param allowEmpty: whether to allow empty strings (return True is parameter name exists but is not set, "x = ")
"""
return name in self.params and (allowEmpty or self.params[name] != "")
def asType(self, name, tp, default=None, allowEmpty=False):
if self.isSet(name, allowEmpty):
if tp == bool:
return self.bool(name, default)
elif tp == list:
return self.split(name, default)
elif tp == np.ndarray:
return self.ndarray(name, default)
else:
return tp(self.params[name])
elif default is not None:
return default
else:
self._undefined(name)
[docs] def setAttr(self, name, instance, default=None, allowEmpty=False):
"""
Set attribute of an object to value of parameter, using same type as existing value or default
:param name: parameter name
:param instance: instance of an object, so instance.name is the value to set
:param default: default value if instance.name does not exist
:param allowEmpty: whether to allow empty values
"""
default = getattr(instance, name, default)
setattr(instance, name, self.asType(name, type(default), default, allowEmpty=allowEmpty))
def getAttr(self, instance, name, default=None, comment=None):
val = getattr(instance, name, default)
self.params[name] = val
if comment:
self.comments[name] = comment
[docs] def bool(self, name, default=False):
"""
Get boolean value
:param name: parameter name
:param default: default value if not set
"""
if self.isSet(name):
s = self.params[name]
if isinstance(s, bool):
return s
if s[0] == 'T':
return True
elif s[0] == 'F':
return False
raise IniError('parameter does not have valid T(rue) or F(alse) boolean value: ' + name)
elif default is not None:
return default
else:
self._undefined(name)
[docs] def bool_list(self, name, default=None):
"""
Get list of boolean values, e.g. from name = T F T
:param name: parameter name
:param default: default value if not set
"""
if not default:
default = []
return self.split(name, default, tp=bool)
[docs] def string(self, name, default=None, allowEmpty=True):
"""
Get string value
:param name: parameter name
:param default: default value if not set
:param allowEmpty: whether to return empty string if value is empty (otherwise return default)
"""
return self.asType(name, str, default, allowEmpty=allowEmpty)
[docs] def list(self, name, default=None, tp=None):
"""
Get list (from space-separated values)
:param name: parameter name
:param default: default value
:param tp: type for each member of the list
"""
if not default:
default = []
return self.split(name, default, tp)
[docs] def float(self, name, default=None):
"""
Get float value
:param name: parameter name
:param default: default value
"""
return self.asType(name, float, default)
[docs] def float_list(self, name, default=None):
"""
Get list of float values
:param name: parameter name
:param default: default value
"""
if not default:
default = []
return self.split(name, default, tp=float)
[docs] def int(self, name, default=None):
"""
Get int value
:param name: parameter name
:param default: default value
"""
return self.asType(name, int, default)
[docs] def int_list(self, name, default=None):
"""
Get list of int values
:param name: parameter name
:param default: default value
"""
if not default:
default = []
return self.split(name, default, tp=int)
[docs] def split(self, name, default=None, tp=None):
"""
Gets a list of values, optionally cast to type tp
:param name: parameter name
:param default: default value
:param tp: type for each list member
"""
if name in self.params and isinstance(self.params[name], (list, tuple)):
if tp is None:
return self.params[name]
else:
return [tp(x) for x in self.params[name]]
s = self.string(name, default)
if isinstance(s, str):
if tp is not None:
return [tp(x) for x in s.split()]
return s.split()
else:
return s
[docs] def ndarray(self, name, default=None, tp=np.float64):
"""
Get numpy array of values
:param name: parameter name
:param default: default value
:param tp: type for array
"""
return np.array(self.split(name, default, tp=tp))
[docs] def array_int(self, name, index=1, default=None):
"""
Get one int value, for entries of the form name(index)
:param name: base parameter name
:param index: index (in brackets)
:param default: default value
"""
return self.int(name + '(%u)' % index, default)
[docs] def array_string(self, name, index=1, default=None):
"""
Get one str value, for entries of the form name(index)
:param name: base parameter name
:param index: index (in brackets)
:param default: default value
"""
return self.string(name + '(%u)' % index, default)
[docs] def array_bool(self, name, index=1, default=None):
"""
Get one boolean value, for entries of the form name(index)
:param name: base parameter name
:param index: index (in brackets)
:param default: default value
"""
return self.bool(name + '(%u)' % index, default)
[docs] def array_float(self, name, index=1, default=None):
"""
Get one float value, for entries of the form name(index)
:param name: base parameter name
:param index: index (in brackets)
:param default: default value
"""
return self.float(name + '(%u)' % index, default)
def relativeFileName(self, name, default=None):
s = self.string(name, default)
if not os.path.isabs(s) and self.original_filename is not None:
return os.path.join(os.path.dirname(self.original_filename), s)
return s