Module thunderlab.configfile

configfile

class ConfigFile: handling of configuration parameter.

Functions

def main()
Expand source code
def main():
    cfg = ConfigFile()
    cfg.add_section('Power spectrum:')
    cfg.add('nfft', 256, '', 'Number of data poinst for fourier transform.')
    cfg.add('windows', 4, '', 'Number of windows on which power spectra are computed.')
    cfg.add_section('Peaks:')
    cfg.add('threshold', 20.0, 'dB', 'Threshold for peak detection.')
    cfg.add('deltaf', 10.0, 'Hz', 'Minimum distance between peaks.')
    cfg.add('flip', True, '', 'Flip peaks.')
    cfg.write()
    print()
    print('set values:')
    s = cfg.set_values(['nfft: 1024, windows: 2', 'threshold: donkey', 'flip: false', 'something: blue'])
    if s is not None:
        print('errors:')
        for es in s:
            print('   ', es)
    cfg.write()
    print()
    print()

    print('delete nfft and windows:')
    del cfg['nfft']
    del cfg['windows']
    cfg.write()

Classes

class ConfigFile (orig=None)
Expand source code
class ConfigFile(dict):
    """Handling of configuration parameter.

    Configuration parameter have a name (key), a value, a unit, a
    description and a default value. ConfigFile is a dictionary with
    the parameter names as keys and the tuple (value, unit,
    description, default) as value.
    
    New parameter can be added with the add() function.

    Configuration parameter can be further structured in the
    configuration file by inserting section titles via add_section().

    The tuple (value, unit, description, default) can be retrieved via
    the name of a parameter using the [] operator.

    The value of a configuration parameter is retrieved by value()
    and set by set().

    Values of several configuration parameter can be mapped to
    new names with the map() function. The resulting dictionary can be
    passed as key-word arguments to a function.

    The configuration parameter can be written to a configuration file
    with dump() and loaded from a file with load() and load_files().
    
    Methods
    -------

    - `add()`: add a new parameter to the configuration.
    - `add_section()`: add a new section to the configuration.
    - `value()`: returns the value of the configuration parameter defined by key.
    - `set(): set the value of the configuration parameter defined by key.
    - `set_values(): set values of configuration parameters from list of str.
    - `map()`: map the values of the configuration onto new names.
    - `write()`: pretty print configuration into file or stream.
    - `load()`: set values of configuration to values from key-value pairs read in from file.
    - `load_files()`: load configuration from current working directory as well as from several levels of a file path.

    """

    def __init__(self, orig=None):
        super().__init__(dict())
        self.sections = dict()
        self.new_section = None
        if not orig is None:
            for k, v in orig.items():
                self[k] = list(v)
            for k, v in orig.sections.items():
                self.sections[k] = v
            self.new_section = None
    
    def add(self, key, value, unit, description):
        """Add a new parameter to the configuration.

        The description of the parameter is a single string. Newline
        characters are intepreted as new paragraphs.

        Parameters
        ----------
        key: str
            Key (name) of the parameter.
        value: any type
            Value of the parameter.
        unit: str
            Unit of the parameter value.
        description: str
            Textual description of the parameter.
        """
        # add a pending section:
        if self.new_section is not None:
            self.sections[key] = self.new_section
            self.new_section = None
        # add configuration parameter (4th element is default value):
        self[key] = [value, unit, description, value]

    def add_section(self, description):
        """Add a new section to the configuration.

        Parameters
        ----------
        description: str
            Textual description of the section
        """
        self.new_section = description
    
    def value(self, key):
        """Returns the value of the configuration parameter defined by key.

        Parameters
        ----------
        key: str
            Key of the configuration parameter.

        Returns
        -------
        value: any type
            Value of the configuraion parameter.
        """
        return self[key][0]

    def set(self, key, value):
        """Set the value of the configuration parameter defined by key.

        Parameters
        ----------
        key: str
            Key of the configuration parameter.
        value: any type
            The new value.

        Raises
        ------
        IndexError:
            If key does not exist.
        """
        if not key in self:
            raise IndexError(f'Key {key} does not exist')
        self[key][0] = value

    def set_values(self, values):
        """Set values of configuration parameters from list of str.
        
        Parameters
        ----------
        values: list of str
            Each string can be one or several key-value pairs
            separated by commas.
            A key-value pair is separated by colons.

        Returns
        -------
        errors: list of str
            Error messages.

        """
        errors = []
        for kvs in values:
            for kv in kvs.strip().split(','):
                if ':' in kv:
                    ss = kv.split(':')
                    key = ss[0].strip()
                    val = ':'.join(ss[1:]).strip()
                    if key in self:
                        vt = type(self[key][0])
                        if vt is bool:
                            if val.lower() in ['true', 'on', 'yes', 't', 'y']:
                                self[key][0] = True
                            elif val.lower() in ['false', 'off', 'no', 'f', 'n']:
                                self[key][0] = False
                            else:
                                errors.append(f'configuration parameter "{key}": cannot convert "{val}" to bool')
                        else:
                            try:
                                self[key][0] = vt(val)
                            except ValueError:
                                errors.append(f'configuration parameter "{key}": cannot convert "{val}" to {vt}')
                    else:
                        errors.append(f'key "{key}" not found in configuration parameters')
        return errors if len(errors) > 0 else None

    def __delitem__(self, key):
        """Remove an entry from the configuration.

        Parameters
        ----------
        key: str
            Key of the configuration parameter to be removed.
        """
        if key in self.sections:
            sec = self.sections.pop(key)
            keys = list(self.keys())
            inx = keys.index(key)+1
            if inx < len(keys):
                next_key = keys[inx]
                if not next_key in self.sections:
                    self.sections[next_key] = sec
        super().__delitem__(key)
        
    def map(self, mapping):
        """Map the values of the configuration onto new names.

        Use this function to generate key-word arguments
        that can be passed on to functions.

        Parameters
        ----------
        mapping: dict
            Dictionary with its keys being the new names
            and its values being the parameter names of the configuration.

        Returns
        -------
        a: dict
            A dictionary with the keys of mapping
            and the corresponding values retrieved from the configuration
            using the values from mapping.
        """
        a = {}
        for dest, src in mapping.items():
            if src in self:
                a[dest] = self.value(src)
        return a
    
    def write(self, fh=sys.stdout, header=None,
              diff_only=False, maxline=60, comments=True):
        """Pretty print configuration into file or stream.

        The description of a configuration parameter is printed out
        right before its key-value pair with an initial comment
        character ('#').  

        Section titles get two comment characters prependend ('##').

        Lines are folded if the character count of parameter
        descriptions or section title exceeds maxline.
        
        A header can be printed initially. This is a simple string that is
        formatted like the section titles.

        Parameters
        ----------
        fh: str or Path or file object
            Stream or file name for writing the configuration.
        header: str
            A string that is written as an introductory comment into the file.
        diff_only: bool
            If true write out only those parameters whose value differs from their default.
        maxline: int
            Maximum number of characters that fit into a line.
        comments: boolean
            Print out descriptions as comments if True.
        """

        def write_comment(fh, comment, maxline, cs):
            # format comment:
            if len(comment) > 0:
                for line in comment.split('\n'):
                    fh.write(cs + ' ')
                    cc = len(cs) + 1  # character count
                    for w in line.strip().split(' '):
                        # line too long?
                        if cc + len(w) > maxline:
                            fh.write('\n' + cs + ' ')
                            cc = len(cs) + 1
                        fh.write(w + ' ')
                        cc += len(w) + 1
                    fh.write('\n')

        # open file:
        own_file = False
        if not hasattr(fh, 'write'):
            fh = open(fh, 'w')
            own_file = True
        # write header:
        First = True
        if comments and not header is None:
            write_comment(fh, header, maxline, '##')
            First = False
        # get length of longest key:
        maxkey = 0
        for key in self.keys():
            if maxkey < len(key):
                maxkey = len(key)
        # write out parameter:
        section = ''
        for key, v in self.items():
            # possible section entry:
            if comments and key in self.sections:
                section = self.sections[key]
            # get value, unit, and comment from v:
            val = None
            unit = ''
            comment = ''
            differs = False
            if hasattr(v, '__len__') and (not isinstance(v, str)):
                val = v[0]
                if len(v) > 1 and len(v[1]) > 0:
                    unit = ' ' + v[1]
                if len(v) > 2:
                    comment = v[2]
                if len(v) > 3:
                    differs = (val != v[3])
            else:
                val = v
            # only write parameter whose value differs:
            if diff_only and not differs:
                continue
            # write out section
            if len(section) > 0:
                if not First:
                    fh.write('\n\n')
                write_comment(fh, section, maxline, '##')
                section = ''
                First = False
            # write key-value pair:
            if comments :
                fh.write('\n')
                write_comment(fh, comment, maxline, '#')
            fh.write('{key:<{width}s}: {val}{unit:s}\n'.format(
                key=key, width=maxkey, val=val, unit=unit))
            First = False
        # close file:
        if own_file:
            fh.close()
            
    def load(self, filename):
        """Set values of configuration to values from key-value pairs read in from file.

        Parameters
        ----------
        filename: str or Path
            Name of the file from which to read the configuration.

        """
        with open(filename, 'r') as f:
            for line in f:
                # do not process empty lines and comments:
                if len(line.strip()) == 0 or line[0] == '#' or not ':' in line:
                    continue
                # parse key value pair:
                key, val = line.split(':', 1)
                key = key.strip()
                # only read values of existing keys:
                if not key in self:
                    continue
                cv = self[key]
                vals = val.strip().split(' ')
                if hasattr(cv, '__len__') and (not isinstance(cv, str)):
                    unit = ''
                    if len(vals) > 1:
                        unit = vals[1]
                    if unit != cv[1]:
                        print(f'unit for {key} is {unit} but should be {cv[1]}')
                    if type(cv[0]) == bool:
                        cv[0] = (vals[0].lower() == 'true'
                                 or vals[0].lower() == 'yes')
                    else:
                        try:
                            cv[0] = type(cv[0])(vals[0])
                        except ValueError:
                            cv[0] = vals[0]
                else:
                    if type(cv[0]) == bool:
                        self[key] = (vals[0].lower() == 'true'
                                     or vals[0].lower() == 'yes')
                    else:
                        try:
                            self[key] = type(cv)(vals[0])
                        except ValueError:
                            self[key] = vals[0]
                        
    def load_files(self, cfgfile, filepath, maxlevel=3, verbose=0):
        """Load configuration from current working directory as well as from several levels of a file path.

        Parameters
        ----------
        cfgfile: str or Path
            Name of the configuration file (without any path).
        filepath: str or Path
            Path of a file. Configuration files are read in from different levels
            of the expanded path.
        maxlevel: int
            Read configuration files from up to maxlevel parent directories.
        verbose: int
            If greater than zero, print out from which files configuration has been loaded.
        """
        # load configuration from the current directory:
        cfgfile = Path(cfgfile)
        if cfgfile.is_file():
            if verbose > 0:
                print(f'load configuration {cfgfile}')
            self.load(cfgfile)

        # load configuration files from higher directories:
        filepath = Path(filepath)
        parents = filepath.resolve().parents
        for k in range(min(maxlevel, len(parents))):
            path = parents[k] / cfgfile
            if path.is_file():
                if verbose > 0:
                    print(f'load configuration {path}')
                self.load(path)

Handling of configuration parameter.

Configuration parameter have a name (key), a value, a unit, a description and a default value. ConfigFile is a dictionary with the parameter names as keys and the tuple (value, unit, description, default) as value.

New parameter can be added with the add() function.

Configuration parameter can be further structured in the configuration file by inserting section titles via add_section().

The tuple (value, unit, description, default) can be retrieved via the name of a parameter using the [] operator.

The value of a configuration parameter is retrieved by value() and set by set().

Values of several configuration parameter can be mapped to new names with the map() function. The resulting dictionary can be passed as key-word arguments to a function.

The configuration parameter can be written to a configuration file with dump() and loaded from a file with load() and load_files().

Methods

  • add(): add a new parameter to the configuration.
  • add_section(): add a new section to the configuration.
  • value(): returns the value of the configuration parameter defined by key.
  • `set(): set the value of the configuration parameter defined by key.
  • `set_values(): set values of configuration parameters from list of str.
  • map(): map the values of the configuration onto new names.
  • write(): pretty print configuration into file or stream.
  • load(): set values of configuration to values from key-value pairs read in from file.
  • load_files(): load configuration from current working directory as well as from several levels of a file path.

Ancestors

  • builtins.dict

Methods

def add(self, key, value, unit, description)
Expand source code
def add(self, key, value, unit, description):
    """Add a new parameter to the configuration.

    The description of the parameter is a single string. Newline
    characters are intepreted as new paragraphs.

    Parameters
    ----------
    key: str
        Key (name) of the parameter.
    value: any type
        Value of the parameter.
    unit: str
        Unit of the parameter value.
    description: str
        Textual description of the parameter.
    """
    # add a pending section:
    if self.new_section is not None:
        self.sections[key] = self.new_section
        self.new_section = None
    # add configuration parameter (4th element is default value):
    self[key] = [value, unit, description, value]

Add a new parameter to the configuration.

The description of the parameter is a single string. Newline characters are intepreted as new paragraphs.

Parameters

key : str
Key (name) of the parameter.
value : any type
Value of the parameter.
unit : str
Unit of the parameter value.
description : str
Textual description of the parameter.
def add_section(self, description)
Expand source code
def add_section(self, description):
    """Add a new section to the configuration.

    Parameters
    ----------
    description: str
        Textual description of the section
    """
    self.new_section = description

Add a new section to the configuration.

Parameters

description : str
Textual description of the section
def value(self, key)
Expand source code
def value(self, key):
    """Returns the value of the configuration parameter defined by key.

    Parameters
    ----------
    key: str
        Key of the configuration parameter.

    Returns
    -------
    value: any type
        Value of the configuraion parameter.
    """
    return self[key][0]

Returns the value of the configuration parameter defined by key.

Parameters

key : str
Key of the configuration parameter.

Returns

value : any type
Value of the configuraion parameter.
def set(self, key, value)
Expand source code
def set(self, key, value):
    """Set the value of the configuration parameter defined by key.

    Parameters
    ----------
    key: str
        Key of the configuration parameter.
    value: any type
        The new value.

    Raises
    ------
    IndexError:
        If key does not exist.
    """
    if not key in self:
        raise IndexError(f'Key {key} does not exist')
    self[key][0] = value

Set the value of the configuration parameter defined by key.

Parameters

key : str
Key of the configuration parameter.
value : any type
The new value.

Raises

Indexerror

If key does not exist.

def set_values(self, values)
Expand source code
def set_values(self, values):
    """Set values of configuration parameters from list of str.
    
    Parameters
    ----------
    values: list of str
        Each string can be one or several key-value pairs
        separated by commas.
        A key-value pair is separated by colons.

    Returns
    -------
    errors: list of str
        Error messages.

    """
    errors = []
    for kvs in values:
        for kv in kvs.strip().split(','):
            if ':' in kv:
                ss = kv.split(':')
                key = ss[0].strip()
                val = ':'.join(ss[1:]).strip()
                if key in self:
                    vt = type(self[key][0])
                    if vt is bool:
                        if val.lower() in ['true', 'on', 'yes', 't', 'y']:
                            self[key][0] = True
                        elif val.lower() in ['false', 'off', 'no', 'f', 'n']:
                            self[key][0] = False
                        else:
                            errors.append(f'configuration parameter "{key}": cannot convert "{val}" to bool')
                    else:
                        try:
                            self[key][0] = vt(val)
                        except ValueError:
                            errors.append(f'configuration parameter "{key}": cannot convert "{val}" to {vt}')
                else:
                    errors.append(f'key "{key}" not found in configuration parameters')
    return errors if len(errors) > 0 else None

Set values of configuration parameters from list of str.

Parameters

values : list of str
Each string can be one or several key-value pairs separated by commas. A key-value pair is separated by colons.

Returns

errors : list of str
Error messages.
def map(self, mapping)
Expand source code
def map(self, mapping):
    """Map the values of the configuration onto new names.

    Use this function to generate key-word arguments
    that can be passed on to functions.

    Parameters
    ----------
    mapping: dict
        Dictionary with its keys being the new names
        and its values being the parameter names of the configuration.

    Returns
    -------
    a: dict
        A dictionary with the keys of mapping
        and the corresponding values retrieved from the configuration
        using the values from mapping.
    """
    a = {}
    for dest, src in mapping.items():
        if src in self:
            a[dest] = self.value(src)
    return a

Map the values of the configuration onto new names.

Use this function to generate key-word arguments that can be passed on to functions.

Parameters

mapping : dict
Dictionary with its keys being the new names and its values being the parameter names of the configuration.

Returns

a : dict
A dictionary with the keys of mapping and the corresponding values retrieved from the configuration using the values from mapping.
def write(self, fh=sys.stdout, header=None, diff_only=False, maxline=60, comments=True)
Expand source code
def write(self, fh=sys.stdout, header=None,
          diff_only=False, maxline=60, comments=True):
    """Pretty print configuration into file or stream.

    The description of a configuration parameter is printed out
    right before its key-value pair with an initial comment
    character ('#').  

    Section titles get two comment characters prependend ('##').

    Lines are folded if the character count of parameter
    descriptions or section title exceeds maxline.
    
    A header can be printed initially. This is a simple string that is
    formatted like the section titles.

    Parameters
    ----------
    fh: str or Path or file object
        Stream or file name for writing the configuration.
    header: str
        A string that is written as an introductory comment into the file.
    diff_only: bool
        If true write out only those parameters whose value differs from their default.
    maxline: int
        Maximum number of characters that fit into a line.
    comments: boolean
        Print out descriptions as comments if True.
    """

    def write_comment(fh, comment, maxline, cs):
        # format comment:
        if len(comment) > 0:
            for line in comment.split('\n'):
                fh.write(cs + ' ')
                cc = len(cs) + 1  # character count
                for w in line.strip().split(' '):
                    # line too long?
                    if cc + len(w) > maxline:
                        fh.write('\n' + cs + ' ')
                        cc = len(cs) + 1
                    fh.write(w + ' ')
                    cc += len(w) + 1
                fh.write('\n')

    # open file:
    own_file = False
    if not hasattr(fh, 'write'):
        fh = open(fh, 'w')
        own_file = True
    # write header:
    First = True
    if comments and not header is None:
        write_comment(fh, header, maxline, '##')
        First = False
    # get length of longest key:
    maxkey = 0
    for key in self.keys():
        if maxkey < len(key):
            maxkey = len(key)
    # write out parameter:
    section = ''
    for key, v in self.items():
        # possible section entry:
        if comments and key in self.sections:
            section = self.sections[key]
        # get value, unit, and comment from v:
        val = None
        unit = ''
        comment = ''
        differs = False
        if hasattr(v, '__len__') and (not isinstance(v, str)):
            val = v[0]
            if len(v) > 1 and len(v[1]) > 0:
                unit = ' ' + v[1]
            if len(v) > 2:
                comment = v[2]
            if len(v) > 3:
                differs = (val != v[3])
        else:
            val = v
        # only write parameter whose value differs:
        if diff_only and not differs:
            continue
        # write out section
        if len(section) > 0:
            if not First:
                fh.write('\n\n')
            write_comment(fh, section, maxline, '##')
            section = ''
            First = False
        # write key-value pair:
        if comments :
            fh.write('\n')
            write_comment(fh, comment, maxline, '#')
        fh.write('{key:<{width}s}: {val}{unit:s}\n'.format(
            key=key, width=maxkey, val=val, unit=unit))
        First = False
    # close file:
    if own_file:
        fh.close()

Pretty print configuration into file or stream.

The description of a configuration parameter is printed out right before its key-value pair with an initial comment character ('#').

Section titles get two comment characters prependend ('##').

Lines are folded if the character count of parameter descriptions or section title exceeds maxline.

A header can be printed initially. This is a simple string that is formatted like the section titles.

Parameters

fh : str or Path or file object
Stream or file name for writing the configuration.
header : str
A string that is written as an introductory comment into the file.
diff_only : bool
If true write out only those parameters whose value differs from their default.
maxline : int
Maximum number of characters that fit into a line.
comments : boolean
Print out descriptions as comments if True.
def load(self, filename)
Expand source code
def load(self, filename):
    """Set values of configuration to values from key-value pairs read in from file.

    Parameters
    ----------
    filename: str or Path
        Name of the file from which to read the configuration.

    """
    with open(filename, 'r') as f:
        for line in f:
            # do not process empty lines and comments:
            if len(line.strip()) == 0 or line[0] == '#' or not ':' in line:
                continue
            # parse key value pair:
            key, val = line.split(':', 1)
            key = key.strip()
            # only read values of existing keys:
            if not key in self:
                continue
            cv = self[key]
            vals = val.strip().split(' ')
            if hasattr(cv, '__len__') and (not isinstance(cv, str)):
                unit = ''
                if len(vals) > 1:
                    unit = vals[1]
                if unit != cv[1]:
                    print(f'unit for {key} is {unit} but should be {cv[1]}')
                if type(cv[0]) == bool:
                    cv[0] = (vals[0].lower() == 'true'
                             or vals[0].lower() == 'yes')
                else:
                    try:
                        cv[0] = type(cv[0])(vals[0])
                    except ValueError:
                        cv[0] = vals[0]
            else:
                if type(cv[0]) == bool:
                    self[key] = (vals[0].lower() == 'true'
                                 or vals[0].lower() == 'yes')
                else:
                    try:
                        self[key] = type(cv)(vals[0])
                    except ValueError:
                        self[key] = vals[0]

Set values of configuration to values from key-value pairs read in from file.

Parameters

filename : str or Path
Name of the file from which to read the configuration.
def load_files(self, cfgfile, filepath, maxlevel=3, verbose=0)
Expand source code
def load_files(self, cfgfile, filepath, maxlevel=3, verbose=0):
    """Load configuration from current working directory as well as from several levels of a file path.

    Parameters
    ----------
    cfgfile: str or Path
        Name of the configuration file (without any path).
    filepath: str or Path
        Path of a file. Configuration files are read in from different levels
        of the expanded path.
    maxlevel: int
        Read configuration files from up to maxlevel parent directories.
    verbose: int
        If greater than zero, print out from which files configuration has been loaded.
    """
    # load configuration from the current directory:
    cfgfile = Path(cfgfile)
    if cfgfile.is_file():
        if verbose > 0:
            print(f'load configuration {cfgfile}')
        self.load(cfgfile)

    # load configuration files from higher directories:
    filepath = Path(filepath)
    parents = filepath.resolve().parents
    for k in range(min(maxlevel, len(parents))):
        path = parents[k] / cfgfile
        if path.is_file():
            if verbose > 0:
                print(f'load configuration {path}')
            self.load(path)

Load configuration from current working directory as well as from several levels of a file path.

Parameters

cfgfile : str or Path
Name of the configuration file (without any path).
filepath : str or Path
Path of a file. Configuration files are read in from different levels of the expanded path.
maxlevel : int
Read configuration files from up to maxlevel parent directories.
verbose : int
If greater than zero, print out from which files configuration has been loaded.