Module Gnumed.pycommon.gmCfgINI
GNUmed INI style configuration handling.
Am Thu, May 22, 2025 at 11:45:31PM -0400 schrieb Grant Edwards via Python-list:
There is sort of a traditional set of locations where applications look to find a config file:
$HOME/
$HOME/.config/
$HOME/.config/
The last two overried all of the others.
Config files that reside in $HOME/ usually start with a dot. Often they end in 'rc'. Config files in other directories usually don't start with a dot.
There's usually an
Many applications will parse two config files: a global one from /etc or /usr/local/ and a user-one from somewhere under $HOME.
Functions
def parse_INI_stream(stream=None, encoding=None)
-
Expand source code
def parse_INI_stream(stream=None, encoding=None): """Parse an iterable for INI-style data. Returns a dict by sections containing a dict of values per section. """ _log.debug('parsing INI-style data stream [%s] using [%s]', stream, encoding) if encoding is None: encoding = 'utf8' data = {} current_group = None current_option = None current_option_path = None inside_list = False line_idx = 0 for line in stream: if type(line) is bytes: line = line.decode(encoding) line = line.replace('\015', '').replace('\012', '').strip() line_idx += 1 if inside_list: if line == '$%s$' % current_option: # end of list inside_list = False continue data[current_option_path].append(line) continue # noise if line == '' or line.startswith('#') or line.startswith(';'): continue # group if line.startswith('['): if not line.endswith(']'): _log.error('group line does not end in "]", aborting') _log.error(line) raise ValueError('INI-stream parsing error') group = line.strip('[]').strip() if group == '': _log.error('group name is empty, aborting') _log.error(line) raise ValueError('INI-stream parsing error') current_group = group continue # option if current_group is None: _log.warning('option found before first group, ignoring') _log.error(line) continue name, remainder = regex.split(r'\s*[=:]\s*', line, maxsplit = 1) if name == '': _log.error('option name empty, aborting') _log.error(line) raise ValueError('INI-stream parsing error') if remainder.strip() == '': if ('=' not in line) and (':' not in line): _log.error('missing name/value separator (= or :), aborting') _log.error(line) raise ValueError('INI-stream parsing error') current_option = name current_option_path = '%s::%s' % (current_group, current_option) if current_option_path in data: _log.warning('duplicate option [%s]', current_option_path) value = remainder.split('#', 1)[0].strip() # start of list ? if value == '$%s$' % current_option: inside_list = True data[current_option_path] = [] continue data[current_option_path] = value if inside_list: _log.critical('unclosed list $%s$ detected at end of config stream [%s]', current_option, stream) raise SyntaxError('end of config stream but still in list') return data
Parse an iterable for INI-style data.
Returns a dict by sections containing a dict of values per section.
def set_option_in_INI_file(filename=None, group=None, option=None, value=None, encoding='utf8')
-
Expand source code
def set_option_in_INI_file(filename=None, group=None, option=None, value=None, encoding='utf8'): _log.debug('setting option "%s" to "%s" in group [%s]', option, value, group) _log.debug('file: %s (%s)', filename, encoding) sink = tempfile.NamedTemporaryFile(suffix = '.cfg', delete = True) sink_name = sink.name sink.close() # close it so it gets deleted so we can safely open it again src = open(filename, mode = 'rt', encoding = encoding) sink = open(sink_name, mode = 'wt', encoding = encoding) # is value a list ? if isinstance(value, type([])): __set_list_in_INI_file(src, sink, group, option, value) else: __set_opt_in_INI_file(src, sink, group, option, value) sink.close() src.close() shutil.copy2(sink_name, filename)
Classes
class gmCfgData
-
Expand source code
class gmCfgData(gmBorg.cBorg): def __init__(self): try: self.__cfg_data except AttributeError: self.__cfg_data = {} self.source_files = {} #-------------------------------------------------- def get(self, group=None, option=None, source_order=None): """Get the value of a configuration option in a config file. <source_order> the order in which config files are searched a list of tuples (source, policy) policy: return: return only this value immediately append: append to list of potential values to return extend: if the value per source happens to be a list extend (rather than append to) the result list returns NONE when there's no value for an option """ if source_order is None: source_order = [('internal', 'return')] results = [] for source, policy in source_order: _log.debug('searching "%s" in [%s] in %s', option, group, source) if group is None: group = source option_path = '%s::%s' % (group, option) try: source_data = self.__cfg_data[source] except KeyError: _log.error('invalid config source [%s]', source) _log.debug('currently known sources: %s', list(self.__cfg_data)) continue try: value = source_data[option_path] except KeyError: _log.debug('option [%s] not in group [%s] in source [%s]', option, group, source) continue _log.debug('option [%s] found in source [%s]', option_path, source) if policy == 'return': return value if policy == 'extend': if isinstance(value, type([])): results.extend(value) else: results.append(value) else: results.append(value) if len(results) == 0: return None return results #-------------------------------------------------- def set_option(self, option=None, value=None, group=None, source=None): """Set a particular option to a particular value. Note that this does NOT PERSIST the option anywhere ! """ if None in [option, value]: raise ValueError('neither <option> nor <value> can be None') if source is None: source = 'internal' try: self.__cfg_data[source] except KeyError: self.__cfg_data[source] = {} if group is None: group = source option_path = '%s::%s' % (group, option) self.__cfg_data[source][option_path] = value #-------------------------------------------------- # API: source related #-------------------------------------------------- def add_stream_source(self, source=None, stream=None, encoding=None): data = parse_INI_stream(stream = stream, encoding = encoding) if source in self.__cfg_data: _log.warning('overriding source <%s> with [%s]', source, stream) self.__cfg_data[source] = data #-------------------------------------------------- def add_file_source(self, source=None, filename=None, encoding='utf8'): """Add a source (a file) to the instance.""" _log.info('file source "%s": %s (%s)', source, filename, encoding) for existing_source, existing_file in self.source_files.items(): if existing_file == filename: if source != existing_source: _log.warning('file [%s] already known as source [%s]', filename, existing_source) _log.warning('adding it as source [%s] may provoke trouble', source) cfg_file = None if filename is not None: try: cfg_file = open(filename, mode = 'rt', encoding = encoding) except IOError: _log.error('cannot open [%s], keeping as dummy source', filename) if cfg_file is None: filename = None if source in self.__cfg_data: _log.warning('overriding source <%s> with dummy', source) self.__cfg_data[source] = {} else: self.add_stream_source(source = source, stream = cfg_file) cfg_file.close() self.source_files[source] = filename #-------------------------------------------------- def remove_source(self, source): """Remove a source from the instance.""" _log.info('removing source <%s>', source) try: del self.__cfg_data[source] except KeyError: _log.warning("source <%s> doesn't exist", source) try: del self.source_files[source] except KeyError: pass #-------------------------------------------------- def reload_file_source(self, filename=None, encoding='utf8'): if filename not in self.source_files.values(): return for src, fname in self.source_files.items(): if fname == filename: self.add_file_source(source = src, filename = fname, encoding = encoding) # don't break the loop because there could be other sources # with the same file (not very reasonable, I know) #break #-------------------------------------------------- def add_cli(self, short_options='', long_options=None): """Add command line parameters to config data. short: string containing one-letter options such as u'h?' for -h -? long: list of strings 'conf-file=' -> --conf-file=<...> 'debug' -> --debug """ _log.info('adding command line arguments') _log.debug('raw command line is:') _log.debug('%s', sys.argv) import getopt if long_options is None: long_options = [] try: opts, remainder = getopt.gnu_getopt ( sys.argv[1:], short_options, long_options ) except getopt.GetoptError as exc: _log.exception('error parsing command line options') print('GNUmed startup: error loading command line options') print('GNUmed startup:', exc) return False data = {} for opt, val in opts: if val == '': data['%s::%s' % ('cli', opt)] = True else: data['%s::%s' % ('cli', opt)] = val self.__cfg_data['cli'] = data return True
A generic Borg mixin for new-style classes.
-
mixin this class with your class' ancestors to borg it
-
there may be many instances of this - PER CHILD CLASS - but they all share state
Ancestors
Methods
def add_cli(self, short_options='', long_options=None)
-
Expand source code
def add_cli(self, short_options='', long_options=None): """Add command line parameters to config data. short: string containing one-letter options such as u'h?' for -h -? long: list of strings 'conf-file=' -> --conf-file=<...> 'debug' -> --debug """ _log.info('adding command line arguments') _log.debug('raw command line is:') _log.debug('%s', sys.argv) import getopt if long_options is None: long_options = [] try: opts, remainder = getopt.gnu_getopt ( sys.argv[1:], short_options, long_options ) except getopt.GetoptError as exc: _log.exception('error parsing command line options') print('GNUmed startup: error loading command line options') print('GNUmed startup:', exc) return False data = {} for opt, val in opts: if val == '': data['%s::%s' % ('cli', opt)] = True else: data['%s::%s' % ('cli', opt)] = val self.__cfg_data['cli'] = data return True
Add command line parameters to config data.
short: string containing one-letter options such as u'h?' for -h -? long: list of strings 'conf-file=' -> –conf-file=<…> 'debug' -> –debug
def add_file_source(self, source=None, filename=None, encoding='utf8')
-
Expand source code
def add_file_source(self, source=None, filename=None, encoding='utf8'): """Add a source (a file) to the instance.""" _log.info('file source "%s": %s (%s)', source, filename, encoding) for existing_source, existing_file in self.source_files.items(): if existing_file == filename: if source != existing_source: _log.warning('file [%s] already known as source [%s]', filename, existing_source) _log.warning('adding it as source [%s] may provoke trouble', source) cfg_file = None if filename is not None: try: cfg_file = open(filename, mode = 'rt', encoding = encoding) except IOError: _log.error('cannot open [%s], keeping as dummy source', filename) if cfg_file is None: filename = None if source in self.__cfg_data: _log.warning('overriding source <%s> with dummy', source) self.__cfg_data[source] = {} else: self.add_stream_source(source = source, stream = cfg_file) cfg_file.close() self.source_files[source] = filename
Add a source (a file) to the instance.
def add_stream_source(self, source=None, stream=None, encoding=None)
-
Expand source code
def add_stream_source(self, source=None, stream=None, encoding=None): data = parse_INI_stream(stream = stream, encoding = encoding) if source in self.__cfg_data: _log.warning('overriding source <%s> with [%s]', source, stream) self.__cfg_data[source] = data
def get(self, group=None, option=None, source_order=None)
-
Expand source code
def get(self, group=None, option=None, source_order=None): """Get the value of a configuration option in a config file. <source_order> the order in which config files are searched a list of tuples (source, policy) policy: return: return only this value immediately append: append to list of potential values to return extend: if the value per source happens to be a list extend (rather than append to) the result list returns NONE when there's no value for an option """ if source_order is None: source_order = [('internal', 'return')] results = [] for source, policy in source_order: _log.debug('searching "%s" in [%s] in %s', option, group, source) if group is None: group = source option_path = '%s::%s' % (group, option) try: source_data = self.__cfg_data[source] except KeyError: _log.error('invalid config source [%s]', source) _log.debug('currently known sources: %s', list(self.__cfg_data)) continue try: value = source_data[option_path] except KeyError: _log.debug('option [%s] not in group [%s] in source [%s]', option, group, source) continue _log.debug('option [%s] found in source [%s]', option_path, source) if policy == 'return': return value if policy == 'extend': if isinstance(value, type([])): results.extend(value) else: results.append(value) else: results.append(value) if len(results) == 0: return None return results
Get the value of a configuration option in a config file.
the order in which config files are searched a list of tuples (source, policy) policy: return: return only this value immediately append: append to list of potential values to return extend: if the value per source happens to be a list extend (rather than append to) the result list returns NONE when there's no value for an option
def reload_file_source(self, filename=None, encoding='utf8')
-
Expand source code
def reload_file_source(self, filename=None, encoding='utf8'): if filename not in self.source_files.values(): return for src, fname in self.source_files.items(): if fname == filename: self.add_file_source(source = src, filename = fname, encoding = encoding) # don't break the loop because there could be other sources # with the same file (not very reasonable, I know) #break
def remove_source(self, source)
-
Expand source code
def remove_source(self, source): """Remove a source from the instance.""" _log.info('removing source <%s>', source) try: del self.__cfg_data[source] except KeyError: _log.warning("source <%s> doesn't exist", source) try: del self.source_files[source] except KeyError: pass
Remove a source from the instance.
def set_option(self, option=None, value=None, group=None, source=None)
-
Expand source code
def set_option(self, option=None, value=None, group=None, source=None): """Set a particular option to a particular value. Note that this does NOT PERSIST the option anywhere ! """ if None in [option, value]: raise ValueError('neither <option> nor <value> can be None') if source is None: source = 'internal' try: self.__cfg_data[source] except KeyError: self.__cfg_data[source] = {} if group is None: group = source option_path = '%s::%s' % (group, option) self.__cfg_data[source][option_path] = value
Set a particular option to a particular value.
Note that this does NOT PERSIST the option anywhere !
-