14

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)

Adam Parkin
  • 17,891
  • 17
  • 66
  • 87
Nick Humrich
  • 14,905
  • 8
  • 62
  • 85

6 Answers6

12

There a few ways to handle this depending on how complex you want to get.

The simplest way is probably just chaining logic together. ConfigParser defines has_option to safely check if an option exists for a section.

apple = config.has_option(section,'apple') and config.get(section,'apple') or None

Alternatively, if you know ahead of time which options should have values you can set the defaults dictionary when instantiating the parser. This has the advantage of retaining and raising any errors for sections you don't know about.

 myDefaults = {'apple':None,'banana':None,'pear':None}
 config = configparser.ConfigParser(defaults=myDefaults)

As stated by Wogan you can create a wrapper function, but you can easily just again use has_option like so:

def get_with_default(config,section,name,default)
    if config.has_option(section,name):
        return config.get(section,name)
    else:
        return default
  • turns out this doesn't work. Even with the defaults, I get a no section error. Is there something missing? – Nick Humrich Jul 24 '14 at 20:15
  • I've offered 3 separate alternatives and links to the documentation, if its still not working I'd suggest expanding your question with more details and actual code along with what ever updates you've made. –  Jul 25 '14 at 01:18
  • Added my changes to the bottom of my question – Nick Humrich Jul 25 '14 at 15:30
8

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.

Mahmoud Abdelkader
  • 23,011
  • 5
  • 41
  • 54
2

Looks to me like you already have your answer in your question:

def get_with_default(section, name, default)
    try:
        return config.get(section, name)
    except (NoSectionError, NoOptionError):
        return default

An alternative could be to set up a dict with defaults for each section and call read_dict(defaults) on the configparser before loading your configuration from the file. This should ensure no section is missing & therefore no exception will be thrown.

buhtz
  • 10,774
  • 18
  • 76
  • 149
Wogan
  • 70,277
  • 5
  • 35
  • 35
2

You could write a wrapper function to have the dict.setdefault behavior if that's what you want:

def config_setdefault_get(config, section, key, default)
    result = default
    try:
        result = config.get(section, key)
    except NoSectionError:
        config.add_section(section)
        config.set(section, key, default)
    except NoOptionError:
        config.set(section, key, default)
    finally:
        return result
pcurry
  • 1,374
  • 11
  • 23
2

Handling the exception in a wrapper function is probably more efficient. If you must avoid the exception handling, a compound if does the trick.

def get_with_default(section,name,default):
    return config.get(section,name) if config.has_section(section) and config.has_option(section, name) else default
Robert Dean
  • 685
  • 5
  • 16
  • Couple of typos... Missing Colon on the function def and "option" should be "name" on the last IF. But it does work once those are fixed. :-) – blissweb Dec 09 '21 at 06:52
0

What about not repeating yourself? Then it's less of a hassle when you have to catch errors.

def read_config(section, default=None, *vars):
    config = configparser.ConfigParser()
    config.read(location)
    ret = []
    for var in vars:
        try:
            ret.append(config.get(section, var))
        except NoSectionError, NoOptionError:
            ret.append(default)
    return ret

Aw yes, Wogan already solved it this way...

jakab922
  • 359
  • 1
  • 2
  • 9