Search code examples
pythonconfigparser

Python configparser getting and setting without exceptions


Everytime you try to get or set to a section using configparser in Python it throws a NoSectionError if the section does not exist. Is there anyway to avoid this? Also, can I also avoid the NoOptionError when getting an option?

For example, using a dictionary, there is the setdefault option: instead of throwing a KeyError when the key does not exist, the dictionary adds the key, sets the key's value to the default value, and returns the default value.

I am currently doing the following for getting attributes:

def read_config(section):
    config = configparser.ConfigParser()
    config.read(location)
    try:
        apple = config.get(section, 'apple')
    except NoSectionError, NoOptionError:
        apple = None
    try:
        pear = config.get(section, 'pear')
    except NoSectionError, NoOptionError:
        pear = None
    try:
        banana = config(section, 'banana')
    except NoSectionError, NoOptionError:
        banana = None
    return apple, pear, banana

And the following for setting them:

def save_to_config(section, apple, pear, banana):
    config = configparser.ConfigParser()
    if not os.path.exists(folder_location):
        os.makedirs(folder_location)

    config.read(location)
    if section not in config.sections():
        config.add_section(section)

    config.set(section, 'apple', apple)
    config.set(section, 'pear', pear)
    config.set(section, 'banana', banana)

Setting isn't too bad because they all have the same section, but getting is well... terrible. There has got to be a better way.

Is there perhaps some one liner where I can reduce this:

try:
    apple = config.get(section, 'apple')
except NoSectionError, NoOptionError:
    apple = None

to this:

apple = config.get_with_default(section, 'apple', None)

-- EDIT --

I have tried to make the following changes per lego's suggestion:

def read_config(section):
    defaults = { section : {'apple': None,
                            'pear': None,
                            'banana': None }} 
    config = configparser.ConfigParser(defaults = defaults)
    config.read(location)

    apple = config.get(section, 'apple')
    pear = config.get(section, 'pear')
    banana = config(section, 'banana')

    return apple, pear, banana

But this still raises a NoSectionError if the section doesn't exist

Note: I have also tried it where defaults = just {'apple': None, 'pear': None, 'banana': None } (no section)


Solution

  • An alternative approach:

    ConfigParser.get offers a vars parameter that can be passed in, which is used as the primary lookup if it's provided, however, it ignores whether there exists a value for the option already on the section.

    We can, therefore, use vars via ducktyping, but we'll change the behavior of .items() to the following:

    • If the config object has the option we're already looking for, we'll take that.
    • Otherwise, we'll return the default from vars.

    Here's a very naive implementation:

    class DefaultOption(dict):
    
        def __init__(self, config, section, **kv):
            self._config = config
            self._section = section
            dict.__init__(self, **kv)
    
        def items(self):
            _items = []
            for option in self:
                if not self._config.has_option(self._section, option):
                    _items.append((option, self[option]))
                else:
                    value_in_config = self._config.get(self._section, option)
                    _items.append((option, value_in_config))
            return _items
    

    In usage:

    def read_config(section, location):
        config = configparser.ConfigParser()
        config.read(location)
        apple = config.get(section, 'apple',
                           vars=DefaultOption(config, section, apple=None))
        pear = config.get(section, 'pear',
                          vars=DefaultOption(config, section, pear=None))
        banana = config.get(section, 'banana',
                            vars=DefaultOption(config, section, banana=None))
        return apple, pear, banana
    
    def save_to_config(section, location, apple, pear, banana):
        config = configparser.ConfigParser()
    
        config.read(location)
        if section not in config.sections():
            config.add_section(section)
    
        config.set(section, 'apple', apple)
        config.set(section, 'pear', pear)
        with open(location, 'wb') as cf:
            config.write(cf)
    

    That being said, this is a little indirect, but perfectly valid.

    Note, that this will still raise NoSectionError.

    If you're trying to handle that as well, ConfigParser.ConfigParser takes a dict_type parameter, so you just instantiate the class with a defaultdict.

    So, change configparser.ConfigParser() to configparser.ConfigParser(dict_type=lambda: defaultdict(list))

    For all intents and purposes though, I'd probably use Lego's suggestions.

    Update for original question edit

    If you want to use the defaults keyword into ConfigParser, it might help to look at how the implementation is defined. Here's the ConfigParser.__init__() code for how defaults are initialized. You'll see that defaults are used completely differently than sections. To dive a bit deeper about the role they play during get(), look at the code for ConfigParser.get(). Basically, if the section isn't DEFAULTSECT, then a NoSectionError is thrown.

    You have two ways to overcome this:

    1. Use the defaultdict idea I proposed above
    2. Modify your read_config function slightly
    def read_config(section):
        defaults = {'apple': None,
                    'pear': None,
                    'banana': None }
        config = configparser.ConfigParser(defaults = defaults)
        config.read(location)
        if not config.has_section(section):
            config.add_section(section)
    
        apple = config.get(section,'apple')
        pear = config.get(section, 'pear')
        banana = config.get(section, 'banana')
    
        return apple, pear, banana
    

    But I say, since this is not a one-liner, it opens up a lot more paths like the DefaultOption class I offered. We can make it even a bit less verbose.