saltstack/salt

View on GitHub
salt/utils/configparser.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-
'''
Custom configparser classes
'''
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import re

# Import Salt libs
import salt.utils.stringutils

# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves.configparser import *  # pylint: disable=no-name-in-module,wildcard-import

try:
    from collections import OrderedDict as _default_dict
except ImportError:
    # fallback for setup.py which hasn't yet built _collections
    _default_dict = dict


# pylint: disable=string-substitution-usage-error
class GitConfigParser(RawConfigParser, object):  # pylint: disable=undefined-variable
    '''
    Custom ConfigParser which reads and writes git config files.

    READ A GIT CONFIG FILE INTO THE PARSER OBJECT

    >>> import salt.utils.configparser
    >>> conf = salt.utils.configparser.GitConfigParser()
    >>> conf.read('/home/user/.git/config')

    MAKE SOME CHANGES

    >>> # Change user.email
    >>> conf.set('user', 'email', 'myaddress@mydomain.tld')
    >>> # Add another refspec to the "origin" remote's "fetch" multivar
    >>> conf.set_multivar('remote "origin"', 'fetch', '+refs/tags/*:refs/tags/*')

    WRITE THE CONFIG TO A FILEHANDLE

    >>> import salt.utils.files
    >>> with salt.utils.files.fopen('/home/user/.git/config', 'w') as fh:
    ...     conf.write(fh)
    >>>
    '''
    DEFAULTSECT = 'DEFAULT'
    SPACEINDENT = ' ' * 8

    def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=True):
        '''
        Changes default value for allow_no_value from False to True
        '''
        super(GitConfigParser, self).__init__(defaults, dict_type, allow_no_value)

    def _read(self, fp, fpname):
        '''
        Makes the following changes from the RawConfigParser:

        1. Strip leading tabs from non-section-header lines.
        2. Treat 8 spaces at the beginning of a line as a tab.
        3. Treat lines beginning with a tab as options.
        4. Drops support for continuation lines.
        5. Multiple values for a given option are stored as a list.
        6. Keys and values are decoded to the system encoding.
        '''
        cursect = None                        # None, or a dictionary
        optname = None
        lineno = 0
        e = None                              # None, or an exception
        while True:
            line = salt.utils.stringutils.to_unicode(fp.readline())
            if not line:
                break
            lineno = lineno + 1
            # comment or blank line?
            if line.strip() == '' or line[0] in '#;':
                continue
            if line.split(None, 1)[0].lower() == 'rem' and line[0] in 'rR':
                # no leading whitespace
                continue
            # Replace space indentation with a tab. Allows parser to work
            # properly in cases where someone has edited the git config by hand
            # and indented using spaces instead of tabs.
            if line.startswith(self.SPACEINDENT):
                line = '\t' + line[len(self.SPACEINDENT):]
            # is it a section header?
            mo = self.SECTCRE.match(line)
            if mo:
                sectname = mo.group('header')
                if sectname in self._sections:
                    cursect = self._sections[sectname]
                elif sectname == self.DEFAULTSECT:
                    cursect = self._defaults
                else:
                    cursect = self._dict()
                    self._sections[sectname] = cursect
                # So sections can't start with a continuation line
                optname = None
            # no section header in the file?
            elif cursect is None:
                raise MissingSectionHeaderError(  # pylint: disable=undefined-variable
                    salt.utils.stringutils.to_str(fpname),
                    lineno,
                    salt.utils.stringutils.to_str(line))
            # an option line?
            else:
                mo = self._optcre.match(line.lstrip())
                if mo:
                    optname, vi, optval = mo.group('option', 'vi', 'value')
                    optname = self.optionxform(optname.rstrip())
                    if optval is None:
                        optval = ''
                    if optval:
                        if vi in ('=', ':') and ';' in optval:
                            # ';' is a comment delimiter only if it follows
                            # a spacing character
                            pos = optval.find(';')
                            if pos != -1 and optval[pos-1].isspace():
                                optval = optval[:pos]
                        optval = optval.strip()
                        # Empty strings should be considered as blank strings
                        if optval in ('""', "''"):
                            optval = ''
                    self._add_option(cursect, optname, optval)
                else:
                    # a non-fatal parsing error occurred.  set up the
                    # exception but keep going. the exception will be
                    # raised at the end of the file and will contain a
                    # list of all bogus lines
                    if not e:
                        e = ParsingError(fpname)  # pylint: disable=undefined-variable
                    e.append(lineno, repr(line))
        # if any parsing errors occurred, raise an exception
        if e:
            raise e  # pylint: disable=raising-bad-type

    def _string_check(self, value, allow_list=False):
        '''
        Based on the string-checking code from the SafeConfigParser's set()
        function, this enforces string values for config options.
        '''
        if self._optcre is self.OPTCRE or value:
            is_list = isinstance(value, list)
            if is_list and not allow_list:
                raise TypeError('option value cannot be a list unless allow_list is True')
            elif not is_list:
                value = [value]
            if not all(isinstance(x, six.string_types) for x in value):
                raise TypeError('option values must be strings')

    def get(self, section, option, as_list=False):
        '''
        Adds an optional "as_list" argument to ensure a list is returned. This
        is helpful when iterating over an option which may or may not be a
        multivar.
        '''
        ret = super(GitConfigParser, self).get(section, option)
        if as_list and not isinstance(ret, list):
            ret = [ret]
        return ret

    def set(self, section, option, value=''):
        '''
        This is overridden from the RawConfigParser merely to change the
        default value for the 'value' argument.
        '''
        self._string_check(value)
        super(GitConfigParser, self).set(section, option, value)

    def _add_option(self, sectdict, key, value):
        if isinstance(value, list):
            sectdict[key] = value
        elif isinstance(value, six.string_types):
            try:
                sectdict[key].append(value)
            except KeyError:
                # Key not present, set it
                sectdict[key] = value
            except AttributeError:
                # Key is present but the value is not a list. Make it into a list
                # and then append to it.
                sectdict[key] = [sectdict[key]]
                sectdict[key].append(value)
        else:
            raise TypeError('Expected str or list for option value, got %s' % type(value).__name__)

    def set_multivar(self, section, option, value=''):
        '''
        This function is unique to the GitConfigParser. It will add another
        value for the option if it already exists, converting the option's
        value to a list if applicable.

        If "value" is a list, then any existing values for the specified
        section and option will be replaced with the list being passed.
        '''
        self._string_check(value, allow_list=True)
        if not section or section == self.DEFAULTSECT:
            sectdict = self._defaults
        else:
            try:
                sectdict = self._sections[section]
            except KeyError:
                raise NoSectionError(  # pylint: disable=undefined-variable
                    salt.utils.stringutils.to_str(section))
        key = self.optionxform(option)
        self._add_option(sectdict, key, value)

    def remove_option_regexp(self, section, option, expr):
        '''
        Remove an option with a value matching the expression. Works on single
        values and multivars.
        '''
        if not section or section == self.DEFAULTSECT:
            sectdict = self._defaults
        else:
            try:
                sectdict = self._sections[section]
            except KeyError:
                raise NoSectionError(  # pylint: disable=undefined-variable
                    salt.utils.stringutils.to_str(section))
        option = self.optionxform(option)
        if option not in sectdict:
            return False
        regexp = re.compile(expr)
        if isinstance(sectdict[option], list):
            new_list = [x for x in sectdict[option] if not regexp.search(x)]
            # Revert back to a list if we removed all but one item
            if len(new_list) == 1:
                new_list = new_list[0]
            existed = new_list != sectdict[option]
            if existed:
                del sectdict[option]
                sectdict[option] = new_list
            del new_list
        else:
            existed = bool(regexp.search(sectdict[option]))
            if existed:
                del sectdict[option]
        return existed

    def write(self, fp_):
        '''
        Makes the following changes from the RawConfigParser:

        1. Prepends options with a tab character.
        2. Does not write a blank line between sections.
        3. When an option's value is a list, a line for each option is written.
           This allows us to support multivars like a remote's "fetch" option.
        4. Drops support for continuation lines.
        '''
        convert = salt.utils.stringutils.to_bytes \
            if 'b' in fp_.mode \
            else salt.utils.stringutils.to_str
        if self._defaults:
            fp_.write(convert('[%s]\n' % self.DEFAULTSECT))
            for (key, value) in six.iteritems(self._defaults):
                value = salt.utils.stringutils.to_unicode(value).replace('\n', '\n\t')
                fp_.write(convert('%s = %s\n' % (key, value)))
        for section in self._sections:
            fp_.write(convert('[%s]\n' % section))
            for (key, value) in six.iteritems(self._sections[section]):
                if (value is not None) or (self._optcre == self.OPTCRE):
                    if not isinstance(value, list):
                        value = [value]
                    for item in value:
                        fp_.write(convert('\t%s\n' % ' = '.join((key, item)).rstrip()))